sc_neurocore_engine 3.8.2

High-performance SIMD backend for SC-NeuroCore stochastic neuromorphic computing
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later
//! SystemVerilog emitter for SC IR graphs.
//!
//! Produces synthesizable RTL that instantiates modules from `hdl/`.
//!
//! Generated module interface:
//! - Clock: `clk`
//! - Reset: `rst_n` (active-low)
//! - One port per `sc.input` / `sc.output` operation
//! - Internal wiring for all intermediate values

use crate::ir::graph::*;

/// Emit a synthesizable SystemVerilog module from an SC graph.
///
/// The graph should pass `verify::verify()` before emission.
pub fn emit(graph: &ScGraph) -> String {
    let mut sv = String::new();

    // Header
    sv.push_str(&format!(
        "// Auto-generated by SC-NeuroCore IR Compiler v3.0\n\
         // Source graph: {}\n\
         // Do not edit — regenerate from IR source.\n\n",
        graph.name
    ));
    sv.push_str("`timescale 1ns / 1ps\n\n");

    // Module declaration
    sv.push_str(&format!("module {} (\n", graph.name));
    sv.push_str("    input wire clk,\n");
    sv.push_str("    input wire rst_n");

    // Collect inputs and outputs for port list
    for op in &graph.ops {
        match op {
            ScOp::Input { name, ty, .. } => {
                let port_width = type_to_width(ty);
                if port_width == 1 {
                    sv.push_str(&format!(",\n    input wire {}", name));
                } else {
                    sv.push_str(&format!(
                        ",\n    input wire [{}:0] {}",
                        port_width - 1,
                        name
                    ));
                }
            }
            ScOp::Output { name, source, .. } => {
                let width = find_value_width(graph, *source);
                if width == 1 {
                    sv.push_str(&format!(",\n    output wire {}", name));
                } else {
                    sv.push_str(&format!(",\n    output wire [{}:0] {}", width - 1, name));
                }
            }
            _ => {}
        }
    }
    sv.push_str("\n);\n\n");

    // Wire declarations for intermediate values
    for op in &graph.ops {
        match op {
            ScOp::Input { .. } | ScOp::Output { .. } => {}
            ScOp::Constant { id, value, .. } => emit_constant(&mut sv, *id, value),
            ScOp::Encode { id, .. } => {
                sv.push_str(&format!("    wire v{};\n", id.0));
            }
            ScOp::BitwiseAnd { id, .. } => {
                sv.push_str(&format!("    wire v{};\n", id.0));
            }
            ScOp::Popcount { id, .. } => {
                sv.push_str(&format!("    logic [63:0] v{};\n", id.0));
            }
            ScOp::LifStep { id, params, .. } => {
                sv.push_str(&format!(
                    "    wire v{}_spike;\n    wire signed [{}:0] v{}_v_out;\n",
                    id.0,
                    params.data_width - 1,
                    id.0
                ));
            }
            ScOp::DenseForward { id, params, .. } => {
                sv.push_str(&format!(
                    "    wire [{}:0] v{}_spikes;\n    wire v{}_running;\n    wire v{}_done;\n",
                    params.n_neurons - 1,
                    id.0,
                    id.0,
                    id.0
                ));
            }
            ScOp::Scale { id, .. } | ScOp::Offset { id, .. } | ScOp::DivConst { id, .. } => {
                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
            }
        }
    }
    sv.push('\n');

    let mut inst_idx = 0_u32;

    // Module instantiations
    for op in &graph.ops {
        match op {
            ScOp::Encode { id, prob, seed, .. } => {
                let prob_wire = value_to_wire(graph, *prob);
                sv.push_str(&format!(
                    "    sc_bitstream_encoder #(\n\
                     \x20       .DATA_WIDTH(16),\n\
                     \x20       .SEED_INIT(16'h{:04X})\n\
                     \x20   ) u_enc_{} (\n\
                     \x20       .clk(clk),\n\
                     \x20       .rst_n(rst_n),\n\
                     \x20       .x_value({}),\n\
                     \x20       .t_index(32'd0),\n\
                     \x20       .bit_out(v{})\n\
                     \x20   );\n\n",
                    seed, inst_idx, prob_wire, id.0
                ));
                inst_idx += 1;
            }
            ScOp::BitwiseAnd { id, lhs, rhs } => {
                let lhs_wire = value_to_wire(graph, *lhs);
                let rhs_wire = value_to_wire(graph, *rhs);
                sv.push_str(&format!(
                    "    sc_bitstream_synapse u_syn_{} (\n\
                     \x20       .pre_bit({}),\n\
                     \x20       .w_bit({}),\n\
                     \x20       .post_bit(v{})\n\
                     \x20   );\n\n",
                    inst_idx, lhs_wire, rhs_wire, id.0
                ));
                inst_idx += 1;
            }
            ScOp::LifStep {
                id,
                current,
                leak,
                gain,
                noise,
                params,
            } => {
                let current_wire = value_to_wire(graph, *current);
                let leak_wire = value_to_wire(graph, *leak);
                let gain_wire = value_to_wire(graph, *gain);
                let noise_wire = value_to_wire(graph, *noise);
                sv.push_str(&format!(
                    "    sc_lif_neuron #(\n\
                     \x20       .DATA_WIDTH({}),\n\
                     \x20       .FRACTION({}),\n\
                     \x20       .V_REST({}),\n\
                     \x20       .V_RESET({}),\n\
                     \x20       .V_THRESHOLD({}),\n\
                     \x20       .REFRACTORY_PERIOD({})\n\
                     \x20   ) u_lif_{} (\n\
                     \x20       .clk(clk),\n\
                     \x20       .rst_n(rst_n),\n\
                     \x20       .leak_k({}),\n\
                     \x20       .gain_k({}),\n\
                     \x20       .I_t({}),\n\
                     \x20       .noise_in({}),\n\
                     \x20       .spike_out(v{}_spike),\n\
                     \x20       .v_out(v{}_v_out)\n\
                     \x20   );\n\n",
                    params.data_width,
                    params.fraction,
                    params.v_rest,
                    params.v_reset,
                    params.v_threshold,
                    params.refractory_period,
                    inst_idx,
                    leak_wire,
                    gain_wire,
                    current_wire,
                    noise_wire,
                    id.0,
                    id.0
                ));
                inst_idx += 1;
            }
            ScOp::DenseForward {
                id,
                inputs,
                weights,
                leak,
                gain,
                params,
            } => {
                let inputs_wire = value_to_wire(graph, *inputs);
                let weights_wire = value_to_wire(graph, *weights);
                let leak_wire = value_to_wire(graph, *leak);
                let gain_wire = value_to_wire(graph, *gain);
                sv.push_str(&format!(
                    "    sc_dense_layer_core #(\n\
                     \x20       .N_INPUTS({}),\n\
                     \x20       .N_NEURONS({}),\n\
                     \x20       .DATA_WIDTH({})\n\
                     \x20   ) u_dense_{} (\n\
                     \x20       .clk(clk),\n\
                     \x20       .rst_n(rst_n),\n\
                     \x20       .start_pulse(1'b1),\n\
                     \x20       .stream_len(32'd{}),\n\
                     \x20       .x_input_fp({}),\n\
                     \x20       .weight_fp({}),\n\
                     \x20       .y_min_fp(16'd0),\n\
                     \x20       .y_max_fp(16'd256),\n\
                     \x20       .cfg_leak({}),\n\
                     \x20       .cfg_gain({}),\n\
                     \x20       .I_t(),\n\
                     \x20       .spikes(v{}_spikes),\n\
                     \x20       .step_valid(),\n\
                     \x20       .run_done(v{}_done),\n\
                     \x20       .running(v{}_running)\n\
                     \x20   );\n\n",
                    params.n_inputs,
                    params.n_neurons,
                    params.data_width,
                    inst_idx,
                    params.stream_length,
                    inputs_wire,
                    weights_wire,
                    leak_wire,
                    gain_wire,
                    id.0,
                    id.0,
                    id.0
                ));
                inst_idx += 1;
            }
            ScOp::Output { name, source, .. } => {
                let src_wire = value_to_wire(graph, *source);
                sv.push_str(&format!("    assign {} = {};\n", name, src_wire));
            }
            ScOp::Scale { id, input, factor } => {
                let in_wire = value_to_wire(graph, *input);
                let scale_int = (*factor * 256.0) as i64; // Q8.8
                sv.push_str(&format!(
                    "    assign v{} = ({} * {}) >>> 8;\n",
                    id.0, in_wire, scale_int
                ));
            }
            ScOp::Offset { id, input, offset } => {
                let in_wire = value_to_wire(graph, *input);
                let offset_int = (*offset * 256.0) as i64;
                sv.push_str(&format!(
                    "    assign v{} = {} + {};\n",
                    id.0, in_wire, offset_int
                ));
            }
            ScOp::DivConst { id, input, divisor } => {
                let in_wire = value_to_wire(graph, *input);
                sv.push_str(&format!(
                    "    assign v{} = {} / {};\n",
                    id.0, in_wire, divisor
                ));
            }
            ScOp::Popcount { id, input } => {
                let in_wire = value_to_wire(graph, *input);
                sv.push_str(&format!(
                    "    // Combinatorial popcount for v{id}\n\
                     \x20   always_comb begin\n\
                     \x20       v{id} = 64'd0;\n\
                     \x20       for (integer _pc_i = 0; _pc_i < 64; _pc_i = _pc_i + 1)\n\
                     \x20           v{id} = v{id} + {{63'd0, {wire}[_pc_i]}};\n\
                     \x20   end\n\n",
                    id = id.0,
                    wire = in_wire,
                ));
            }
            _ => {}
        }
    }

    sv.push_str("\nendmodule\n");
    sv
}

