vj-codegen 0.2.0

LLVM code generator of the vj SystemVerilog simulator.
//! 4-state packed value model. Narrow = one LLVM i(2N): value lane [N-1:0],
//! unknown (x/z) lane [2N-1:N]. Per-bit lane 2*unk+val matches sv-lang::Bit.
//!
//! Task 16 (CG-173): wide-value limb storage (`WideValueRef`, below) uses a
//! SECOND, DIFFERENT per-bit plane-pair polarity -- the IEEE 1800
//! `t_vpi_vecval`/`svLogicVecVal` `(aval,bval)` convention (verbatim from
//! `/opt/slang/external/ieee1800/vpi_user.h`: "bit encoding: ab: 00=0,
//! 10=1, 11=X, 01=Z"), i.e. `(aval,bval)`: `(0,0)->0, (1,0)->1, (0,1)->Z,
//! (1,1)->X`. This is NOT the same plane-pair as the narrow path's
//! `(val,unk)` "2*unk+val" encoding above (CG-017) -- notably X and Z swap
//! which plane is "distinguishing" -- so the two must never be mixed up or
//! reused for each other's storage. `aval_bval_to_bit`/`bit_to_aval_bval`
//! below are the one pair of conversion functions between the two; every
//! wide-path primitive in `ops.rs` (`wide_add`/`wide_gather`/`wide_scatter`/
//! `lower_const_any`) goes through this same pair, never a re-derived
//! inline match, so a future correction only has one place to change.
//!
//! Per CG-173 / ARCHITECTURE.md ยง7 item 1's pattern for provisional
//! interfaces: this is codegen's PROPOSED convention, pending ratification
//! by State/Nets/Runtime. If a different convention is ultimately
//! ratified, only these two functions (and no other wide-path code, which
//! is written entirely in terms of them) need to change.
use std::num::NonZeroU32;

use inkwell::context::Context;
use inkwell::values::{FloatValue, IntValue, PointerValue};
use sv_lang::{Bit, SVInt};

use crate::error::LoweringError;

// Task 15 (CG-016, CG-019, CG-129): narrow-vs-wide storage dispatch.
//
// CG-016 fixes the narrow representation's own ceiling at `WIDE_THRESHOLD`
// (default 128): a scalar/vector of width `N <= WIDE_THRESHOLD` still lowers
// to one packed LLVM `i(2N)` value (unchanged from Tasks 1-14). CG-019/
// CG-129: above that, a value is instead described by a `WideValueRef` --
// stack storage for `K = ceil(N/64)` limbs of `{aval: i64, bval: i64}`,
// least-significant limb first, matching `svLogicVecVal`'s word layout (per
// this task's own ledger requirement text) so a future DPI pass-through
// needs no reshuffling. `OpBuilder::{wide_alloca,wide_store_limb,
// wide_load_limb}` (ops.rs) are the only things that actually read/write
// that memory; this module only owns the descriptor types plus the pure
// ceil-division arithmetic.
pub const WIDE_THRESHOLD: u32 = 65535;

/// The wide-path memory descriptor (CG-129): a pointer to `limb_count`
/// consecutive `{aval: i64, bval: i64}` limbs on the stack, built by
/// `OpBuilder::wide_alloca`.
#[derive(Clone, Copy)]
pub struct WideValueRef<'ctx> {
    pub base_ptr: PointerValue<'ctx>,
    pub limb_count: u32,
}

/// `ceil(width / 64)`, the exact CG-019/CG-129 limb-count formula --
/// exercised directly (not just as a side effect of `lower_const_any`) by
/// `wide_limb_count_matches_ceil_division` in `tests/wide_value.rs`.
pub fn wide_limb_count(width: u32) -> u32 {
    width.div_ceil(64)
}

/// The single (aval,bval) -> `Bit` decode point for the wide-path polarity
/// documented in this module's header comment (IEEE 1800
/// `t_vpi_vecval`/`svLogicVecVal`: "ab: 00=0, 10=1, 11=X, 01=Z"). The exact
/// CG-173 truth table -- `aval_bval_polarity_round_trips_all_four_states`
/// (tests/wide_arith.rs) checks this against `bit_to_aval_bval` below for
/// an identity round trip on all four `Bit` states.
pub fn aval_bval_to_bit(aval: bool, bval: bool) -> Bit {
    match (aval, bval) {
        (false, false) => Bit::Zero,
        (true, false) => Bit::One,
        (false, true) => Bit::Z,
        (true, true) => Bit::X,
    }
}

