vj-codegen 0.1.0

LLVM code generator of the vj SystemVerilog simulator.
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
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
//! Bridge to the sv-lang frontend: elaborate SV source into a frozen Design.
//!
//! Task 11 (A2-part2, `docs/superpowers/plans/2026-09-26-wave-A2-process-
//! kernels.md`; cross-track note: this builds wave-A codegen's own **CG-092**
//! row, not an A2 ledger ID -- see [`lower_all_instance_bodies`]'s own doc
//! comment) extends this file with a rayon-based, per-`InstanceBody` fan-out
//! entry point, additive to [`compile_source`]/[`find_function`] above.
//!
//! **Fan-out mechanism, reconciled at the wave-B integration merge
//! (docs/LEDGER.md "parallel per-InstanceBody lowering"):** this task
//! originally routed the fan-out through `Design::par_visit_from` (sv-lang's
//! optional `rayon` feature), claiming the whole design's `Instance` symbols
//! *inside* the parallel closure under a `Mutex<Vec<Symbol<'d>>>` dedup lock.
//! `sv-sim-codegen` Task 22 (`session.rs`) independently built the same
//! "one worker per unique InstanceBody" capability with plain
//! `rayon::prelude::*` over a caller-owned `&[Symbol]` slice collected up
//! front, which is the ONE fan-out mechanism this crate keeps (workspace
//! `Cargo.toml`'s `rayon` dependency comment has the full rationale;
//! `sv-lang`'s `rayon` feature/`Design::par_visit_from` is no longer used
//! anywhere in this crate). This function now matches that shape:
//! [`sv_lang::Symbol::visit`] (the plain, sequential walk -- `Design` has no
//! equivalent, but `design.root().visit(..)` reaches every symbol at every
//! nesting depth, the same set `par_visit_from` did) collects and
//! structural-type-dedupes every `Instance` body up front, single-threaded
//! (cheap: a symbol-tree walk, not the expensive lowering itself -- no
//! `Mutex` needed, since nothing here runs concurrently yet), and only the
//! resulting already-deduped slice is hand to `rayon`'s `par_iter` for the
//! actual, expensive per-unit lowering -- so no `Context`/`Module` is ever
//! reachable from more than one thread, matching BINDING CORRECTION 3
//! exactly as `session.rs` does. This is still genuinely "rayon-based
//! codegen parallelism ... one worker per unique InstanceBody" (CG-092's own
//! literal requirement text); `tests/parallel_lowering.rs`'s
//! `lowering_a_5module_design_uses_more_than_one_worker_thread` still proves
//! real, observed multi-thread execution under this shape.
use crate::types::TypeWidth;
use std::collections::HashMap;
use std::thread::ThreadId;
use sv_sim_sim::symid::SymbolUid;

use inkwell::context::Context;
use rayon::prelude::*;

use sv_lang::kinds::SymbolKind;
use sv_lang::{Compilation, Design, Session, Symbol, Walk};
use sv_sim_sim::symid::SymbolId;

use sv_sim_sim::process::cfg::{
    ProcessLoopKind, build_cfg, contains_unschedulable_fork_join, lower_to_segments,
};
use sv_sim_sim::process::liveness::compute_liveness;

use crate::accessor::declare_accessor_fns;
use crate::error::LoweringError;
use crate::module::{
    CombSensitivityIndex, SliceProcKind, build_comb_sensitivity_index, discover_processes,
};
use crate::process_lower::{
    SignalSlot, SignalSlotClass, build_lowering_plan, lower_kernel_fn_with_repr,
};
use crate::value::ValueRepr;

/// Extra slang driver options every [`compilation`] of this process uses
/// (set once by the command line): top-level parameter overrides
/// (`-G NAME=VALUE`, an invocation option the LRM leaves to tools, like
/// Verilator's and Icarus's `-G`/`-P`) and compatibility switches such as
/// `--allow-use-before-declare`.
static DRIVER_OPTIONS: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());

/// Sets the extra driver options of every later [`compilation`].
pub fn set_driver_options(options: Vec<String>) {
    if let Ok(mut o) = DRIVER_OPTIONS.lock() {
        *o = options;
    }
}

