vj-codegen 0.2.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
//! Task 16 (`docs/superpowers/plans/2026-09-23-wave-A-abi.md`; ledger
//! AB-009): a hand-written LLVM-IR "counter" fixture -- the JIT-loadable
//! equivalent of `vj/tests/fixtures/test_kernel.rs`'s Rust-source
//! fixture. The JIT path loads bitcode, never a compiled `.so`/staticlib, so
//! the *same logical program* ("increment a counter on each Active-region
//! dispatch, gated on a request flag") this crate's AOT-dynamic/AOT-static
//! fixtures already implement in Rust must be re-expressed as hand-built IR
//! here, for `vj/tests/cross_mode_differential.rs`'s AB-009
//! three-way differential test.
//!
//! **Why this lives in `vj-codegen`, not `vj`:** `vj`
//! may never depend on `inkwell`/`llvm-sys` directly (AB-023/AB-024,
//! enforced by `xtask/tests/dep_graph.rs::
//! inkwell_and_llvm_sys_only_under_codegen`, which walks the full — not
//! `--no-deps` — dependency graph and therefore also catches a
//! `[dev-dependencies]` edge). `vj` already depends on
//! `vj-codegen` for production reasons (`pipeline::build_pipeline`), so
//! [`counter_fixture_bitcode`]'s `Vec<u8>` return type is this module's
//! entire public surface reaching across that boundary -- no `inkwell` type
//! is ever named outside this file.
//!
//! **Cross-track note (new file, purely additive — flag to the `codegen`
//! track at merge time, same pattern as this plan's other Deviations):**
//! implements exactly the four ABI functions the differential test drives,
//! by the exact frozen `vj_abi::entry`/`accessor` symbol-name constants,
//! with the exact calling convention `rustc` itself generates for each real
//! signature. Verified directly (not guessed): compiling an equivalent Rust
//! source defining `sim_eval_region(h: SimHandle, region: Region, t:
//! SimTime) -> EvalResult` with `rustc --emit=llvm-ir -O` and reading back
//! the real lowering shows `SimTime` (`{sim_units: i64, delta: u32}`, 16
//! bytes) is SysV-classified as two INTEGER eightbytes and passed as a
//! literal `{ i64, i64 }` struct value (never `byval`, never split into
//! `i64,i32`) — this file's `simtime_ty` below is built to match that
//! observed lowering exactly, not the "natural" `{i64,i32}` struct shape.
//! `EvalResult` (36 bytes) is MEMORY-classified and returned through a
//! hidden `sret` pointer in parameter position 0 — the exact convention
//! `process_lower.rs::lower_kernel_fn` already uses for the same reason
//! (`EvalResult` returned by `ProcessFn`), and which
//! `tests/jit_process_fn.rs` already round-trips end to end through a real
//! Rust-typed `extern "C" fn` pointer, so this file reuses that
//! already-proven pattern rather than inventing a new one. This fixture
//! deliberately ignores `t`'s actual bit content (only its parameter
//! type/position matters for calling-convention correctness) and ignores
//! `SimHandle`/`local`/`resume_pc` values everywhere (this fixture's state
//! lives in two module-private globals, not anything reachable from those
//! opaque parameters) -- a documented simplification, since this fixture
//! exists solely to drive one cross-mode differential test, not to
//! generally exercise handle/local-block semantics (already covered
//! elsewhere).
use inkwell::AddressSpace;
use inkwell::attributes::{Attribute, AttributeLoc};
use inkwell::builder::Builder;
use inkwell::context::Context;
use inkwell::module::{Linkage, Module};
use inkwell::types::AnyType;
use inkwell::values::PointerValue;

use vj_abi::accessor::{SV_GET_SCALAR_SYMBOL, SV_SET_SCALAR_SYMBOL};
use vj_abi::entry::{SIM_EVAL_REGION_SYMBOL, SIM_INIT_SYMBOL};

/// `SlotId(0)`'s meaning in this fixture -- the dispatch counter, mirroring
/// `vj/tests/fixtures/test_kernel.rs`'s own slot 0 (`CELL`).
pub const COUNTER_SLOT: u32 = 0;
/// `SlotId(3)`'s meaning in this fixture -- the Active-region dispatch gate,
/// mirroring that same Rust fixture's own slot 3 (`REQUEST_CELL`).
pub const REQUEST_SLOT: u32 = 3;

