vj-codegen 0.1.0

LLVM code generator of the vj SystemVerilog simulator.
//! PLA modeling system tasks (LRM 1800-2023 20.16): `$async$and$array`,
//! `$sync$nor$plane`, ... -- the sixteen `$<sync|async>$<and|or|nand|nor>$
//! <array|plane>(mem, input_terms, output_terms)` tasks.
//!
//! One call EVALUATES the logic array once: for each personality row `k`
//! (ascending address order, LRM 20.16.3) the row's bits select which input
//! terms take part, the selected terms are combined (AND for `and`/`nand`,
//! OR for `or`/`nor`), `nand`/`nor` invert, and row `k`'s result becomes
//! output term `k` (the first row drives the leftmost output bit, so the
//! output concatenation `{b1,b2,..}` / `b[1:m]` reads in row order). The
//! result is assigned to the output terms by the ordinary lvalue lowering,
//! so concatenation targets and part selects work.
//!
//! * `$sync$...` is exactly that one evaluation at the call (LRM 20.16.1:
//!   the synchronous forms control the time the array is evaluated).
//! * `$async$...` evaluates it at the call too, and the process CFG builder
//!   (`sv_sim_sim::process::cfg`, `walk_pla_async`) additionally forks a
//!   detached process that re-runs the same statement whenever an input
//!   term or any word of the personality memory changes ("the evaluations
//!   are automatically performed whenever an input term changes value or
//!   any word in the personality memory is changed").
//!
//! Personality bit meaning (20.16.4), per memory-row bit (leftmost bit pairs
//! with the leftmost input term):
//!
//! | format | `1`        | `0`            | `x`                  | `z`/`?` |
//! |--------|------------|----------------|----------------------|---------|
//! | array  | take input | do not take it | (not defined: skip)  | skip    |
//! | plane  | take true  | take complement| worst case: term = x | skip    |
//!
//! "Skip" contributes the identity of the row's operation (1 for AND, 0 for
//! OR), which is why an all-`?` row of an `and` plane is constant 1 (LRM
//! 20.16.4 Example 2). An unknown INPUT bit propagates through the ordinary
//! 4-state operators (`xxx -> xxx1` in that example).
//!
//! The array format's treatment of `x`/`z` personality bits is not defined by
//! the LRM ("allows a 1 or 0"); they are treated as 0 (not taken).

use inkwell::values::IntValue;
use sv_lang::kinds::ExpressionKind;
use sv_sim_sim::symid::SymbolUid;

use crate::error::LoweringError;
use crate::expr::{LowerCtx, lower_expr};
use crate::ops::OpBuilder;
use crate::value::PackedValue;

/// `which` parsed from a `$sync$and$array`-style name; `None` when `name` is
/// not one of the sixteen PLA tasks.
#[derive(Clone, Copy)]
struct Pla {
    /// `and`/`nand` (else `or`/`nor`).
    and: bool,
    invert: bool,
    plane: bool,
}

fn parse_name(name: &str) -> Option<Pla> {
    let mut parts = name.strip_prefix('$')?.split('$');
    let kind = parts.next()?;
    let logic = parts.next()?;
    let format = parts.next()?;
    if parts.next().is_some() || !matches!(kind, "sync" | "async") {
        return None;
    }
    let (and, invert) = match logic {
        "and" => (true, false),
        "nand" => (true, true),
        "or" => (false, false),
        "nor" => (false, true),
        _ => return None,
    };
    let plane = match format {
        "array" => false,
        "plane" => true,
        _ => return None,
    };
    Some(Pla { and, invert, plane })
}

/// Whether the system task `name` is one of the sixteen PLA tasks (20.16).
pub fn is_pla_task(name: &str) -> bool {
    parse_name(name).is_some()
}

/// Whether the system task `name` is one of the eight `$async$...` PLA tasks.
pub fn is_async_pla_task(name: &str) -> bool {
    name.starts_with("$async$") && is_pla_task(name)
}