/// The return type of a user system function declared to slang.
#[derive(Clone, Copy, Debug)]
pub enum SystfReturn {
    Typed(sv_lang::SystemFunctionType),
    /// A `vpiSizedFunc` / `vpiSizedSignedFunc`: `logic [width-1:0]`, signed
    /// or not, `width` as its `sizetf` reported (38.37.1).
    Sized {
        width: u32,
        signed: bool,
    },
}

/// A user system function to declare: `(name, return type, number of
/// arguments)`, every argument `integer`.
pub type SystfDeclaration = (String, SystfReturn, usize);

/// Declares the user system functions `systfs` to `comp`.
fn declare_systfs(
    comp: &mut Compilation,
    systfs: &[SystfDeclaration],
) -> Result<(), sv_lang::Error> {
    for (name, ret, nargs) in systfs {
        let args = vec![sv_lang::SystemFunctionType::Integer; *nargs];
        match *ret {
            SystfReturn::Typed(t) => comp.add_nonconstant_system_function_typed(name, t, &args)?,
            SystfReturn::Sized { width, signed } => {
                comp.add_nonconstant_system_function_sized(name, width, signed, &args)?
            }
        }
    }
    Ok(())
}

/// The user system functions every multi-file compilation
/// ([`compile_units`]) declares first: the system tasks and functions a VPI
/// library registered (`crate::vpi_systf`).
static SYSTEM_FUNCTIONS: std::sync::Mutex<Vec<SystfDeclaration>> =
    std::sync::Mutex::new(Vec::new());

/// Sets the user system functions of every later [`compile_units`].
pub fn set_system_functions(decls: Vec<SystfDeclaration>) {
    if let Ok(mut d) = SYSTEM_FUNCTIONS.lock() {
        *d = decls;
    }
}

/// A [`Session`] whose syntax trees parse as IEEE 1800-2023, the same
/// language version [`compilation`] uses (slang otherwise parses as 1800-2017,
/// so a 2023-only construct would be rejected before elaboration).
pub fn session() -> Session {
    Session::new().with_language_version(sv_lang::LanguageVersion::V1800_2023)
}

/// The language sv-sim implements: IEEE 1800-2023 (slang otherwise defaults
/// to 1800-2017, whose rules differ -- e.g. an unsized decimal literal wider
/// than 32 bits is truncated to 32 under 2017 but kept at its full width
/// under 2023, LRM 1800-2023 5.7.1 -- and `weak_reference` (8.30) does not
/// exist). The compilation options come from a `Driver` given `--std`. The
/// driver insists on an existing input file before it processes options: an
/// empty one, removed again at once (its sources are never parsed -- the
/// trees come from `session`, which should be made by [`session`] so they
/// parse under the same version).
pub fn compilation(session: &Session) -> Result<Compilation, sv_lang::Error> {
    static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let extra = DRIVER_OPTIONS.lock().map(|o| o.clone()).unwrap_or_default();
    let empty = std::env::temp_dir().join(format!(
        "sv-sim-std-{}-{}.sv",
        std::process::id(),
        NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
    ));
    std::fs::write(&empty, "")
        .map_err(|e| sv_lang::Error::InvalidArgument(format!("{}: {e}", empty.display())))?;
    let bag = (|| {
        let mut driver = sv_lang::Driver::new()?;
        let mut args = vec![
            "sv-sim".to_string(),
            "--std".to_string(),
            "1800-2023".to_string(),
        ];
        args.extend(extra);
        args.push(empty.to_string_lossy().into_owned());
        driver.parse_args(args)?;
        driver.process_options()?;
        driver.create_option_bag()
    })();
    let _ = std::fs::remove_file(&empty);
    Compilation::from_option_bag(session, &bag?)
}

/// `comp.compile()`, then binds the bodies of the module-side interface
/// `extern` method definitions the freeze sweep never reaches
/// ([`sv_sim_sim::netlist::itf::force_extern_impl_bodies`]).
fn compile_design(comp: Compilation) -> Result<Design, sv_lang::Error> {
    let mut design = comp.compile()?;
    sv_sim_sim::netlist::itf::force_extern_impl_bodies(&mut design);
    check_parameter_overrides(&design)?;
    Ok(design)
}

