vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
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
645
646
647
648
649
650
651
652
653
//! The design image: everything a slang-free runner needs, as plain data.
//!
//! `vj build-aot` plans a design with slang, lowers it to machine code, and
//! embeds one [`DesignImage`] in the produced `.so`; the runner
//! (`vj-exec`) decodes it and assembles the same `StateEngine` + scheduler
//! `vj run` assembles, without ever touching slang. `vj run` itself
//! builds the same value from its own locals and instantiates it through the
//! same code, so there is exactly one boot sequence (a second one is how "card
//! vs sim" drift happens).
//!
//! The image holds no pointer and no hash-map iteration order: it is a
//! `Vec`-ordered value with a hand-written, versioned, little-endian codec
//! ([`codec`]), so equal designs encode to identical bytes.

pub mod codec;
pub mod covdecl;
pub mod verif;

use crate::collections::FrozenData;
use crate::netlist::{InstanceId, TemplateKey};
use crate::objects::TemplateSnapshot;
use crate::sched::{ProcessKind, SpawnSpec, WaitTableEntry};
use crate::state::{DriveStrengthPair, EdgeKind, FourStateValue, NetKind, StateLayout};

/// First eight bytes of an encoded image.
pub const IMAGE_MAGIC: [u8; 8] = *b"VJ-IMAGE";

/// Bumped on any change to the encoded layout.
pub const IMAGE_VERSION: u32 = 61;

/// Which kind of state instance an [`InstanceImage`] is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InstanceKind {
    /// An elaborated module instance (`registration_order`).
    Elaborated,
    /// A package's variables (a pseudo-instance with its own template).
    Package,
    /// The synthetic driver-contribution bank.
    Bank,
}

/// One unique `(module, parameters)` template and its state layout.
#[derive(Clone, Debug, PartialEq)]
pub struct TemplateImage {
    pub key: TemplateKey,
    pub layout: StateLayout,
}

/// One state instance, in allocation order (the order decides each instance's
/// arena range and `SimHandle`, so it is part of the design).
#[derive(Clone, Debug, PartialEq)]
pub struct InstanceImage {
    pub id: InstanceId,
    pub template: TemplateKey,
    pub kind: InstanceKind,
    /// Hierarchical path (`top.u0`), empty for package and bank instances.
    pub path: String,
}

/// An instance's foreign slots: slots `own..own + targets.len()` alias the
/// listed `(owner instance, owner slot)` cells.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForeignImage {
    pub instance: InstanceId,
    /// The instance's own slot count (what the first foreign slot follows).
    pub own_slots: u32,
    /// `Some`: the target names a window of the owner's slot (a `ref` port
    /// bound to a select, LRM 23.3.3.2) -- its own shape (width, class) is
    /// already the window's, laid out by `process_lower::SignalSlot`.
    pub targets: Vec<(InstanceId, u32, Option<crate::state::AliasWindow>)>,
}

/// A slot whose initial value is overwritten after the reset blit (a net starts
/// at Z, but the bits a driver drives start at X).
#[derive(Clone, Debug, PartialEq)]
pub struct XWindow {
    pub instance: InstanceId,
    pub slot: u32,
    pub value: FourStateValue,
}

/// A slot's storage class, as a wire or a resolver sees it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PlaceClass {
    /// At most 64 bits: the accessor ABI's `(val, unk)` scalar.
    Inline,
    /// More than 64 bits: `(aval, bval)` word arrays.
    Wide,
    /// An unpacked array: `rows` rows of one element each.
    Memory { rows: u32 },
}

/// One signal of one instance (the bank included), as a native process
/// addresses it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlaceImage {
    pub instance: InstanceId,
    /// The slot in the instance's own numbering (a foreign slot included).
    pub slot: u32,
    pub width: u32,
    pub class: PlaceClass,
}

/// One instance port connection, realized as LRM 23.3.3's synthetic
/// continuous assignment: a native process copying `src` to `dst`, with static
/// sensitivity on `src`'s cell. The list order is the registration order (after
/// the continuous assignments).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WireImage {
    pub src: PlaceImage,
    /// `Some((lo, width))`: only that sub-range of `src` is copied.
    pub src_range: Option<(u32, u32)>,
    pub dst: PlaceImage,
    /// `Some((lo, width))`: the value lands in only that sub-range of `dst`.
    pub dst_range: Option<(u32, u32)>,
    /// A bit-select/part-select connection (a read-modify-write of the
    /// destination), as opposed to a whole-signal or whole-memory copy.
    pub ranged: bool,
}

