rsemu 0.0.4

A multiplatform emulator in pure Rust, built bottom-up on a generic framework.
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
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
//! The conformance runner for `SingleStepTests/r3000`.
//!
//! `ROADMAP.md` §0: *accuracy is measured, never asserted*. This is the
//! measurement, and for MIPS I it is the only corpus that exists —
//! [`SingleStepTests/r3000`](https://github.com/SingleStepTests/r3000), **MIT**
//! (verified against the upstream `LICENSE`, © 2025 SingleStepTests). 56 files,
//! one per instruction, 1000 vectors each: 56 000 single-instruction tests
//! covering essentially the whole MIPS I user-level integer set.
//!
//! # What it is worth, honestly
//!
//! The vectors were **generated by another emulator's interpreter** (ares,
//! ISC-licensed) rather than by silicon, which the corpus's own README says.
//! That makes it a peer opinion rather than an oracle: a disagreement is
//! evidence, not proof, and each one has to be argued against the manual. It
//! is still by far the best thing available, because it exercises the two
//! things that break naive R3000 models — the **branch delay slot** and the
//! **load delay slot** — explicitly, as fields of every vector's initial and
//! final state.
//!
//! Only the corpus's *output* is consumed here. No emulator source of any
//! licence was opened for any part of this core.
//!
//! # The ledger
//!
//! `ROADMAP.md` §0 asks every core to ship a known-failures ledger that only
//! ever shrinks. This one is **empty**. At the commit that added this file,
//! **55 000 of 55 000** vectors passed with none skipped — every file at
//! 1000/1000, `GTE.json.bin` excepted because the corpus does not ship one.
//!
//! Four things this core had wrong were found by running it, and each is now
//! a unit test as well:
//!
//! * an exception in the delay slot of an **untaken** branch must still set
//!   `Cause.BD` — the delay slot exists whichever way the branch went;
//! * a branch or jump inside another branch's delay slot measures its target
//!   from where its own delay slot really is, not from `pc + 4`;
//! * `REGIMM` decodes the comparison from **bit 0** of `rt` alone, so all 32
//!   encodings are branches, while only `10000` and `10001` link;
//! * `JALR` writes the `rd` it names, so `jalr $zero, $rs` discards the link
//!   rather than quietly writing `$31`.
//!
//! A fifth, `lw $t0,x; lw $t0,y`, is architecturally UNPREDICTABLE and the
//! corpus and this core now agree by choice rather than by obligation — see
//! `Exec::deliver`.
//!
//! # Running it
//!
//! The corpus is **downloaded, never vendored** (`ROADMAP.md` §1, §12), so the
//! test is gated on an environment variable naming the directory of
//! `.json.bin` files:
//!
//! ```text
//! scripts/fetch-testdata.sh mips-r3000
//! RSEMU_MIPS_TESTS=testdata/r3000/v1 cargo test --all-features mips -- --nocapture
//! ```
//!
//! `RSEMU_MIPS_TESTS_ONLY` takes a comma-separated list of substrings to
//! narrow a run down while iterating. Without the variable the test prints why
//! it did nothing and passes, so `cargo test` stays hermetic and offline.
//!
//! The `.json.bin` container is read directly rather than through the
//! repository's `transcode_json.py`: it is a flat array of fixed-size records
//! (documented by that script) and parsing it needs no JSON reader, which the
//! dependency policy would not allow anyway.
//!
//! # What is compared, and what is not
//!
//! Compared: all 32 general registers, `HI`, `LO`, the program counter, the
//! **pending load** (which register and which value), whether the next
//! instruction is in a **branch delay slot** and where control goes after it,
//! `EPC`, and `Cause`'s exception code and `BD` bit.
//!
//! Not compared, and why:
//!
//! * **`Cause.CE`** — the corpus writes a coprocessor number into it on
//!   address errors, where the architecture leaves the field undefined. Ours
//!   stays zero.
//! * **`TAR`** — a register of the generating emulator's own; the R3000A's CP0
//!   register 6 (`JumpDest`) is a debug latch this core stores and does not
//!   update.
//! * **The per-cycle bus trace's `sz` field** — the corpus reports the number
//!   of bytes the instruction *needed*, and this core issues the access the
//!   silicon does (an aligned word for `LWL`/`LWR`, byte enables for
//!   `SWL`/`SWR`). The addresses and the values *are* compared, through
//!   memory: every read cycle seeds a word, and every write cycle is checked
//!   against what the core left there.
//!
//! # The address-space caveat
//!
//! The corpus treats memory as a flat 32-bit space with no segment decoding,
//! and puts both instructions and data at addresses inside `kseg0` and
//! `kseg1`. A real R3000 strips the top three bits of those, and so does this
//! core — so the physical address it presents to the bus is not the address
//! the corpus wrote. The runner therefore applies the **same** segment mapping
//! to the corpus's addresses when it seeds memory and when it checks it, which
//! keeps the two consistent. Two corpus addresses in one vector could in
//! principle alias onto one physical address; with random 32-bit values and at
//! most three addresses per vector that has not happened, and it would show up
//! as a failure rather than as a false pass.