/// Every `-G NAME=VALUE` must name a parameter of a top-level module: slang
/// ignores an override no top declares, so a typo (or an option meant for
/// another tool's design) would silently run the default value. The LRM
/// leaves the option to tools; Verilator rejects the same mistake.
fn check_parameter_overrides(design: &Design) -> Result<(), sv_lang::Error> {
    let opts = DRIVER_OPTIONS.lock().map(|o| o.clone()).unwrap_or_default();
    let mut it = opts.iter();
    while let Some(o) = it.next() {
        if o != "-G" {
            continue;
        }
        let Some(name) = it.next().and_then(|v| v.split('=').next()) else {
            continue;
        };
        // A hierarchical `-G a.b=1` is a defparam: slang diagnoses it itself.
        if name.contains('.') {
            continue;
        }
        let declared = design.top_instances().any(|inst| {
            inst.instance_body().is_some_and(|b| {
                b.members()
                    .any(|m| m.kind() == SymbolKind::Parameter && m.name() == name)
            })
        });
        if !declared {
            return Err(sv_lang::Error::InvalidArgument(format!(
                "-G {name}: no top-level module declares a parameter `{name}`"
            )));
        }
    }
    Ok(())
}

pub fn compile_source(src: &str) -> Result<Design, sv_lang::Error> {
    let session = session();
    let mut comp = compilation(&session)?;
    comp.add_source(src)?;
    compile_design(comp)
}

/// [`compile_source`] for the contents of the file `path`: diagnostics and
/// source locations name the file.
pub fn compile_source_named(src: &str, path: &str) -> Result<Design, sv_lang::Error> {
    compile_source_named_with_systfs(src, path, &[])
}

/// [`compile_source_named`], with user system functions declared first:
/// the system tasks and functions a VPI library registered (`crate::vpi_systf`).
pub fn compile_source_named_with_systfs(
    src: &str,
    path: &str,
    systfs: &[SystfDeclaration],
) -> Result<Design, sv_lang::Error> {
    let session = session();
    let tree = session.parse_named(src, path, path)?;
    let mut comp = compilation(&session)?;
    declare_systfs(&mut comp, systfs)?;
    declare_iverilog_extensions(&mut comp, std::iter::once(src), systfs)?;
    comp.add(&tree)?;
    compile_design(comp)
}

/// The iverilog system functions sv-sim lowers itself, lowered by
/// `crate::vpi_systf::lower_call`: `$simtime` (the current time in
/// global-precision ticks, unscaled by the module's timescale), `$is_signed`
/// (1 if the argument expression is signed, else 0) and `$sizeof` (the
/// argument's width in bits). Each is declared (to slang) only when a source
/// mentions it and no VPI library registered the same name, so every other
/// design is compiled exactly as before. `$simtime` is typed `time` (64 bits,
/// see `crate::vpi_systf::lower_simtime`). The argument of the other two is declared `integer` only so slang accepts any
/// integral expression; the lowering reads the unconverted argument's type.
fn declare_iverilog_extensions<'a>(
    comp: &mut Compilation,
    sources: impl Iterator<Item = &'a str>,
    registered: &[SystfDeclaration],
) -> Result<(), sv_lang::Error> {
    use sv_lang::SystemFunctionType::{Bit, Int, Integer, Time};
    let sources: Vec<&str> = sources.collect();
    for (name, ret, nargs) in [
        (SIMTIME_NAME, Time, 0),
        (IS_SIGNED_NAME, Bit, 1),
        (SIZEOF_NAME, Int, 1),
    ] {
        if registered.iter().any(|r| r.0 == name) || !sources.iter().any(|s| s.contains(name)) {
            continue;
        }
        comp.add_nonconstant_system_function_typed(name, ret, &vec![Integer; nargs])?;
    }
    Ok(())
}

/// iverilog's `$simtime`.
pub const SIMTIME_NAME: &str = "$simtime";
/// iverilog's `$is_signed`.
pub const IS_SIGNED_NAME: &str = "$is_signed";
/// iverilog's `$sizeof`.
pub const SIZEOF_NAME: &str = "$sizeof";