/// Lowers one PLA task call (see the module docs).
pub(crate) fn lower_pla_call<'ctx, 'd>(
    cx: &LowerCtx<'ctx, 'd, '_>,
    name: &str,
    e: sv_lang::Expression<'d>,
) -> Result<(), LoweringError> {
    let pla = parse_name(name).ok_or_else(|| {
        LoweringError::unsupported_at(&format!("{name} is not a PLA task"), &cx.fn_sym)
    })?;
    let ops = e.operands();
    if ops.len() != 3 {
        return Err(LoweringError::unsupported_at(
            &format!(
                "{name} takes (memory, input_terms, output_terms); got {} arguments",
                ops.len()
            ),
            &cx.fn_sym,
        ));
    }
    // The memory argument: a fixed-size unpacked array of this module (the
    // same shape `$readmemb` accepts).
    let unwrap = |x: sv_lang::Expression<'d>| {
        if x.kind() == ExpressionKind::Assignment {
            x.left().unwrap_or(x)
        } else {
            x
        }
    };
    let mem_expr = unwrap(ops[0]);
    let mem_sym = if mem_expr.kind() == ExpressionKind::ArbitrarySymbol {
        mem_expr.arbitrary_symbol()
    } else if sv_sim_sim::symid::is_value_name(mem_expr.kind()) {
        mem_expr.referenced_symbol()
    } else {
        None
    };
    let mem = mem_sym
        .and_then(|s| cx.mem_signals.get(&s.uid()).copied())
        .ok_or_else(|| {
            LoweringError::unsupported_at(
                &format!(
                    "{name}: the first argument must name a fixed-size unpacked array \
                     (the personality memory) of this module (got a {:?} expression)",
                    mem_expr.kind()
                ),
                &cx.fn_sym,
            )
        })?;
    let input = lower_expr(cx, ops[1])?.into_packed(
        &format!("{name}: the input terms must be an integral value"),
        &cx.fn_sym,
    )?;
    let out_lhs = unwrap(ops[2]);
    let out_width = crate::select::lvalue_width(cx, out_lhs)?;
    // LRM 20.16.3: the personality is as wide as the number of input terms
    // and as deep as the number of output terms.
    if mem.elem_width != input.width {
        return Err(LoweringError::unsupported_at(
            &format!(
                "{name}: the personality memory is {} bits wide but there are {} input terms \
                 (LRM 20.16.3: as wide as the number of input terms)",
                mem.elem_width, input.width
            ),
            &cx.fn_sym,
        ));
    }
    if mem.num_rows != out_width {
        return Err(LoweringError::unsupported_at(
            &format!(
                "{name}: the personality memory has {} words but there are {out_width} output \
                 terms (LRM 20.16.3: as deep as the number of output terms)",
                mem.num_rows
            ),
            &cx.fn_sym,
        ));
    }
    let acc = cx.mem_accessor.ok_or_else(|| {
        LoweringError::unsupported_at(
            &format!("{name}: memory read with no accessor context"),
            &cx.fn_sym,
        )
    })?;
    let op = OpBuilder {
        ctx: cx.ctx,
        b: &cx.b,
    };
    let n = input.width;
    let i64_ty = cx.ctx.i64_type();
    let two = |v: IntValue<'ctx>| op.pack(v, op.i_n(n).const_zero(), n, cx.repr, false);
    let mut result: Option<PackedValue<'ctx>> = None;
    for r in 0..mem.num_rows {
        let raw = crate::accessor::build_get_mem_word(
            cx,
            acc.fns,
            acc.handle,
            mem.slot,
            i64_ty.const_int(u64::from(r), false),
            n,
        );
        let row = PackedValue {
            llvm: raw,
            width: n,
            repr: cx.repr,
            signed: false,
        };
        let (v, u) = (op.lane_val(row), op.lane_unk(row));
        let nv = op.not(v);
        let nu = op.not(u);
        let m1 = op.and(v, nu);
        let m0 = op.and(nv, nu);
        let mx = op.and(nv, u);
        // Per-bit term: the input where the bit selects it, else the
        // operation's identity.
        let a = input;
        let term = match (pla.and, pla.plane) {
            // (A | ~M1) & (~A | ~M0)
            (true, true) => {
                let t1 = op.or4(a, two(op.not(m1)));
                let t0 = op.or4(op.not4(a), two(op.not(m0)));
                op.and4(t1, t0)
            }
            // A | ~M1
            (true, false) => op.or4(a, two(op.not(m1))),
            // (A & M1) | (~A & M0)
            (false, true) => {
                let t1 = op.and4(a, two(m1));
                let t0 = op.and4(op.not4(a), two(m0));
                op.or4(t1, t0)
            }
            // A & M1
            (false, false) => op.and4(a, two(m1)),
        };
        // Plane `x`: the "worst case" -- the term is unknown whatever the
        // input is (0 ^ x = 1 ^ x = x).
        let term = if pla.plane {
            op.xor4(term, op.pack(mx, mx, n, cx.repr, false))
        } else {
            term
        };
        let bit = if pla.and {
            op.redand4(term)
        } else {
            op.redor4(term)
        };
        let bit = if pla.invert { op.not4(bit) } else { bit };
        // Row 0 is the leftmost output term: shift the accumulated result up
        // and append this row's bit at the least significant end.
        result = Some(match result {
            None => bit,
            Some(acc_v) => concat(&op, acc_v, bit, cx.repr),
        });
    }
    let value = result.ok_or_else(|| {
        LoweringError::unsupported_at(&format!("{name}: an empty personality memory"), &cx.fn_sym)
    })?;
    crate::select::lower_assign_to(cx, out_lhs, value)
}

/// `{hi, lo}` of two packed values (no sign).
fn concat<'ctx>(
    op: &OpBuilder<'_, 'ctx>,
    hi: PackedValue<'ctx>,
    lo: PackedValue<'ctx>,
    repr: crate::value::ValueRepr,
) -> PackedValue<'ctx> {
    let w = hi.width + lo.width;
    let ext = |x: IntValue<'ctx>| op.b.build_int_z_extend(x, op.i_n(w), "pla_ext").unwrap();
    let sh = |x: IntValue<'ctx>| {
        op.b.build_left_shift(x, op.i_n(w).const_int(u64::from(lo.width), false), "pla_sh")
            .unwrap()
    };
    let val = op.or(sh(ext(op.lane_val(hi))), ext(op.lane_val(lo)));
    let unk = op.or(sh(ext(op.lane_unk(hi))), ext(op.lane_unk(lo)));
    op.pack(val, unk, w, repr, false)
}