use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

use crate::core::space::{AddressSpace, RamStore, Region, UnassignedPolicy};

use super::cp0::{Segment, cause_bits};
use super::{Arch, Config, Cpu};

/// One processor state, as a vector's `initial` or `final` half.
#[derive(Debug, Clone, PartialEq, Eq)]
struct VectorState {
    regs: [u32; 32],
    hi: u32,
    lo: u32,
    epc: u32,
    /// The generating emulator's target-address register. Not modelled here.
    _tar: u32,
    cause: u32,
    pc: u32,
    /// Whether the instruction at `pc` is in a branch delay slot.
    ///
    /// The container calls this `delay.load`, and the field names are the
    /// wrong way round in it: the object named `load` carries the *branch*
    /// delay and the one named `branch` carries the *load* delay. Named for
    /// what they hold rather than for what the file calls them.
    branch_slot: bool,
    /// Whether that branch was taken.
    branch_taken: bool,
    /// Where it goes if it was.
    branch_target: u32,
    /// Which register a pending load will write, or `None`.
    load_reg: Option<u32>,
    /// The value it will write.
    load_value: u32,
}

/// One bus cycle the corpus recorded.
#[derive(Debug, Clone, Copy)]
struct Cycle {
    /// A bit set: 4 is an instruction fetch, 1 a read, 2 a write.
    actions: u32,
    /// How many bytes the instruction needed.
    size: u32,
    /// The address, as the corpus's flat space numbers it.
    addr: u32,
    /// The whole 32-bit value on the bus. A byte or halfword store puts the
    /// **whole register** here, because that is what the pins carry and it is
    /// the bus that narrows it.
    value: u32,
}

impl Cycle {
    const fn is_write(self) -> bool {
        self.actions & 2 != 0
    }
}

/// One test vector.
#[derive(Debug, Clone)]
struct Vector {
    name: String,
    opcode: u32,
    opcode_addr: u32,
    initial: VectorState,
    expected: VectorState,
    cycles: Vec<Cycle>,
}

/// A little-endian reader over the container.
struct Reader<'a> {
    bytes: &'a [u8],
    at: usize,
}