/// How [`compile_units`] sets up the compilation: the command-line options of
/// a multi-file run.
#[derive(Debug, Clone, Default)]
pub struct UnitOptions {
    /// `-I` / `+incdir+`: where `` `include `` looks after the including file's directory.
    pub include_dirs: Vec<String>,
    /// `--top`: the modules to elaborate as tops. Empty: every module no
    /// other instantiates (LRM 23.3.1).
    pub top_modules: Vec<String>,
    /// `--libmap FILE`: library map files (LRM 33.3.1), read in order.
    pub library_maps: Vec<String>,
    /// `-L LIB`: the library search order (LRM 33.8.1), overriding the
    /// map files' declaration order.
    pub liblist: Vec<String>,
}

/// Elaborates several source files together: `units` are `(name, path, text)`,
/// each parsed as a compilation unit of one session whose source manager has
/// `options.include_dirs`, under the compilation options a `Driver` makes of
/// `--std 1800-2023` and `options.top_modules` (see [`compilation`]).
pub fn compile_units(
    units: &[(String, String, String)],
    options: &UnitOptions,
) -> Result<Design, sv_lang::Error> {
    let (driver, bag) = units_driver(options)?;
    // The driver's source manager carries the include directories; the trees
    // (and the design) keep it alive.
    let session = driver
        .session()
        .clone()
        .with_language_version(sv_lang::LanguageVersion::V1800_2023);
    let mut comp = Compilation::from_option_bag(&session, &bag)?;
    let systfs = SYSTEM_FUNCTIONS
        .lock()
        .map(|d| d.clone())
        .unwrap_or_default();
    declare_systfs(&mut comp, &systfs)?;
    declare_iverilog_extensions(&mut comp, units.iter().map(|u| u.2.as_str()), &systfs)?;
    for (name, path, text) in units {
        let tree = session.parse_named(text, name, path)?;
        comp.add(&tree)?;
    }
    compile_design(comp)
}

/// Parses `units` (see [`compile_units`]) and stops: the preprocessor and
/// the grammar run, elaboration does not (`sv-sim run --parse-only`, the
/// check a suite's parsing and preprocessing tests ask for; IEEE 1800-2023
/// clause 22 and Annex A).
pub fn parse_units(
    units: &[(String, String, String)],
    options: &UnitOptions,
) -> Result<(), sv_lang::Error> {
    let (driver, _bag) = units_driver(options)?;
    let mut errors = String::new();
    let session = driver
        .session()
        .clone()
        .with_language_version(sv_lang::LanguageVersion::V1800_2023);
    for (name, path, text) in units {
        let diags = session.parse_named(text, name, path)?.diagnostics();
        if diags.has_errors() {
            errors.push_str(&diags.to_string());
        }
    }
    if errors.is_empty() {
        Ok(())
    } else {
        Err(sv_lang::Error::InvalidArgument(errors))
    }
}

/// The driver and option bag a multi-file run's compilation is made from.
fn units_driver(
    options: &UnitOptions,
) -> Result<(sv_lang::Driver, sv_lang::OptionBag), sv_lang::Error> {
    static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let empty = std::env::temp_dir().join(format!(
        "sv-sim-units-{}-{}.sv",
        std::process::id(),
        NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
    ));
    std::fs::write(&empty, "")
        .map_err(|e| sv_lang::Error::InvalidArgument(format!("{}: {e}", empty.display())))?;
    let setup = (|| {
        let mut driver = sv_lang::Driver::new()?;
        let mut args = vec![
            "sv-sim".to_string(),
            "--std".to_string(),
            "1800-2023".to_string(),
        ];
        for dir in &options.include_dirs {
            args.extend(["-I".to_string(), dir.clone()]);
        }
        for top in &options.top_modules {
            args.extend(["--top".to_string(), top.clone()]);
        }
        args.extend(DRIVER_OPTIONS.lock().map(|o| o.clone()).unwrap_or_default());
        args.push(empty.to_string_lossy().into_owned());
        driver.parse_args(args)?;
        driver.process_options()?;
        let bag = driver.create_option_bag()?;
        Ok::<_, sv_lang::Error>((driver, bag))
    })();
    let _ = std::fs::remove_file(&empty);
    setup
}