/// One driver of a resolved net: its contribution cell in the bank, its
/// strength, and its strength-ambiguity mask cell (the bits that are `L`/`H`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DriverImage {
    pub slot: u32,
    pub strength: DriveStrengthPair,
    pub ambiguous: Option<u32>,
    /// A unidirectional MOS gate's driver passes its data input's strength
    /// (LRM 28.4, Table 28-8), read at resolve time.
    pub src: Option<StrengthSrc>,
}

/// Where a MOS gate's driver takes its strength from: the data input net
/// (`%v` registry `key`, bit `bit`), lowered a level when `resistive`. The
/// driver drives bit `out_bit` of its net. `tick` is the input group's
/// strength-change cell, which wakes the output's resolver on a change in
/// strength only.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StrengthSrc {
    pub key: u64,
    pub bit: u32,
    pub out_bit: u32,
    pub resistive: bool,
    pub tick: u32,
}

/// A group of nets resolved together (`nets`: one, or several joined by
/// `inout` ports).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GroupImage {
    pub kind: NetKind,
    pub width: u32,
    /// `(instance, slot)` of each net.
    pub nets: Vec<(InstanceId, u32)>,
    pub drivers: Vec<DriverImage>,
    /// Part of a switch network, which resolves it.
    pub switched: bool,
    /// A `trireg` group's charge decay time in scheduler ticks.
    pub decay: Option<u64>,
    /// A `trireg` group's charge strength as a `%v` code (LRM 28.16.1): 5
    /// small, 4 medium (the default), 6 large.
    pub charge: u8,
    /// `%v` registry keys (`vj_runtime::fmt::strength_key`) of the group's
    /// nets, same order: the resolver records the group's per-bit strengths
    /// under each. Empty unless the design prints `%v`.
    pub strength_keys: Vec<u64>,
    /// A bank cell the resolver bumps whenever the group's strengths change
    /// (a MOS gate downstream reads them: [`StrengthSrc::tick`]).
    pub strength_tick: Option<u32>,
    /// The `$dumpports` port views of the group (LRM 21.7.4.3.2): empty
    /// unless the design calls `$dumpports`.
    pub sides: Vec<SideSpec>,
}

/// One dumped port's split of a group's drivers, for the resolver to resolve
/// separately (LRM 21.7.4.3.2: the test fixture's drivers and the DUT's own).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SideSpec {
    /// The registry key the per-side result is recorded under
    /// ([`DumpSignalImage::side_key`]).
    pub key: u64,
    /// Parallel to the group's `drivers`: the driver belongs to the dumped
    /// instance (the DUT side), not to the fixture around it.
    pub inside: Vec<bool>,
}

/// A bidirectional switch's control terminal.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ControlImage {
    /// Bit `bit` of a `width`-bit signal.
    Bit {
        instance: InstanceId,
        slot: u32,
        width: u32,
        bit: u32,
    },
    /// 0, 1, or 2 for unknown.
    Const(u8),
}

/// When a bidirectional switch conducts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConductsImage {
    Always,
    /// `tranif1` (`true`) / `tranif0` (`false`): when the control is 1 / 0.
    When(ControlImage, bool),
}

/// One bit-to-bit bidirectional switch: `(group, bit)` ends, where `group`
/// indexes [`NetworkImage::groups`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SwitchImage {
    pub a: (u32, u32),
    pub b: (u32, u32),
    pub conducts: ConductsImage,
    pub resistive: bool,
}

/// Groups connected by bidirectional switches, resolved together.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NetworkImage {
    /// Indices into [`DesignImage::groups`].
    pub groups: Vec<u32>,
    pub switches: Vec<SwitchImage>,
}

/// What a kernel is in the registration order.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KernelRole {
    /// A procedural block (`initial`/`always*`/`final`).
    Process,
    /// A module-level continuous assignment: an `AlwaysComb` process with zero
    /// frame words.
    ContAssign,
    /// A variable's non-constant initializer: a continuous assignment that
    /// seeds from its instance's initialization RNG.
    VarInit,
}