/// The single `Bit` -> (aval,bval) encode point for the wide-path polarity
/// -- the inverse of `aval_bval_to_bit` above, same truth table.
pub fn bit_to_aval_bval(b: Bit) -> (bool, bool) {
    match b {
        Bit::Zero => (false, false),
        Bit::One => (true, false),
        Bit::Z => (false, true),
        Bit::X => (true, true),
    }
}

// Deviation from plan: Task 8 (CG-037) already introduced `LoweredValue`
// (`Packed`/`Real`) as `lower_expr`'s actual return-type dispatch point --
// the plan's proposed `AnyValue{Narrow,Wide}` predates that task and would
// either collide with it or require rewiring every existing arm of
// `lower_expr` (not among this task's listed `Files:`, and genuinely out of
// scope while Task 16's wide arithmetic doesn't exist yet to give a wide
// `BinaryOp` result anywhere to come from). `AnyValue` is kept as a small,
// separate, self-contained narrow/wide dispatch type -- exactly the shape
// the plan specifies (`Narrow(PackedValue)`/`Wide(WideValueRef)`, with
// `expect_narrow`) -- produced today only by `OpBuilder::lower_const_any`
// (the constant-lowering dispatch point CG-016/CG-019/CG-129's acceptance
// tests exercise). Wiring `lower_expr` itself through `AnyValue` is left to
// whichever later task first needs a non-constant wide value (Task 16
// onward), same as `docs/ledger.json`'s own CG-016 status ("partial") /
// CG-019 status ("open") already record this as incremental.
#[derive(Clone, Copy)]
pub enum AnyValue<'ctx> {
    Narrow(PackedValue<'ctx>),
    Wide(WideValueRef<'ctx>),
}

impl<'ctx> AnyValue<'ctx> {
    /// Unwraps the narrow case, or a loud `LoweringError` for a wide one --
    /// the "narrow-only unwrap helper" every pre-Task-15 caller of
    /// `lower_expr`/`const_svint` keeps using unchanged.
    pub fn expect_narrow(
        self,
        what: &str,
        sym: &sv_lang::Symbol,
    ) -> Result<PackedValue<'ctx>, LoweringError> {
        match self {
            AnyValue::Narrow(p) => Ok(p),
            AnyValue::Wide(_) => Err(LoweringError::unsupported_at(what, sym)),
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ValueRepr {
    FourState,
    TwoState,
}

#[derive(Clone, Copy)]
pub struct PackedValue<'ctx> {
    pub llvm: IntValue<'ctx>,
    pub width: u32,
    pub repr: ValueRepr,
    /// Whether the originating `sv_lang::Type` is signed
    /// (`Type::is_signed()`), threaded through from the point a
    /// `PackedValue` is first constructed (a literal or a `locals` load) so
    /// signed-sensitive ops (`div4`/`mod4`) never have to re-derive it
    /// ad hoc. Per ARCHITECTURE.md's Codegen note: codegen trusts
    /// slang's already-inserted `Conversion` nodes to equalize operand
    /// signedness before a binary op is lowered, so a binary op's result
    /// (and any op reading a single operand's `signed`) only ever needs to
    /// consult its left/only operand's `signed`, exactly like it already
    /// only consults `a.width`, never `b.width`.
    pub signed: bool,
}

// Task 8 (CG-037): SV `real`/`shortreal` is always 2-state -- there is no
// X/Z representation for a floating value (LRM 6.12.2 doesn't define one),
// so `RealValue` is deliberately a bare `FloatValue` wrapper with NO
// unknown-plane field, unlike `PackedValue`. `real_operand_has_no_unknown_
// plane` (tests/real.rs) is the compile-time proof: constructing one with
// only `llvm` set is the whole test.
#[derive(Clone, Copy)]
pub struct RealValue<'ctx> {
    pub llvm: FloatValue<'ctx>,
}

pub fn readback_bits(bytes: &[u8], width: u32, repr: ValueRepr) -> Vec<Bit> {
    let get = |bit: u32| -> u8 {
        let byte = (bit / 8) as usize;
        if byte >= bytes.len() {
            0
        } else {
            (bytes[byte] >> (bit % 8)) & 1
        }
    };
    (0..width)
        .map(|i| match repr {
            ValueRepr::TwoState => {
                if get(i) == 1 {
                    Bit::One
                } else {
                    Bit::Zero
                }
            }
            ValueRepr::FourState => match 2 * get(width + i) + get(i) {
                0 => Bit::Zero,
                1 => Bit::One,
                2 => Bit::X,
                _ => Bit::Z,
            },
        })
        .collect()
}

// Corrected vs. plan: inkwell 0.10 (LLVM22) `Context::custom_width_int_type`
// takes `NonZeroU32` and returns `Result<IntType, &'static str>`, not a bare
// `u32` -> `IntType`.
fn int_type(ctx: &Context, w: u32) -> inkwell::types::IntType<'_> {
    ctx.custom_width_int_type(NonZeroU32::new(w).expect("zero-width int type"))
        .expect("custom_width_int_type")
}