/// Elaborates `files` under library map files (LRM 1800-2023 33.3): slang's
/// own source loader reads the maps (`--libmap`, in order; an `include`
/// statement nests another, 33.3.2), maps every file to the library whose
/// `file_path_spec` matches it (33.3.1.1, 33.3.3; a file no spec matches
/// goes to `work`) and binds instances through the libraries in map order
/// or `options.liblist` (`-L`, 33.8.1) or the `config` `options.top_modules`
/// names (33.4). The files are read by slang itself, from disk.
pub fn compile_library_units(
    files: &[String],
    options: &UnitOptions,
) -> Result<Design, sv_lang::Error> {
    let mut driver = sv_lang::Driver::new()?;
    let mut args = vec![
        "sv-sim".to_string(),
        "--std".to_string(),
        "1800-2023".to_string(),
    ];
    for dir in &options.include_dirs {
        args.extend(["-I".to_string(), dir.clone()]);
    }
    for map in &options.library_maps {
        args.extend(["--libmap".to_string(), map.clone()]);
    }
    for lib in &options.liblist {
        args.extend(["-L".to_string(), lib.clone()]);
    }
    for top in &options.top_modules {
        args.extend(["--top".to_string(), top.clone()]);
    }
    args.extend(DRIVER_OPTIONS.lock().map(|o| o.clone()).unwrap_or_default());
    args.extend(files.iter().cloned());
    driver.parse_args(args)?;
    driver.process_options()?;
    let ok = driver.parse_sources()?;
    if !ok {
        return Err(sv_lang::Error::InvalidArgument(
            "the library map or a source file failed to load".to_string(),
        ));
    }
    let trees = driver.trees();
    let bag = driver.create_option_bag()?;
    let session = driver
        .session()
        .clone()
        .with_language_version(sv_lang::LanguageVersion::V1800_2023);
    let mut comp = Compilation::from_option_bag(&session, &bag)?;
    for tree in &trees {
        comp.add(tree)?;
    }
    let design = compile_design(comp)?;
    // slang only picks automatic tops from the default library, so a design
    // whose cells all sit in mapped libraries has none: refuse by name rather
    // than run nothing (LRM 23.3.1's selection across libraries is not
    // modelled; `--top LIB.CELL` or `--top CONFIG` names the design).
    if design.top_instances().next().is_none() && options.top_modules.is_empty() {
        return Err(sv_lang::Error::InvalidArgument(
            "no top-level module in the default library `work`; name the design with \
             --top LIB.CELL or --top CONFIG when the cells are mapped to other libraries \
             (LRM 1800-2023 33.4.1.1)"
                .to_string(),
        ));
    }
    Ok(design)
}

pub fn find_function<'d>(design: &'d Design, name: &str) -> Option<Symbol<'d>> {
    for inst in design.top_instances() {
        if let Some(body) = inst.instance_body()
            && let Some(sym) = body.find(name)
            && sym.kind() == SymbolKind::Subroutine
        {
            return Some(sym);
        }
    }
    None
}

/// Task 11's own entry point: a `Design` borrow plus the [`ValueRepr`] every
/// worker lowers against, bundled so [`lower_all_instance_bodies`] takes one
/// argument instead of two. Fields are private by design -- [`Self::new`] is
/// the only constructor -- purely to leave room for this struct to grow
/// caller-invisible bookkeeping later without an API break (there is no
/// invariant an outside crate could violate by writing `design`/`mode`
/// directly; this is a forward-compatibility choice, not a soundness one).
pub struct CodegenSession<'d> {
    design: &'d Design,
    mode: ValueRepr,
}

impl<'d> CodegenSession<'d> {
    pub fn new(design: &'d Design, mode: ValueRepr) -> Self {
        Self { design, mode }
    }
}