/// One compiled kernel the scheduler registers as a process.
#[derive(Clone, Debug, PartialEq)]
pub struct KernelImage {
    /// Index into [`DesignImage::symbols`]: the kernel function's name.
    pub symbol: u32,
    pub frame_words: u32,
    pub kind: ProcessKind,
    pub instance: InstanceId,
    pub role: KernelRole,
    /// `always @*`-style head: registered first, so it is already waiting when
    /// the time-zero updates change its inputs.
    pub level_head: bool,
    /// Runs in the Reactive region (a checker's procedures).
    pub reactive: bool,
    /// A program block's `initial` thread (LRM 24.3): the simulation ends
    /// implicitly once every such thread and its descendants have ended.
    pub program_initial: bool,
    /// `false` for a kernel the scheduler never registers (it only hosts an
    /// assertion's action blocks).
    pub registered: bool,
    /// `AlwaysComb`/`AlwaysLatch`: the template slots the whole procedure
    /// reads, each an "any change" edge.
    pub read_set: Vec<u32>,
    /// `AlwaysComb`/`AlwaysLatch`: `(slot, lsb, width)` windows narrowing a
    /// `read_set` slot the body reads only through constant selects (LRM
    /// 9.2.2.2.1); a slot without a window is sensitive to its whole value.
    pub read_windows: Vec<(u32, u32, u32)>,
    /// `AlwaysFF` with an asynchronous reset: its template slot and edge.
    pub reset: Option<(u32, EdgeKind)>,
    /// `ProcessCfg::disable_scopes` as `(resume pc, label, exit pc)`: what a
    /// `disable` of another process resumes this one at (LRM 9.6.2).
    pub disable_scopes: Vec<(u32, String, u32)>,
}

/// A fork branch's kernel (`SpawnSpec::kernel` indexes these).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForkBranchImage {
    pub symbol: u32,
    pub frame_words: u32,
    /// `(parent frame word, child frame word)` of each shared variable.
    pub captures: Vec<(u32, u32)>,
    /// The branch's `ProcessCfg::disable_scopes` as `(resume pc, label, exit
    /// pc)`, like [`KernelImage::disable_scopes`].
    pub disable_scopes: Vec<(u32, String, u32)>,
}

/// A VCD scope type (LRM 1800-2023 21.7.2.1): what kind of scope a dumped
/// signal sits in.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScopeKind {
    /// An elaborated module instance.
    Module,
    /// A generate block or a named sequential block.
    Begin,
    /// A named fork.
    Fork,
    Task,
    Function,
}

impl ScopeKind {
    /// The keyword a VCD `$scope` line uses.
    pub fn as_str(self) -> &'static str {
        match self {
            ScopeKind::Module => "module",
            ScopeKind::Begin => "begin",
            ScopeKind::Fork => "fork",
            ScopeKind::Task => "task",
            ScopeKind::Function => "function",
        }
    }
}

/// A VCD `$var` type (LRM 1800-2023 21.7.2.1 `var_type`; `parameter` is
/// not a dumped signal). A `uwire` is `wire` (21.7.2.3); `logic`/`bit`/`byte`
/// and the other 2/4-state vectors are `reg`; `Event` has no value, only the
/// marker of a trigger (21.7.2.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VcdVarType {
    Reg,
    Wire,
    Integer,
    Time,
    Real,
    RealTime,
    Supply0,
    Supply1,
    Tri,
    TriAnd,
    TriOr,
    TriReg,
    Tri0,
    Tri1,
    WAnd,
    WOr,
    Event,
}

impl VcdVarType {
    pub const ALL: [VcdVarType; 17] = [
        VcdVarType::Reg,
        VcdVarType::Wire,
        VcdVarType::Integer,
        VcdVarType::Time,
        VcdVarType::Real,
        VcdVarType::RealTime,
        VcdVarType::Supply0,
        VcdVarType::Supply1,
        VcdVarType::Tri,
        VcdVarType::TriAnd,
        VcdVarType::TriOr,
        VcdVarType::TriReg,
        VcdVarType::Tri0,
        VcdVarType::Tri1,
        VcdVarType::WAnd,
        VcdVarType::WOr,
        VcdVarType::Event,
    ];