/// Builds the fixture module described in this file's module doc in a
/// fresh, exclusively-owned [`Context`], then serializes it to an in-memory
/// LLVM bitcode blob -- the only representation
/// [`crate::orc::JitBackend::add_module_from_bitcode`] accepts. The
/// `Context` this module was built in is dropped at the end of this
/// function; the returned bytes carry no borrow on it (bitcode is a fully
/// self-contained serialization, the same contract
/// `orc::JitBackend::add_module_from_bitcode`'s own doc comment relies on).
pub fn counter_fixture_bitcode() -> Vec<u8> {
    let ctx = Context::create();
    let module = build_counter_fixture_module(&ctx);
    module.write_bitcode_to_memory().as_slice().to_vec()
}

fn build_counter_fixture_module(ctx: &Context) -> Module<'_> {
    let module = ctx.create_module("cross_mode_counter_fixture");
    let builder = ctx.create_builder();

    let ptr_ty = ctx.ptr_type(AddressSpace::default());
    let i8_ty = ctx.i8_type();
    let i32_ty = ctx.i32_type();
    let i64_ty = ctx.i64_type();
    let i128_ty = ctx.i128_type();
    let void_ty = ctx.void_type();

    // The fixture's two cells. `Linkage::Private`: never resolved by an
    // external symbol lookup (same convention `systask.rs`'s own
    // module-internal globals use), only ever touched by this module's own
    // four functions below.
    let counter_global = module.add_global(i128_ty, None, "counter_cell");
    counter_global.set_initializer(&i128_ty.const_int(0, false));
    counter_global.set_linkage(Linkage::Private);
    let request_global = module.add_global(i128_ty, None, "request_cell");
    request_global.set_initializer(&i128_ty.const_int(0, false));
    request_global.set_linkage(Linkage::Private);

    build_sim_init(
        ctx,
        &module,
        &builder,
        ptr_ty,
        i32_ty,
        i64_ty,
        i128_ty,
        counter_global,
        request_global,
    );
    build_sim_eval_region(
        ctx,
        &module,
        &builder,
        ptr_ty,
        i8_ty,
        i32_ty,
        i64_ty,
        i128_ty,
        void_ty,
        counter_global,
        request_global,
    );
    build_accessor(
        ctx,
        &module,
        &builder,
        SV_GET_SCALAR_SYMBOL,
        ptr_ty,
        i32_ty,
        i128_ty,
        Some(void_ty), // unused for the getter; see build_accessor's own doc
        counter_global,
        request_global,
        true,
    );
    build_accessor(
        ctx,
        &module,
        &builder,
        SV_SET_SCALAR_SYMBOL,
        ptr_ty,
        i32_ty,
        i128_ty,
        Some(void_ty),
        counter_global,
        request_global,
        false,
    );

    assert!(
        module.verify().is_ok(),
        "counter_fixture module failed to verify:\n{}",
        module.print_to_string().to_string()
    );
    module
}

#[allow(clippy::too_many_arguments)]
fn build_sim_init<'ctx>(
    ctx: &'ctx Context,
    module: &Module<'ctx>,
    builder: &Builder<'ctx>,
    ptr_ty: inkwell::types::PointerType<'ctx>,
    i32_ty: inkwell::types::IntType<'ctx>,
    i64_ty: inkwell::types::IntType<'ctx>,
    i128_ty: inkwell::types::IntType<'ctx>,
    counter_global: inkwell::values::GlobalValue<'ctx>,
    request_global: inkwell::values::GlobalValue<'ctx>,
) {
    // sim_init(design_id: u32) -> SimHandle
    let fn_ty = ptr_ty.fn_type(&[i32_ty.into()], false);
    let f = module.add_function(SIM_INIT_SYMBOL, fn_ty, None);
    let entry = ctx.append_basic_block(f, "entry");
    builder.position_at_end(entry);
    builder
        .build_store(
            counter_global.as_pointer_value(),
            i128_ty.const_int(0, false),
        )
        .unwrap();
    builder
        .build_store(
            request_global.as_pointer_value(),
            i128_ty.const_int(0, false),
        )
        .unwrap();
    // A fixed, non-null, well-aligned handle value -- this fixture never
    // dereferences it (its state lives in the two globals above), same
    // "opaque, real, non-null pointer identity only" convention
    // `vj/tests/fixtures/test_kernel.rs::sim_init` and Task 15's
    // `jit_abi_entry_points.rs` fixture both already use.
    let fixed_addr = i64_ty.const_int(0xC0DE_C0DE, false);
    let handle_ptr = builder
        .build_int_to_ptr(fixed_addr, ptr_ty, "handle")
        .unwrap();
    builder.build_return(Some(&handle_ptr)).unwrap();
    assert!(f.verify(true), "sim_init failed to verify");
}