/// One [`lower_one_instance_body`] result: the emitted LLVM IR for one
/// structurally-distinct `InstanceBody`'s own fresh `Module`, plus enough
/// metadata for a caller (or a test) to identify and verify it without
/// re-parsing `ir` itself.
#[derive(Debug, Clone)]
pub struct LoweredUnit {
    /// The originating `InstanceBody`'s definition name (`Symbol::name()`,
    /// e.g. `"sub"` for `module sub; ... endmodule`) -- stable across a
    /// parallel vs. sequential run of the same design (unlike this `Vec`'s
    /// own position, which depends on worker completion order), so a caller
    /// comparing two runs' output should key on this field, never on index.
    pub unit_name: String,
    /// Every kernel function [`crate::process_lower::lower_kernel_fn_with_repr`]
    /// emitted into this unit's `Module`, index-aligned with that body's own
    /// [`crate::module::discover_processes`] order.
    pub kernel_names: Vec<String>,
    /// `module.print_to_string()` -- the whole emitted `Module`'s textual
    /// LLVM IR, captured before this unit's own `Context`/`Module` are
    /// dropped (see this file's module doc comment: nothing here keeps an
    /// `inkwell` type alive past [`lower_one_instance_body`] returning).
    pub ir: String,
    /// Which OS thread actually ran this unit's lowering -- real
    /// diagnostic data (not a test-only hook): a caller profiling
    /// [`lower_all_instance_bodies`]'s own parallelism can see it, and it is
    /// exactly what proves real parallelism to this task's own
    /// `lowering_a_5module_design_uses_more_than_one_worker_thread` test
    /// without needing a side channel [`lower_all_instance_bodies`]'s public
    /// signature has no room for.
    pub thread_id: ThreadId,
}

/// Converts one `sv_sim_sim::process::cfg`-track lowering failure (a
/// distinct type from this crate's own [`LoweringError`] by design -- see
/// that type's own doc comment on why `sv-sim-sim` cannot depend on this
/// crate to share it) into this crate's own error type, citing `sym` the
/// same way [`LoweringError::unsupported_at`] always does. Mirrors
/// `sv-sim-bin::run::sim_lowering_to_run_error` exactly, one crate over (the
/// same "reconciled at the one call site that needs both types" pattern
/// that file's own doc comment names).
fn sim_lowering_to_codegen_error(
    e: sv_sim_sim::process::cfg::LoweringError,
    sym: &Symbol,
) -> LoweringError {
    LoweringError::unsupported_at(&e.to_string(), sym)
}

/// `SliceProcKind` -> `process::cfg::ProcessLoopKind`: mirrors
/// `sv-sim-bin::run::loop_kind_of` exactly (see that function's own doc
/// comment for the reasoning) -- duplicated here rather than shared,
/// following the same "documented duplication" precedent
/// `sv-sim-bin::run::signal_slots_of`'s own doc comment names for exactly
/// this tension (no existing shared home for a 2-crate-used mapping this
/// small, and inventing one mid-task would touch a file neither this task's
/// nor that one's own Files list names).
fn loop_kind_of(kind: SliceProcKind) -> ProcessLoopKind {
    match kind {
        SliceProcKind::Initial
        | SliceProcKind::AlwaysComb
        | SliceProcKind::AlwaysLatch
        | SliceProcKind::Final
        | SliceProcKind::StaticAssertion => ProcessLoopKind::OneShot,
        SliceProcKind::Always | SliceProcKind::AlwaysFF => ProcessLoopKind::Looping,
    }
}

/// A minimal, self-contained `SymbolId` -> [`SignalSlot`] map for `body`'s
/// own direct `Variable`/`Net`/`Port` members, assigning slot indices in
/// simple declaration order. Deliberately NOT `state::layout::
/// build_template_layout`'s own filter/order (mirrored more carefully by
/// `sv-sim-bin::run::signal_slots_of`, which that function's own doc
/// comment explains must match it exactly because it feeds a REAL
/// `StateEngine`) -- this task's own scope note is explicit that
/// `lower_all_instance_bodies`'s output is never wired to `sv-sim-bin run`
/// or any live `StateEngine` this wave, so no such ordering contract binds
/// this helper; it exists only so [`build_lowering_plan`]'s signature has a
/// real, well-typed map to resolve external signal references against.
fn signal_slots_of(body: &Symbol<'_>) -> HashMap<SymbolId, SignalSlot> {
    let mut map = HashMap::new();
    let mut slot_index: u32 = 0;
    for member in sv_sim_sim::netlist::module_items(body) {
        let kind = member.kind();
        if !matches!(
            kind,
            SymbolKind::Variable | SymbolKind::Net | SymbolKind::Port
        ) {
            continue;
        }
        if kind == SymbolKind::Port
            && (member.port_internal_symbol().is_some() || member.port_is_null_port())
        {
            continue;
        }
        let ty = if kind == SymbolKind::Port {
            member.port_type()
        } else {
            member.value_type()
        };
        let Some(ty) = ty else {
            continue;
        };
        map.insert(
            member.uid(),
            SignalSlot {
                slot: slot_index,
                width: ty.value_width(),
                signed: ty.is_signed(),
                class: SignalSlotClass::Inline,
            },
        );
        slot_index += 1;
    }
    map
}