    /// The keyword a VCD `$var` line uses.
    pub fn as_str(self) -> &'static str {
        match self {
            VcdVarType::Reg => "reg",
            VcdVarType::Wire => "wire",
            VcdVarType::Integer => "integer",
            VcdVarType::Time => "time",
            VcdVarType::Real => "real",
            VcdVarType::RealTime => "realtime",
            VcdVarType::Supply0 => "supply0",
            VcdVarType::Supply1 => "supply1",
            VcdVarType::Tri => "tri",
            VcdVarType::TriAnd => "triand",
            VcdVarType::TriOr => "trior",
            VcdVarType::TriReg => "trireg",
            VcdVarType::Tri0 => "tri0",
            VcdVarType::Tri1 => "tri1",
            VcdVarType::WAnd => "wand",
            VcdVarType::WOr => "wor",
            VcdVarType::Event => "event",
        }
    }
}

/// One statement-coverage site (LRM 40.2.1 a): the module definition it is
/// declared in and the source identity of the statement (the VPI side maps a
/// statement object to its site by it, 40.5.2).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StmtSiteImage {
    pub def: String,
    pub buffer: u32,
    pub start: u64,
    pub end: u64,
    /// The slang `StatementKind` as its raw `u16`.
    pub kind: u16,
    pub nest: u32,
}

/// An FSM named by the 40.4 pragmas, in one instance.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsmImage {
    /// The instance whose module carries the pragmas.
    pub instance: InstanceId,
    /// The FSM's name (40.4.2).
    pub name: String,
    /// The state vector's pieces, most significant first.
    pub parts: Vec<FsmPartImage>,
    /// The legal states (40.4.6): the parameter's name and its value.
    pub states: Vec<(String, u64)>,
}

/// One piece of an FSM state vector: bits `lsb..=msb` of a signal.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsmPartImage {
    pub slot: u32,
    pub msb: u32,
    pub lsb: u32,
}

/// One signal `$dumpvars` can select (LRM 21.7): a variable or net of an
/// instance, with the scope path the waveform shows it under. Memories
/// and collections are not dumpable and are not listed.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DumpSignalImage {
    pub instance: InstanceId,
    /// The name of the module definition `instance` instantiates (what
    /// `$coverage_*(SV_COV_MODULE, "def name")` selects by, LRM 40.3.2.1).
    pub def_name: String,
    /// Scope path, outermost first (`["top", "u1", "blk"]`).
    pub scope: Vec<String>,
    /// Each scope's VCD type, parallel to `scope`.
    pub scope_kinds: Vec<ScopeKind>,
    pub name: String,
    pub width: u32,
    /// The VCD `$var` type (LRM 21.7.2.1 `var_type`).
    pub var_type: VcdVarType,
    /// The slot in the instance's own numbering.
    pub slot: u32,
    pub wide: bool,
    /// A `real`/`shortreal`/`realtime`: the cell holds the IEEE bits.
    pub real: bool,
    /// A class-typed variable: an object-heap handle, never traced.
    pub class_handle: bool,
    /// The direction of the port this signal is the internal symbol of, for
    /// `$dumpports` (LRM 21.7.3): `None` for a signal that is no port.
    pub port: Option<PortDir>,
    /// For a port, the `%v` registry key (`vj_runtime::fmt::strength_key`)
    /// of the net whose recorded strengths and driver counts describe the
    /// port's fixture-side value: its own net, or for an input port connected
    /// to a whole parent net of the same width that net (the child's input
    /// net carries only a strong copy). `None`: no record (a variable, or a
    /// design without `$dumpports`), so the port is driven at strong by one
    /// driver.
    pub strength_key: Option<u64>,
    /// For a port, the key [`SideSpec::key`] records the fixture/DUT split
    /// of its net's drivers under; `None` when the net has no resolver (or
    /// the design has no `$dumpports`).
    pub side_key: Option<u64>,
}