#[allow(clippy::too_many_arguments)]
fn build_sim_eval_region<'ctx>(
    ctx: &'ctx Context,
    module: &Module<'ctx>,
    builder: &Builder<'ctx>,
    ptr_ty: inkwell::types::PointerType<'ctx>,
    i8_ty: inkwell::types::IntType<'ctx>,
    i32_ty: inkwell::types::IntType<'ctx>,
    i64_ty: inkwell::types::IntType<'ctx>,
    i128_ty: inkwell::types::IntType<'ctx>,
    void_ty: inkwell::types::VoidType<'ctx>,
    counter_global: inkwell::values::GlobalValue<'ctx>,
    request_global: inkwell::values::GlobalValue<'ctx>,
) {
    // sim_eval_region(SimHandle, Region, SimTime) -> EvalResult.
    // `EvalResult`'s own `#[repr(C)]` layout: `{outcome: u32}{reschedule:
    // [u8;32]}`, 36 bytes, align 4 -- same struct shape
    // `process_lower.rs::lower_kernel_fn` already builds for its own
    // sret attribute (see this file's module doc for why re-deriving that
    // shape independently, rather than trusting a mismatched guess, matters
    // here).
    let eval_result_ty = ctx.struct_type(&[i32_ty.into(), i8_ty.array_type(32).into()], false);
    // See this file's module doc: verified directly against `rustc
    // --emit=llvm-ir`'s real lowering of an equivalent Rust signature.
    let simtime_ty = ctx.struct_type(&[i64_ty.into(), i64_ty.into()], false);
    let fn_ty = void_ty.fn_type(
        &[
            ptr_ty.into(),     // sret (EvalResult*)
            ptr_ty.into(),     // handle (unused)
            i32_ty.into(),     // region
            simtime_ty.into(), // t (unused; see module doc)
        ],
        false,
    );
    let f = module.add_function(SIM_EVAL_REGION_SYMBOL, fn_ty, None);
    let sret_kind_id = Attribute::get_named_enum_kind_id("sret");
    f.add_attribute(
        AttributeLoc::Param(0),
        ctx.create_type_attribute(sret_kind_id, eval_result_ty.as_any_type_enum()),
    );

    let sret = f.get_nth_param(0).unwrap().into_pointer_value();
    let region = f.get_nth_param(2).unwrap().into_int_value();

    let entry_bb = ctx.append_basic_block(f, "entry");
    let active_bb = ctx.append_basic_block(f, "active");
    let increment_bb = ctx.append_basic_block(f, "increment");
    let exit_bb = ctx.append_basic_block(f, "exit");
    let trap_bb = ctx.append_basic_block(f, "trap");

    builder.position_at_end(entry_bb);
    // Region discriminants mirror `vj_abi::Region`'s frozen repr(u32)
    // layout (Active=0, Inactive=1, Nba=2, Reactive=3, Postponed=4). Every
    // non-Active region is a documented no-op in this fixture (it never
    // stages an NBA write or anything Reactive/Postponed-runnable) -- an
    // out-of-range value traps (`trap_bb`), never silently falls through,
    // mirroring `test_kernel.rs::sim_eval_region`'s own
    // `other => panic!(...)` default arm.
    builder
        .build_switch(
            region,
            trap_bb,
            &[
                (i32_ty.const_int(0, false), active_bb),
                (i32_ty.const_int(1, false), exit_bb),
                (i32_ty.const_int(2, false), exit_bb),
                (i32_ty.const_int(3, false), exit_bb),
                (i32_ty.const_int(4, false), exit_bb),
            ],
        )
        .unwrap();

    builder.position_at_end(active_bb);
    let request = builder
        .build_load(i128_ty, request_global.as_pointer_value(), "request")
        .unwrap()
        .into_int_value();
    let nonzero = builder
        .build_int_compare(
            inkwell::IntPredicate::NE,
            request,
            i128_ty.const_int(0, false),
            "request_nonzero",
        )
        .unwrap();
    builder
        .build_conditional_branch(nonzero, increment_bb, exit_bb)
        .unwrap();

    builder.position_at_end(increment_bb);
    let counter = builder
        .build_load(i128_ty, counter_global.as_pointer_value(), "counter")
        .unwrap()
        .into_int_value();
    let incremented = builder
        .build_int_add(counter, i128_ty.const_int(1, false), "incremented")
        .unwrap();
    builder
        .build_store(counter_global.as_pointer_value(), incremented)
        .unwrap();
    builder.build_unconditional_branch(exit_bb).unwrap();

    builder.position_at_end(exit_bb);
    store_zeroed_eval_result(ctx, builder, sret);

    builder.position_at_end(trap_bb);
    builder.build_unreachable().unwrap();

    assert!(f.verify(true), "sim_eval_region failed to verify");
}