/// Lowers every process `discover_processes` finds in `body` into its own,
/// freshly-created `inkwell::Context`+`Module` -- never a `Context`/`Module`
/// shared with any other call to this function, structurally: this
/// function's own signature takes no `Context` parameter at all, so no
/// caller (including [`lower_all_instance_bodies`]'s own parallel fan-out)
/// can pass one in, which is exactly
/// `each_worker_gets_its_own_inkwell_context_never_shared`'s own point --
/// `Context` sharing across threads is a `Send`/`Sync` compile error by
/// construction, so this is a type-system guarantee, not a runtime
/// discipline this function has to uphold by hand.
///
/// `comb_sensitivity` is the whole-design index [`build_comb_sensitivity_index`]
/// computes once, up front (cheap, and `discover_processes` needs it for
/// `AlwaysComb`/`AlwaysLatch`'s own implicit sensitivity list) -- shared
/// read-only across every call this function's own callers make from
/// separate worker threads, sound because it is plain owned data (a
/// `HashMap`), not a live `Analysis`/`Symbol` borrow.
pub fn lower_one_instance_body<'d>(
    body: Symbol<'d>,
    comb_sensitivity: &CombSensitivityIndex,
    mode: ValueRepr,
) -> Result<LoweredUnit, LoweringError> {
    let ctx = Context::create();
    let module = ctx.create_module(&format!("sv_sim_unit_{}", body.name()));
    let fns = declare_accessor_fns(&ctx, &module);

    let processes = discover_processes(&body, comb_sensitivity)?;
    let signals = signal_slots_of(&body);

    let mut kernel_names = Vec::with_capacity(processes.len());
    for (i, proc) in processes.iter().enumerate() {
        let root = sv_sim_sim::process::cfg::proc_body(&proc.sym).ok_or_else(|| {
            LoweringError::unsupported_at("procedural block has no body statement", &proc.sym)
        })?;
        let cfg = build_cfg(&root, loop_kind_of(proc.kind))
            .map_err(|e| sim_lowering_to_codegen_error(e, &proc.sym))?;
        // Mirrors `sv-sim-bin::run::run_design`'s own check: fork/join has
        // no scheduling semantics yet this wave (construction-only,
        // PR-069/`docs/design/process.md` §7) -- `lower_to_segments` has no
        // `SegTerminator` shape for it, so this must fail loudly here
        // rather than let that become an internal-invariant panic deeper in
        // the pipeline.
        if contains_unschedulable_fork_join(&cfg) {
            return Err(LoweringError::unsupported_at(
                "fork/join has no scheduling semantics yet -- construction-only this wave \
                 (PR-069/docs/design/process.md §7)",
                &proc.sym,
            ));
        }
        let frame =
            compute_liveness(&cfg).map_err(|e| sim_lowering_to_codegen_error(e, &proc.sym))?;
        let segments = lower_to_segments(&cfg, &frame);
        let plan = build_lowering_plan(
            format!("sv_sim_proc_{}_{}", body.name(), i),
            proc.sym,
            segments,
            frame,
            signals.clone(),
        );
        let fv = lower_kernel_fn_with_repr(&ctx, &module, &fns, &plan, mode)?;
        kernel_names.push(fv.get_name().to_str().unwrap().to_string());
    }

    Ok(LoweredUnit {
        unit_name: body.name().to_string(),
        kernel_names,
        ir: module.print_to_string().to_string(),
        thread_id: std::thread::current().id(),
    })
}