/// A port's direction as extended VCD records it (LRM 21.7.4.3.1): an input
/// is dumped as the test fixture's value (`D`/`U`/`N`/`Z`), an output as the
/// device's (`L`/`H`/`X`/`T`), an inout/ref port with the unknown-direction
/// characters (`0`/`1`/`?`/`F`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PortDir {
    Input,
    Output,
    Inout,
}

/// What a design needs of DPI-C (LRM 1800-2023 35): the C functions its
/// imports call, and the SystemVerilog subroutines it exports to C.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DpiImage {
    /// The C identifier of every imported function or task, sorted. A runner
    /// checks each resolves before the design starts.
    pub imports: Vec<String>,
    /// Each exported C identifier and the [`DesignImage::symbols`] index of the
    /// function the C side reaches it through, sorted by identifier.
    pub exports: Vec<(String, u32)>,
    /// `(C identifier, instance scope, variant)`: which variant of an export
    /// each instance that exports it runs.
    pub variants: Vec<(String, String, u32)>,
    /// The design has a timed export task (LRM 35.7): every process kernel
    /// runs on a fiber, so that an import task's C code may block in it
    /// (`CompiledKernelProcess::with_task_fibers`).
    pub task_fibers: bool,
}

/// The run-wide timing options a design was built under (`vj`'s `TimingOptions`), in plain
/// data. They shape what the build computed (the SDF annotations it replays, the corner it
/// resolved, the negative-timing planning), so the runner may not change them: it refuses a
/// conflicting flag by name instead of running a different design from the one that was built.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TimingOptionsImage {
    /// `--timing-check-per-bit` (LRM 31.8).
    pub per_bit_vectors: bool,
    /// `--enable-negative-timing-checks` (LRM 31.9.4).
    pub negative_timing_checks: bool,
    /// `--timing-reject-limit-pct`, as the IEEE bits of the percentage.
    pub reject_limit_pct: Option<u64>,
    /// `--timing-error-limit-pct`, as the IEEE bits of the percentage.
    pub error_limit_pct: Option<u64>,
    /// `--pulse-style`: 0 on-event, 1 on-detect.
    pub pulse_style: Option<u8>,
    /// `--showcancelled` / `--noshowcancelled`.
    pub showcancelled: Option<bool>,
    /// `--sdf-mtm` (the corner `TOOL_CONTROL` selects): 0 minimum, 1 typical, 2 maximum.
    pub sdf_mtm: Option<u8>,
}

/// The design's timing section: the options it was built under and the timing plan (gate and
/// UDP primitives, delay lines, specify paths, timing checks, SDF annotation deltas) as an
/// opaque, deterministic byte string. Its typed codec lives in `vj-exec`, which owns the
/// code that runs it; this crate only carries and checksums it. Empty for a design with no
/// timing constructs.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TimingImage {
    pub options: TimingOptionsImage,
    pub plan: Vec<u8>,
}