fn type_to_width(ty: &ScType) -> usize {
    ty.bit_width()
}

fn find_value_width(graph: &ScGraph, id: ValueId) -> usize {
    for op in &graph.ops {
        if op.result_id() == id {
            return match op {
                ScOp::Input { ty, .. } => type_to_width(ty),
                ScOp::Constant { ty, .. } => type_to_width(ty),
                ScOp::Encode { .. } | ScOp::BitwiseAnd { .. } => 1,
                ScOp::Popcount { .. } => 64,
                ScOp::LifStep { params, .. } => params.data_width as usize,
                ScOp::DenseForward { params, .. } => params.n_neurons,
                ScOp::Scale { .. } | ScOp::Offset { .. } | ScOp::DivConst { .. } => 64,
                ScOp::Output { source, .. } => find_value_width(graph, *source),
            };
        }
    }
    16
}

fn value_to_wire(graph: &ScGraph, id: ValueId) -> String {
    for op in &graph.ops {
        if op.result_id() == id {
            return match op {
                ScOp::Input { name, .. } => name.clone(),
                ScOp::Constant { id, .. } => format!("c{}", id.0),
                ScOp::LifStep { id, .. } => format!("v{}_spike", id.0),
                ScOp::DenseForward { id, .. } => format!("v{}_spikes", id.0),
                _ => format!("v{}", id.0),
            };
        }
    }
    format!("v{}", id.0)
}