impl<'a> Reader<'a> {
    const fn new(bytes: &'a [u8]) -> Reader<'a> {
        Reader { bytes, at: 0 }
    }

    fn u32(&mut self) -> Option<u32> {
        let slice = self.bytes.get(self.at..self.at + 4)?;
        self.at += 4;
        Some(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
    }

    fn u64(&mut self) -> Option<u64> {
        let lo = u64::from(self.u32()?);
        let hi = u64::from(self.u32()?);
        Some(lo | (hi << 32))
    }

    /// A Pascal string in a fixed-size field: one length byte, then the text,
    /// then padding.
    fn pascal(&mut self, field: usize) -> Option<String> {
        let slice = self.bytes.get(self.at..self.at + field)?;
        self.at += field;
        let len = usize::from(slice[0]).min(field - 1);
        Some(String::from_utf8_lossy(&slice[1..1 + len]).into_owned())
    }

    fn state(&mut self) -> Option<VectorState> {
        let mut regs = [0u32; 32];
        for slot in &mut regs {
            *slot = self.u32()?;
        }
        let hi = self.u32()?;
        let lo = self.u32()?;
        let epc = self.u32()?;
        let tar = self.u32()?;
        let cause = self.u32()?;
        let pc = self.u32()?;
        // The container's `load` object: the branch delay.
        let branch_target = self.u32()?;
        let branch_slot = self.u32()? != 0;
        let branch_taken = self.u32()? != 0;
        // The container's `branch` object: the load delay. A target of -1 is
        // "no pending load".
        let load_target = self.u32()? as i32;
        let load_value = self.u32()?;
        Some(VectorState {
            regs,
            hi,
            lo,
            epc,
            _tar: tar,
            cause,
            pc,
            branch_slot,
            branch_taken,
            branch_target,
            load_reg: (0..32).contains(&load_target).then_some(load_target as u32),
            load_value,
        })
    }

    fn vector(&mut self) -> Option<Vector> {
        let name = self.pascal(51)?;
        let opcode = self.u32()?;
        let opcode_addr = self.u32()?;
        let initial = self.state()?;
        let expected = self.state()?;
        let count = self.u32()? as usize;
        let mut cycles = Vec::with_capacity(count.min(64));
        for _ in 0..count {
            let value = self.u64()? as u32;
            let actions = self.u32()?;
            let addr = self.u64()? as u32;
            let size = self.u32()?;
            cycles.push(Cycle {
                actions,
                size,
                addr,
                value,
            });
        }
        Some(Vector {
            name,
            opcode,
            opcode_addr,
            initial,
            expected,
            cycles,
        })
    }
}

/// Parse a whole `.json.bin` file.
fn parse(bytes: &[u8]) -> Result<Vec<Vector>, String> {
    let mut r = Reader::new(bytes);
    let count = r.u32().ok_or("truncated header")? as usize;
    let mut out = Vec::with_capacity(count.min(4096));
    for i in 0..count {
        out.push(
            r.vector()
                .ok_or_else(|| format!("truncated at vector {i}"))?,
        );
    }
    Ok(out)
}

/// The physical address this core presents for a virtual one.
///
/// `kseg0` and `kseg1` strip the top three bits; everything else is the
/// identity on a part with no TLB, which is the configuration the corpus's
/// flat address space describes.
fn phys(vaddr: u32) -> u32 {
    let segment = Segment::of(vaddr);
    if segment.mapped() {
        vaddr
    } else {
        Segment::unmapped_phys(vaddr)
    }
}

/// The page size the runner maps memory in.
const PAGE: u32 = 0x1000;

/// Why a vector did not run.
#[derive(Debug, Clone, PartialEq, Eq)]
enum Skip {
    /// Two of the vector's addresses collide once the segment map has been
    /// applied, so the flat space the corpus assumes cannot be modelled.
    Aliased,
}

/// What one vector produced.
#[derive(Debug, Clone, PartialEq, Eq)]
enum Outcome {
    Pass,
    Fail(Vec<String>),
    Skipped(Skip),
}

/// Run one vector.
fn run_one(v: &Vector) -> Outcome {
    // -- memory ------------------------------------------------------------
    //
    // Map a page for every address the vector touches. Anything else faults
    // rather than reading zero, so a core that computed the wrong address is
    // caught rather than silently reading a plausible value.
    let mut pages: BTreeMap<u32, Arc<RamStore>> = BTreeMap::new();
    // The bytes memory holds when the instruction starts, and the bytes it
    // must hold when it finishes, both keyed by physical address.
    let mut seed: BTreeMap<u32, u8> = BTreeMap::new();
    let mut expected: BTreeMap<u32, u8> = BTreeMap::new();
    let mut words: BTreeSet<u32> = BTreeSet::new();
    let mut seen: BTreeMap<u32, u32> = BTreeMap::new();
    for c in &v.cycles {
        let word = c.addr & !3;
        let p = phys(word);
        if let Some(prior) = seen.insert(p, word)
            && prior != word
        {
            return Outcome::Skipped(Skip::Aliased);
        }
        words.insert(p);
        pages
            .entry(p & !(PAGE - 1))
            .or_insert_with(|| Arc::new(RamStore::new(u64::from(PAGE))));
        // One rule for reads and writes alike: the `size` bytes at `addr` are
        // the **low** `size` bytes of `value`, in address order. That is the
        // bus convention the corpus records — the datum sits in the low lanes
        // whatever its alignment — and it is why a byte read at an odd address
        // reports its byte in bits 7..0 rather than in the lane the address
        // would suggest.
        for i in 0..c.size.min(4) {
            let at = phys(c.addr).wrapping_add(i);
            let byte = ((c.value >> (8 * i)) & 0xff) as u8;
            // First cycle wins for the seed: within one instruction a read
            // happens before the write that follows it.
            seed.entry(at).or_insert(byte);
            if c.is_write() {
                expected.insert(at, byte);
            }
        }
    }
    let space = AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::FAULT);
    {
        let mut topology = space.topology();
        for (base, store) in &pages {
            topology
                .map(Region::ram("ram", Arc::clone(store)), u64::from(*base))
                .expect("a page fits");
        }
    }
    let byte_at = |map: &BTreeMap<u32, u8>, at: u32| map.get(&at).copied().unwrap_or(0);
    for (at, byte) in &seed {
        let store = &pages[&(at & !(PAGE - 1))];
        store
            .write_u8(u64::from(at & (PAGE - 1)), *byte)
            .expect("in range");
    }

    // -- the processor -----------------------------------------------------
    //
    // The LSI part, because the corpus's memory is flat: with no TLB, `kuseg`
    // and `kseg2` are the identity and only `kseg0`/`kseg1` are folded, which
    // is what `phys` above mirrors. Little-endian, which is what the corpus's
    // byte-level values say.
    let cpu = Cpu::new(Config::new(Arch::LR33300).with_reset_vector(v.initial.pc));
    cpu.attach_space(Arc::new(space));

    let s = &v.initial;
    for (i, value) in s.regs.iter().enumerate() {
        cpu.set_reg(i as u32, *value);
    }
    cpu.set_hi_lo(s.hi, s.lo);
    let next_pc = if s.branch_slot && s.branch_taken {
        s.branch_target
    } else {
        s.pc.wrapping_add(4)
    };
    cpu.set_control(s.pc, next_pc, s.branch_slot);
    let mut cp0 = cpu.cp0();
    cp0.epc = s.epc;
    // `Status` is not in the corpus at all, and the vectors' faults land on
    // `0x8000_0080` — the *cached* general vector — so `BEV` is clear and the
    // processor is in kernel mode with interrupts off, which is the only
    // configuration that produces those addresses.
    cp0.status = 0;
    // The whole of `Cause` is randomised in the initial state and preserved
    // across an instruction that raises nothing, so it is restored rather than
    // cleared — otherwise every non-faulting vector would disagree about the
    // leftover exception code. The six *hardware* interrupt bits are read live
    // off the pins by this core, so they are driven onto the pins instead of
    // being stored.
    cp0.cause = s.cause & !cause_bits::HW;
    cpu.set_cp0(cp0);
    for pin in 0..6 {
        let bit = 1 << (cause_bits::HW_SHIFT + pin);
        cpu.set_interrupt(pin, s.cause & bit != 0);
    }
    cpu.set_pending_load(s.load_reg.map(|reg| (reg, s.load_value)));

    cpu.step();

    // -- comparison --------------------------------------------------------
    let want = &v.expected;
    let mut bad: Vec<String> = Vec::new();
    for i in 0..32u32 {
        let got = cpu.reg(i);
        if got != want.regs[i as usize] {
            bad.push(format!(
                "r{i} = {got:#010x}, expected {:#010x}",
                want.regs[i as usize]
            ));
        }
    }
    if cpu.hi() != want.hi {
        bad.push(format!(
            "hi = {:#010x}, expected {:#010x}",
            cpu.hi(),
            want.hi
        ));
    }
    if cpu.lo() != want.lo {
        bad.push(format!(
            "lo = {:#010x}, expected {:#010x}",
            cpu.lo(),
            want.lo
        ));
    }
    if cpu.pc() != want.pc {
        bad.push(format!(
            "pc = {:#010x}, expected {:#010x}",
            cpu.pc(),
            want.pc
        ));
    }
    if cpu.in_delay_slot() != want.branch_slot {
        bad.push(format!(
            "delay slot = {}, expected {}",
            cpu.in_delay_slot(),
            want.branch_slot
        ));
    } else if want.branch_slot {
        // The corpus keeps the branch target even when the branch was not
        // taken; this core keeps only where control actually goes, so the
        // comparison is against that rather than against the raw field.
        let want_next = if want.branch_taken {
            want.branch_target
        } else {
            want.pc.wrapping_add(4)
        };
        if cpu.next_pc() != want_next {
            bad.push(format!(
                "next pc = {:#010x}, expected {want_next:#010x}",
                cpu.next_pc()
            ));
        }
    }
    let got_load = cpu.pending_load();
    let want_load = want.load_reg.map(|r| (r, want.load_value));
    if got_load != want_load {
        bad.push(format!(
            "pending load = {got_load:x?}, expected {want_load:x?}"
        ));
    }
    let cp0 = cpu.cp0();
    if cp0.epc != want.epc {
        bad.push(format!(
            "epc = {:#010x}, expected {:#010x}",
            cp0.epc, want.epc
        ));
    }
    let mask = cause_bits::EXC_CODE | cause_bits::BD;
    if cp0.cause & mask != want.cause & mask {
        bad.push(format!(
            "cause = {:#010x}, expected {:#010x} (masked to ExcCode and BD)",
            cp0.cause & mask,
            want.cause & mask
        ));
    }

    // Memory: every byte of every word the vector touched must hold what the
    // corpus says — the value a write cycle wrote, or the value that was there
    // before if nothing wrote it. Checking the untouched bytes is the half
    // that catches a store reaching outside its byte enables.
    for word in &words {
        let store = &pages[&(word & !(PAGE - 1))];
        for i in 0..4u32 {
            let at = word.wrapping_add(i);
            let got = store.read_u8(u64::from(at & (PAGE - 1))).expect("in range");
            let want = expected
                .get(&at)
                .copied()
                .unwrap_or_else(|| byte_at(&seed, at));
            if got != want {
                bad.push(format!(
                    "memory at {at:#010x} = {got:#04x}, expected {want:#04x}"
                ));
            }
        }
    }

    if bad.is_empty() {
        Outcome::Pass
    } else {
        Outcome::Fail(bad)
    }
}

/// Every `.json.bin` in a directory, in name order.
fn corpus_files(dir: &Path) -> Vec<PathBuf> {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return Vec::new();
    };
    let mut out: Vec<PathBuf> = entries
        .filter_map(std::result::Result::ok)
        .map(|e| e.path())
        .filter(|p| p.to_string_lossy().ends_with(".json.bin"))
        .collect();
    out.sort();
    out
}

