use crate::ir::graph::*;
pub fn emit(graph: &ScGraph) -> Result<String, String> {
let mut sv = String::new();
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");
sv.push_str(&format!("module {} (\n", graph.name));
sv.push_str(" input wire clk,\n");
sv.push_str(" input wire rst_n");
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");
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::BitwiseXor { id, .. } => {
sv.push_str(&format!(" wire v{};\n", id.0));
}
ScOp::Reduce { id, .. } => {
sv.push_str(&format!(" wire [63:0] v{};\n", id.0));
}
ScOp::GraphForward { id, n_features, .. } => {
sv.push_str(&format!(
" wire [{}:0] v{};\n",
n_features.saturating_sub(1),
id.0
));
}
ScOp::SoftmaxAttention { id, .. } => {
sv.push_str(&format!(" wire [63:0] v{};\n", id.0));
}
ScOp::KuramotoStep { id, .. } => {
sv.push_str(&format!(" wire [63:0] v{};\n", 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;
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::BitwiseXor { id, lhs, rhs } => {
let lhs_wire = value_to_wire(graph, *lhs);
let rhs_wire = value_to_wire(graph, *rhs);
sv.push_str(&format!(
" assign v{} = {} ^ {};\n",
id.0, lhs_wire, rhs_wire
));
}
ScOp::Reduce { id, input, mode } => {
let in_wire = value_to_wire(graph, *input);
let label = match mode {
ReduceMode::Sum => "reduce_sum",
ReduceMode::Max => "reduce_max",
};
sv.push_str(&format!(
" // {label}: passthrough for single-element; multi-element requires adder/comparator tree\n\
\x20 assign v{id} = {wire};\n",
label = label,
id = id.0,
wire = in_wire,
));
}
ScOp::GraphForward {
id,
features: _,
adjacency: _,
n_nodes,
n_features,
} => {
return Err(format!(
"GraphForward (v{}, {} nodes × {} features) has no synthesizable RTL implementation yet",
id.0, n_nodes, n_features
));
}
ScOp::SoftmaxAttention { id, dim_k, .. } => {
return Err(format!(
"SoftmaxAttention (v{}, dim_k={}) has no synthesizable RTL implementation yet",
id.0, dim_k
));
}
ScOp::KuramotoStep { id, .. } => {
return Err(format!(
"KuramotoStep (v{}) has no synthesizable RTL implementation yet",
id.0
));
}
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; 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");
Ok(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 { .. } | ScOp::BitwiseXor { .. } => 1,
ScOp::Popcount { .. } | ScOp::Reduce { .. } => 64,
ScOp::LifStep { params, .. } => params.data_width as usize,
ScOp::DenseForward { params, .. } => params.n_neurons,
ScOp::GraphForward { n_features, .. } => *n_features,
ScOp::SoftmaxAttention { .. }
| ScOp::KuramotoStep { .. }
| 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; 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
));
}
}
}
}