/// Everything a runner needs to assemble and run one design.
#[derive(Clone, Debug, PartialEq)]
pub struct DesignImage {
    /// `vj_abi::VJ_ABI_VERSION` the kernels were compiled against.
    pub abi_version: u32,
    /// The scheduler tick exponent (`1e<tick_exp>` seconds), which `%t`
    /// defaults to.
    pub tick_exp: i32,
    /// Build default for `--x-initial-zero`.
    pub x_initial_zero: bool,
    pub templates: Vec<TemplateImage>,
    pub instances: Vec<InstanceImage>,
    pub foreign: Vec<ForeignImage>,
    pub x_windows: Vec<XWindow>,
    /// Port connection wires, in registration order.
    pub wires: Vec<WireImage>,
    /// Resolved net groups (resolvers register in group order, switch
    /// networks first).
    pub groups: Vec<GroupImage>,
    pub networks: Vec<NetworkImage>,
    /// Every function name the image refers to by index.
    pub symbols: Vec<String>,
    pub kernels: Vec<KernelImage>,
    pub fork_table: Vec<SpawnSpec>,
    pub fork_branches: Vec<ForkBranchImage>,
    pub wait_table: Vec<WaitTableEntry>,
    /// Every interned string but `""`, in id order (`crate::strings::snapshot`).
    /// A string's id is baked into machine code and into cells' initial
    /// values, so the runner re-interns these first, in this order.
    pub strings: Vec<Vec<u8>>,
    /// Every constant collection (`crate::collections::frozen_snapshot`), the
    /// same way: entry `i` is handle `(1 << 32) | (i + 1)`.
    pub collections: Vec<FrozenData>,
    /// Every class template's runtime metadata (`crate::objects::template_snapshot`),
    /// by ascending template id. Registered with the object heap before the
    /// design runs, so `release`, `copy`, `$cast` and the cycle collector see
    /// the same classes they saw in the process that built the image.
    pub classes: Vec<TemplateSnapshot>,
    /// Every dumpable signal, in the order `$dumpvars` numbers them.
    pub dump_signals: Vec<DumpSignalImage>,
    /// `Some(defining module name of each statement site)` when the code was
    /// generated with statement-coverage probes (`build-aot --coverage`, LRM
    /// 40.2.1 a); `None` otherwise.
    pub stmt_cov: Option<Vec<StmtSiteImage>>,
    /// The FSMs the 40.4 pragmas name, one per instance (LRM 40.2.1 c).
    pub fsms: Vec<FsmImage>,
    /// The design's DPI-C imports and exports.
    pub dpi: DpiImage,
    /// Assertions, checker free variables and sampled-value guards.
    pub verif: verif::VerifImage,
    /// Timing options and the timing plan (primitives, specify, timing checks, SDF).
    pub timing: TimingImage,
    /// The constraint solver's programs and `randomize()` call-site specs
    /// (`crate::constraints::registry::snapshot`), opaque here; empty for a
    /// design that never randomizes.
    pub randomize: Vec<u8>,
    /// The resolvers no [`GroupImage`] describes (user-defined nettypes,
    /// unpacked-array nets), opaque here: its typed codec lives in
    /// `vj-exec::usernets`, which owns the code that runs it. Empty for a
    /// design with none.
    pub native_nets: Vec<u8>,
    /// The VPI object model (`vj` `vpi::model::VpiModel::encode`: objects, handle
    /// relations, properties, expression nodes, name indexes), opaque here; empty for an
    /// image built without VPI. Present so a design runs the same user VPI libraries under
    /// the AOT runner as under the JIT, with no slang at run time.
    pub vpi: Vec<u8>,
}

impl DesignImage {
    /// An image with no content (for tests and as a builder's start).
    pub fn empty(abi_version: u32) -> Self {
        DesignImage {
            abi_version,
            tick_exp: 0,
            x_initial_zero: false,
            templates: Vec::new(),
            instances: Vec::new(),
            foreign: Vec::new(),
            x_windows: Vec::new(),
            wires: Vec::new(),
            groups: Vec::new(),
            networks: Vec::new(),
            symbols: Vec::new(),
            kernels: Vec::new(),
            fork_table: Vec::new(),
            fork_branches: Vec::new(),
            wait_table: Vec::new(),
            strings: Vec::new(),
            collections: Vec::new(),
            classes: Vec::new(),
            dump_signals: Vec::new(),
            stmt_cov: None,
            fsms: Vec::new(),
            dpi: DpiImage::default(),
            verif: verif::VerifImage::default(),
            timing: TimingImage::default(),
            randomize: Vec::new(),
            native_nets: Vec::new(),
            vpi: Vec::new(),
        }
    }

    /// Interns `name`, returning its index in [`Self::symbols`].
    pub fn intern(&mut self, name: &str) -> u32 {
        if let Some(i) = self.symbols.iter().position(|s| s == name) {
            return i as u32;
        }
        self.symbols.push(name.to_owned());
        (self.symbols.len() - 1) as u32
    }

    /// The name behind a symbol index.
    pub fn symbol(&self, index: u32) -> Option<&str> {
        self.symbols.get(index as usize).map(String::as_str)
    }

    /// The synthetic driver-contribution bank, if the design has one.
    pub fn bank(&self) -> Option<InstanceId> {
        self.instances
            .iter()
            .find(|i| i.kind == InstanceKind::Bank)
            .map(|i| i.id)
    }

    /// The layout of `key`'s template.
    pub fn layout_of(&self, key: &TemplateKey) -> Option<&StateLayout> {
        self.templates
            .iter()
            .find(|t| t.key == *key)
            .map(|t| &t.layout)
    }
}