#[test]
fn single_step_tests_r3000() {
    let Ok(dir) = std::env::var("RSEMU_MIPS_TESTS") else {
        println!(
            "conformance: RSEMU_MIPS_TESTS is not set, so nothing ran.\n\
             `scripts/fetch-testdata.sh mips-r3000` downloads the corpus \
             (SingleStepTests/r3000, MIT); the corpus is never committed."
        );
        return;
    };
    let only: Vec<String> = std::env::var("RSEMU_MIPS_TESTS_ONLY")
        .unwrap_or_default()
        .split(',')
        .filter(|s| !s.is_empty())
        .map(str::to_string)
        .collect();

    let files = corpus_files(Path::new(&dir));
    assert!(!files.is_empty(), "no .json.bin files under {dir}");

    let mut total = 0usize;
    let mut passed = 0usize;
    let mut skipped = 0usize;
    let mut failing_files: Vec<String> = Vec::new();
    for path in files {
        let name = path
            .file_name()
            .map(|n| n.to_string_lossy().replace(".json.bin", ""))
            .unwrap_or_default();
        if !only.is_empty() && !only.iter().any(|s| name.contains(s.as_str())) {
            continue;
        }
        let bytes = std::fs::read(&path).expect("the corpus is readable");
        let vectors = match parse(&bytes) {
            Ok(v) => v,
            Err(e) => panic!("{name}: {e}"),
        };
        let mut file_passed = 0usize;
        let mut file_skipped = 0usize;
        let mut first: Option<(String, Vec<String>)> = None;
        for v in &vectors {
            match run_one(v) {
                Outcome::Pass => file_passed += 1,
                Outcome::Skipped(_) => file_skipped += 1,
                Outcome::Fail(why) => {
                    if first.is_none() {
                        first = Some((
                            format!(
                                "{} (opcode {:#010x} at {:#010x})",
                                v.name, v.opcode, v.opcode_addr
                            ),
                            why,
                        ));
                    }
                }
            }
        }
        let ran = vectors.len() - file_skipped;
        total += ran;
        passed += file_passed;
        skipped += file_skipped;
        let failed = ran - file_passed;
        if failed == 0 {
            println!("{name:10} {file_passed:5}/{ran:<5} ok");
        } else {
            failing_files.push(name.clone());
            println!("{name:10} {file_passed:5}/{ran:<5} FAILED {failed}");
            if let Some((what, why)) = first {
                println!("           first: {what}");
                for line in why.iter().take(6) {
                    println!("             {line}");
                }
            }
        }
    }
    println!("conformance: {passed}/{total} vectors, {skipped} skipped");
    assert!(
        failing_files.is_empty(),
        "{} file(s) failed: {}",
        failing_files.len(),
        failing_files.join(", ")
    );
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_container_parser_rejects_a_truncated_file() {
        assert!(parse(&[]).is_err());
        assert!(parse(&[1, 0, 0, 0]).is_err());
        assert_eq!(parse(&[0, 0, 0, 0]).map(|v| v.len()), Ok(0));
    }

    #[test]
    fn the_segment_map_the_runner_mirrors_is_the_cores() {
        assert_eq!(phys(0x8123_4567), 0x0123_4567);
        assert_eq!(phys(0xa123_4567), 0x0123_4567);
        assert_eq!(phys(0x0123_4567), 0x0123_4567);
        assert_eq!(phys(0xc123_4567), 0xc123_4567);
    }
}