fn emit_constant(sv: &mut String, id: ValueId, value: &ScConst) {
    match value {
        ScConst::F64(v) => {
            let fp = (*v * 256.0) as i64; // Q8.8
            sv.push_str(&format!(
                "    localparam signed [15:0] c{} = 16'sd{};\n",
                id.0, fp
            ));
        }
        ScConst::I64(v) => {
            sv.push_str(&format!(
                "    localparam signed [15:0] c{} = 16'sd{};\n",
                id.0, v
            ));
        }
        ScConst::U64(v) => {
            sv.push_str(&format!("    localparam [31:0] c{} = 32'd{};\n", id.0, v));
        }
        ScConst::F64Vec(vec) => {
            let width = vec.len().saturating_mul(16);
            if width == 0 {
                sv.push_str(&format!("    wire [0:0] c{};\n", id.0));
                return;
            }
            sv.push_str(&format!("    wire [{}:0] c{};\n", width - 1, id.0));
            for (i, v) in vec.iter().enumerate() {
                let fp = (*v * 256.0) as i64;
                sv.push_str(&format!(
                    "    assign c{}[{} +: 16] = 16'sd{};\n",
                    id.0,
                    i * 16,
                    fp
                ));
            }
        }
        ScConst::I64Vec(vec) => {
            let width = vec.len().saturating_mul(16);
            if width == 0 {
                sv.push_str(&format!("    wire [0:0] c{};\n", id.0));
                return;
            }
            sv.push_str(&format!("    wire [{}:0] c{};\n", width - 1, id.0));
            for (i, v) in vec.iter().enumerate() {
                sv.push_str(&format!(
                    "    assign c{}[{} +: 16] = 16'sd{};\n",
                    id.0,
                    i * 16,
                    v
                ));
            }
        }
    }
}