/// Task 11's own public entry point (cross-track note: this closes wave-A
/// codegen's own **CG-092** row -- "rayon-based codegen parallelism ... one
/// worker per unique InstanceBody" -- not an A2-part2 ledger ID; see this
/// file's module doc comment and the plan's own Task 11 header). An
/// alternative, batch-oriented lowering entry point alongside `sv-sim-bin
/// run`'s existing sequential single-shared-`Module` pipeline
/// (`crate::pipeline::build_pipeline`) -- **not a replacement for it**: see
/// `docs/superpowers/plans/2026-09-26-wave-A2-process-kernels.md`'s Task 11
/// scope note for why the two cannot be unified this task (independent
/// `inkwell::Context`s cannot be linked/JIT'd together as one
/// `ExecutionEngine` without machinery this task does not build). This
/// function's real, in-scope consumer is Task 4's AOT `.o`-per-unit link
/// step, whose own `docs/design/codegen.md` §4 text ("each worker's Module
/// only needs consistent declarations ... for symbols defined elsewhere")
/// describes exactly a linker's job, not a JIT's.
///
/// **Fan-out mechanism, reconciled at the wave-B integration merge (see
/// this file's own module doc comment):** the dedup scan and the actual
/// parallel lowering are now two separate passes. First, a plain
/// **sequential** `Symbol::visit` walk from `sess.design.root()` reaches
/// every symbol at every nesting depth (the same set `Design::par_visit_from`
/// used to reach) and collects every distinct-by-structural-type `Instance`
/// body into a caller-owned `Vec<Symbol<'d>>` -- no `Mutex` needed, since
/// this pass is single-threaded by construction:
/// - skips anything that isn't `SymbolKind::Instance` (or has no
///   `instance_body()`);
/// - keeps a body only if no body already collected has the same
///   structural type (`Symbol::instance_body_has_same_type` -- `sv_lang`
///   mints a distinct `InstanceBody` object per instance even for two
///   textually-identical instantiations, verified live: `sub s1(); sub
///   s2();` yields `instance_body_has_same_type() == true` but
///   `Symbol::eq() == false` for `s1`/`s2`'s own bodies, so structural-type
///   equality, never symbol/pointer identity, is the right dedup key).
///
/// Second, that already-deduped slice is hand to plain `rayon::prelude::*`
/// (`par_iter().map(lower_one_instance_body)`, the same fan-out shape
/// `session.rs::lower_all_instance_bodies` uses) -- so the only thing ever
/// shared across worker threads is a `&[Symbol<'d>]` of already-claimed,
/// already-distinct bodies, and each worker's own [`lower_one_instance_body`]
/// call still creates, uses, and drops its own fresh `Context`/`Module`
/// entirely within that one thread (BINDING CORRECTION 3). This is still
/// genuinely "rayon-based codegen parallelism ... one worker per unique
/// InstanceBody" (CG-092's own literal requirement text); `tests/
/// parallel_lowering.rs`'s `lowering_a_5module_design_uses_more_than_one_
/// worker_thread` still proves real, observed multi-thread execution.
///
/// Returns `Err` if any [`lower_one_instance_body`] call failed (the first
/// failure in `rayon`'s own collection order, itself worker-completion-order
/// dependent, not source order -- the same "first observed, not first in
/// source order" caveat any parallel fan-out has). The returned `Vec`'s
/// order depends on worker completion order (see [`LoweredUnit::unit_name`]'s
/// own doc comment) -- never assumed stable by this function's own callers.
pub fn lower_all_instance_bodies<'d>(
    sess: &CodegenSession<'d>,
) -> Result<Vec<LoweredUnit>, LoweringError> {
    let comb_sensitivity = build_comb_sensitivity_index(sess.design)?;

    let mut claimed: Vec<Symbol<'d>> = Vec::new();
    sess.design.root().visit(|sym| {
        if sv_sim_sim::netlist::is_uninstantiated_scope(&sym) {
            return Walk::Skip;
        }
        if sym.kind() == SymbolKind::Instance
            && let Some(body) = sym.instance_body()
            && !claimed.iter().any(|u| body.instance_body_has_same_type(u))
        {
            claimed.push(body);
        }
        Walk::Continue
    });

    claimed
        .par_iter()
        .map(|body| lower_one_instance_body(*body, &comb_sensitivity, sess.mode))
        .collect()
}