/// Writes a compile-time-constant all-zero 36-byte `EvalResult` bit pattern
/// (`outcome = EvalOutcome::Completed (0)`, `reschedule.opaque` all zero)
/// into the `sret`-pointed return slot and returns -- the only outcome this
/// fixture's `sim_eval_region` ever reports. Same technique
/// `process_lower.rs::store_eval_result_bytes_and_return` already uses and
/// `tests/jit_process_fn.rs` already proves round-trips correctly through a
/// real Rust `extern "C" fn` caller.
fn store_zeroed_eval_result<'ctx>(ctx: &'ctx Context, b: &Builder<'ctx>, sret: PointerValue<'ctx>) {
    let i8_ty = ctx.i8_type();
    let zero = i8_ty.const_int(0, false);
    let elems = vec![zero; 36];
    let arr = i8_ty.const_array(&elems);
    b.build_store(sret, arr).unwrap();
    b.build_return(None).unwrap();
}

/// Builds either `sv_get_scalar` (`is_get = true`) or `sv_set_scalar`
/// (`is_get = false`) -- both share the identical `slot`-keyed
/// switch-to-{counter,request}-global shape, differing only in
/// load-then-return vs. store-then-return-void at each case. `void_ty` is
/// only used by the setter's return type; passed as `Option` so one
/// function signature serves both (the getter's own return type is
/// `i128_ty`, computed internally, never `void_ty`).
#[allow(clippy::too_many_arguments)]
fn build_accessor<'ctx>(
    ctx: &'ctx Context,
    module: &Module<'ctx>,
    builder: &Builder<'ctx>,
    symbol: &str,
    ptr_ty: inkwell::types::PointerType<'ctx>,
    i32_ty: inkwell::types::IntType<'ctx>,
    i128_ty: inkwell::types::IntType<'ctx>,
    void_ty: Option<inkwell::types::VoidType<'ctx>>,
    counter_global: inkwell::values::GlobalValue<'ctx>,
    request_global: inkwell::values::GlobalValue<'ctx>,
    is_get: bool,
) {
    let f = if is_get {
        let fn_ty = i128_ty.fn_type(&[ptr_ty.into(), i32_ty.into()], false);
        module.add_function(symbol, fn_ty, None)
    } else {
        let fn_ty = void_ty
            .expect("setter needs void_ty")
            .fn_type(&[ptr_ty.into(), i32_ty.into(), i128_ty.into()], false);
        module.add_function(symbol, fn_ty, None)
    };

    let slot = f.get_nth_param(1).unwrap().into_int_value();
    let value = if is_get {
        None
    } else {
        Some(f.get_nth_param(2).unwrap())
    };

    let entry_bb = ctx.append_basic_block(f, "entry");
    let counter_bb = ctx.append_basic_block(f, "counter_slot");
    let request_bb = ctx.append_basic_block(f, "request_slot");
    let trap_bb = ctx.append_basic_block(f, "trap");

    builder.position_at_end(entry_bb);
    // Exhaustive over this fixture's two known slots; any other slot traps
    // rather than silently doing nothing -- mirroring
    // `test_kernel.rs::sv_get_scalar`/`sv_set_scalar`'s own
    // `other => panic!(...)` default arm.
    builder
        .build_switch(
            slot,
            trap_bb,
            &[
                (i32_ty.const_int(u64::from(COUNTER_SLOT), false), counter_bb),
                (i32_ty.const_int(u64::from(REQUEST_SLOT), false), request_bb),
            ],
        )
        .unwrap();

    for (bb, global) in [(counter_bb, counter_global), (request_bb, request_global)] {
        builder.position_at_end(bb);
        if is_get {
            let v = builder
                .build_load(i128_ty, global.as_pointer_value(), "v")
                .unwrap();
            builder.build_return(Some(&v)).unwrap();
        } else {
            builder
                .build_store(global.as_pointer_value(), value.unwrap())
                .unwrap();
            builder.build_return(None).unwrap();
        }
    }

    builder.position_at_end(trap_bb);
    builder.build_unreachable().unwrap();

    assert!(f.verify(true), "{symbol} failed to verify");
}

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

    /// A cheap in-crate sanity check (the real cross-mode behavior is
    /// exercised by `vj/tests/cross_mode_differential.rs`, which
    /// this function exists for): the fixture module built here verifies
    /// and serializes to a non-empty bitcode blob, and defines every symbol
    /// name the differential test resolves by.
    #[test]
    fn counter_fixture_bitcode_is_nonempty_and_module_defines_all_four_symbols() {
        let ctx = Context::create();
        let module = build_counter_fixture_module(&ctx);
        for symbol in [
            SIM_INIT_SYMBOL,
            SIM_EVAL_REGION_SYMBOL,
            SV_GET_SCALAR_SYMBOL,
            SV_SET_SCALAR_SYMBOL,
        ] {
            assert!(
                module.get_function(symbol).is_some(),
                "counter_fixture module must define {symbol:?}"
            );
        }
        let bitcode = counter_fixture_bitcode();
        assert!(!bitcode.is_empty());
    }

    /// Mutation-test kill: the only prior coverage of this fixture's
    /// `sim_eval_region` (`counter_fixture_bitcode_is_nonempty_and_module_
    /// defines_all_four_symbols` above, and `vj/tests/
    /// cross_mode_differential.rs`) either never calls it at all, or is
    /// entirely gated behind the `static-test-kernel` Cargo feature (off by
    /// default -- that whole test file compiles to zero tests without it),
    /// so a plain `cargo test --workspace` never actually executes this
    /// fixture's request-gate logic. That left `build_sim_eval_region`'s
    /// `IntPredicate::NE` request-nonzero check free to be flipped to `EQ`
    /// (incrementing the counter exactly when the gate is zero, instead of
    /// nonzero) with zero test failures anywhere in the workspace.
    ///
    /// This test loads the real fixture through the real, production
    /// `orc::JitBackend` (the same backend `cross_mode_differential.rs`'s
    /// JIT leg uses) and drives `sim_init`/`sim_eval_region`/
    /// `sv_get_scalar`/`sv_set_scalar` directly, with no Cargo feature
    /// required: dispatching `Region::Active` while the request gate
    /// (`REQUEST_SLOT`) is still zero (as `sim_init` leaves it) must NOT
    /// increment the counter; setting the gate nonzero and dispatching
    /// again must increment it by exactly one.
    #[test]
    fn sim_eval_region_only_increments_the_counter_when_the_request_gate_is_nonzero() {
        let bitcode = counter_fixture_bitcode();

        let backend = crate::orc::JitBackend::new().expect("JitBackend::new");
        backend
            .add_module_from_bitcode(&bitcode)
            .expect("add_module_from_bitcode");

        // SAFETY: this module defines each symbol below with exactly the
        // ABI shape this file's own module doc derives and justifies --
        // identical reasoning to `cross_mode_differential.rs`'s JIT leg,
        // which resolves the same four symbols off the same fixture the
        // same way.
        let sim_init: vj_abi::entry::SimInitFn =
            unsafe { backend.get_fn(SIM_INIT_SYMBOL) }.expect("sim_init must resolve");
        let sim_eval_region: vj_abi::entry::SimEvalRegionFn =
            unsafe { backend.get_fn(SIM_EVAL_REGION_SYMBOL) }
                .expect("sim_eval_region must resolve");
        let sv_get_scalar: vj_abi::accessor::SvGetScalarFn =
            unsafe { backend.get_fn(SV_GET_SCALAR_SYMBOL) }.expect("sv_get_scalar must resolve");
        let sv_set_scalar: vj_abi::accessor::SvSetScalarFn =
            unsafe { backend.get_fn(SV_SET_SCALAR_SYMBOL) }.expect("sv_set_scalar must resolve");

        let h = sim_init(0);
        let t = vj_abi::SimTime {
            sim_units: 0,
            delta: 0,
        };

        // sim_init zeroes both cells -- the request gate starts at 0.
        let r = sim_eval_region(h, vj_abi::Region::Active, t);
        assert_eq!(r.outcome, vj_abi::EvalOutcome::Completed);
        assert_eq!(
            sv_get_scalar(h, vj_abi::SlotId(COUNTER_SLOT)),
            0,
            "the counter must NOT increment while the request gate (REQUEST_SLOT) is zero"
        );

        // Set the gate nonzero, dispatch again: must increment exactly once.
        sv_set_scalar(h, vj_abi::SlotId(REQUEST_SLOT), 1);
        let r2 = sim_eval_region(h, vj_abi::Region::Active, t);
        assert_eq!(r2.outcome, vj_abi::EvalOutcome::Completed);
        assert_eq!(
            sv_get_scalar(h, vj_abi::SlotId(COUNTER_SLOT)),
            1,
            "the counter must increment by exactly one once the request gate is nonzero"
        );
    }
}