/// Number of 64-bit LLVM constant words needed to hold `bits` significant
/// bits (`LLVMConstIntOfArbitraryPrecision`'s own word granularity).
fn words_for_bits(bits: u32) -> usize {
    (bits as u64).div_ceil(64) as usize
}

/// Builds the little-endian LLVM word array for a packed 4-state constant
/// (`2*width` significant bits: value lane at bit offset 0, unknown lane at
/// bit offset `width`) or a 2-state one (`width` significant bits, value
/// lane only). Placed one bit at a time -- never by shifting an entire
/// plane by `width` at once -- so this is correct for any `width` up to
/// `WIDE_THRESHOLD` (the narrow path's ceiling), not just `<= 64`: a
/// whole-plane shift by `width>=64` bits is itself the exact overflow bug
/// `const_svint_four_state_width64_boundary_no_panic_no_corruption`
/// (tests/value.rs) already guards for the width==64 case; generalizing to
/// per-bit placement extends that same guarantee up to width==128 without
/// re-deriving a new "how many words, and where do lanes overlap" formula
/// per width class.
pub(crate) fn packed_words(bits_lsb_first: &[Bit], width: u32, repr: ValueRepr) -> Vec<u64> {
    let total_bits = match repr {
        ValueRepr::TwoState => width,
        ValueRepr::FourState => width * 2,
    };
    let mut words = vec![0u64; words_for_bits(total_bits)];
    let mut set_bit = |bit_index: u32, v: bool| {
        if v {
            words[(bit_index / 64) as usize] |= 1u64 << (bit_index % 64);
        }
    };
    for (i, b) in bits_lsb_first.iter().enumerate() {
        let i = i as u32;
        let (vv, uu) = match b {
            Bit::Zero => (false, false),
            Bit::One => (true, false),
            Bit::X => (false, true),
            Bit::Z => (true, true),
        };
        set_bit(i, vv);
        if repr == ValueRepr::FourState {
            set_bit(width + i, uu);
        }
    }
    words
}

pub fn const_svint<'ctx>(ctx: &'ctx Context, v: &SVInt, repr: ValueRepr) -> PackedValue<'ctx> {
    let bits = vj_sim::state::svint_bits(v)
        .expect("a constant whose bits are unrecoverable past sv-lang's expansion cap");
    const_bits(ctx, &bits, v.is_signed(), repr)
}

/// A constant from its bits (LSB first); the width is `bits.len()`.
pub fn const_bits<'ctx>(
    ctx: &'ctx Context,
    bits: &[Bit],
    signed: bool,
    repr: ValueRepr,
) -> PackedValue<'ctx> {
    let width = bits.len() as u32;
    assert!(
        width <= WIDE_THRESHOLD && width > 0,
        "narrow path supports 1..=WIDE_THRESHOLD-bit ({WIDE_THRESHOLD}) constants only"
    );
    let words = packed_words(bits, width, repr);
    let t = match repr {
        ValueRepr::TwoState => int_type(ctx, width),
        ValueRepr::FourState => int_type(ctx, width * 2),
    };
    PackedValue {
        llvm: t.const_int_arbitrary_precision(&words),
        width,
        repr,
        signed,
    }
}