Skip to main content

sc_neurocore_engine/ir/
emit_sv.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later | Commercial license available
2// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
3// © Code 2020–2026 Miroslav Šotek. All rights reserved.
4// ORCID: 0009-0009-3560-0851
5// Contact: www.anulum.li | protoscience@anulum.li
6// SC-NeuroCore — SystemVerilog emitter for SC IR graphs
7
8//! SystemVerilog emitter for SC IR graphs.
9//!
10//! Produces synthesizable RTL that instantiates modules from `hdl/`.
11//!
12//! Generated module interface:
13//! - Clock: `clk`
14//! - Reset: `rst_n` (active-low)
15//! - One port per `sc.input` / `sc.output` operation
16//! - Internal wiring for all intermediate values
17
18use crate::ir::graph::*;
19
20/// Emit a synthesizable SystemVerilog module from an SC graph.
21///
22/// The graph should pass `verify::verify()` before emission.
23pub fn emit(graph: &ScGraph) -> Result<String, String> {
24    let mut sv = String::new();
25
26    // Header
27    sv.push_str(&format!(
28        "// Auto-generated by SC-NeuroCore IR Compiler v3.0\n\
29         // Source graph: {}\n\
30         // Do not edit — regenerate from IR source.\n\n",
31        graph.name
32    ));
33    sv.push_str("`timescale 1ns / 1ps\n\n");
34
35    // Module declaration
36    sv.push_str(&format!("module {} (\n", graph.name));
37    sv.push_str("    input wire clk,\n");
38    sv.push_str("    input wire rst_n");
39
40    // Collect inputs and outputs for port list
41    for op in &graph.ops {
42        match op {
43            ScOp::Input { name, ty, .. } => {
44                let port_width = type_to_width(ty);
45                if port_width == 1 {
46                    sv.push_str(&format!(",\n    input wire {}", name));
47                } else {
48                    sv.push_str(&format!(
49                        ",\n    input wire [{}:0] {}",
50                        port_width - 1,
51                        name
52                    ));
53                }
54            }
55            ScOp::Output { name, source, .. } => {
56                let width = find_value_width(graph, *source);
57                if width == 1 {
58                    sv.push_str(&format!(",\n    output wire {}", name));
59                } else {
60                    sv.push_str(&format!(",\n    output wire [{}:0] {}", width - 1, name));
61                }
62            }
63            _ => {}
64        }
65    }
66    sv.push_str("\n);\n\n");
67
68    // Wire declarations for intermediate values
69    for op in &graph.ops {
70        match op {
71            ScOp::Input { .. } | ScOp::Output { .. } => {}
72            ScOp::Constant { id, value, .. } => emit_constant(&mut sv, *id, value),
73            ScOp::Encode { id, .. } => {
74                sv.push_str(&format!("    wire v{};\n", id.0));
75            }
76            ScOp::BitwiseAnd { id, .. } => {
77                sv.push_str(&format!("    wire v{};\n", id.0));
78            }
79            ScOp::Popcount { id, .. } => {
80                sv.push_str(&format!("    logic [63:0] v{};\n", id.0));
81            }
82            ScOp::LifStep { id, params, .. } => {
83                sv.push_str(&format!(
84                    "    wire v{}_spike;\n    wire signed [{}:0] v{}_v_out;\n",
85                    id.0,
86                    params.data_width - 1,
87                    id.0
88                ));
89            }
90            ScOp::DenseForward { id, params, .. } => {
91                sv.push_str(&format!(
92                    "    wire [{}:0] v{}_spikes;\n    wire v{}_running;\n    wire v{}_done;\n",
93                    params.n_neurons - 1,
94                    id.0,
95                    id.0,
96                    id.0
97                ));
98            }
99            ScOp::BitwiseXor { id, .. } => {
100                sv.push_str(&format!("    wire v{};\n", id.0));
101            }
102            ScOp::Reduce { id, .. } => {
103                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
104            }
105            ScOp::GraphForward { id, n_features, .. } => {
106                sv.push_str(&format!(
107                    "    wire [{}:0] v{};\n",
108                    n_features.saturating_sub(1).max(0),
109                    id.0
110                ));
111            }
112            ScOp::SoftmaxAttention { id, .. } => {
113                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
114            }
115            ScOp::KuramotoStep { id, .. } => {
116                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
117            }
118            ScOp::Scale { id, .. } | ScOp::Offset { id, .. } | ScOp::DivConst { id, .. } => {
119                sv.push_str(&format!("    wire [63:0] v{};\n", id.0));
120            }
121        }
122    }
123    sv.push('\n');
124
125    let mut inst_idx = 0_u32;
126
127    // Module instantiations
128    for op in &graph.ops {
129        match op {
130            ScOp::Encode { id, prob, seed, .. } => {
131                let prob_wire = value_to_wire(graph, *prob);
132                sv.push_str(&format!(
133                    "    sc_bitstream_encoder #(\n\
134                     \x20       .DATA_WIDTH(16),\n\
135                     \x20       .SEED_INIT(16'h{:04X})\n\
136                     \x20   ) u_enc_{} (\n\
137                     \x20       .clk(clk),\n\
138                     \x20       .rst_n(rst_n),\n\
139                     \x20       .x_value({}),\n\
140                     \x20       .t_index(32'd0),\n\
141                     \x20       .bit_out(v{})\n\
142                     \x20   );\n\n",
143                    seed, inst_idx, prob_wire, id.0
144                ));
145                inst_idx += 1;
146            }
147            ScOp::BitwiseAnd { id, lhs, rhs } => {
148                let lhs_wire = value_to_wire(graph, *lhs);
149                let rhs_wire = value_to_wire(graph, *rhs);
150                sv.push_str(&format!(
151                    "    sc_bitstream_synapse u_syn_{} (\n\
152                     \x20       .pre_bit({}),\n\
153                     \x20       .w_bit({}),\n\
154                     \x20       .post_bit(v{})\n\
155                     \x20   );\n\n",
156                    inst_idx, lhs_wire, rhs_wire, id.0
157                ));
158                inst_idx += 1;
159            }
160            ScOp::LifStep {
161                id,
162                current,
163                leak,
164                gain,
165                noise,
166                params,
167            } => {
168                let current_wire = value_to_wire(graph, *current);
169                let leak_wire = value_to_wire(graph, *leak);
170                let gain_wire = value_to_wire(graph, *gain);
171                let noise_wire = value_to_wire(graph, *noise);
172                sv.push_str(&format!(
173                    "    sc_lif_neuron #(\n\
174                     \x20       .DATA_WIDTH({}),\n\
175                     \x20       .FRACTION({}),\n\
176                     \x20       .V_REST({}),\n\
177                     \x20       .V_RESET({}),\n\
178                     \x20       .V_THRESHOLD({}),\n\
179                     \x20       .REFRACTORY_PERIOD({})\n\
180                     \x20   ) u_lif_{} (\n\
181                     \x20       .clk(clk),\n\
182                     \x20       .rst_n(rst_n),\n\
183                     \x20       .leak_k({}),\n\
184                     \x20       .gain_k({}),\n\
185                     \x20       .I_t({}),\n\
186                     \x20       .noise_in({}),\n\
187                     \x20       .spike_out(v{}_spike),\n\
188                     \x20       .v_out(v{}_v_out)\n\
189                     \x20   );\n\n",
190                    params.data_width,
191                    params.fraction,
192                    params.v_rest,
193                    params.v_reset,
194                    params.v_threshold,
195                    params.refractory_period,
196                    inst_idx,
197                    leak_wire,
198                    gain_wire,
199                    current_wire,
200                    noise_wire,
201                    id.0,
202                    id.0
203                ));
204                inst_idx += 1;
205            }
206            ScOp::DenseForward {
207                id,
208                inputs,
209                weights,
210                leak,
211                gain,
212                params,
213            } => {
214                let inputs_wire = value_to_wire(graph, *inputs);
215                let weights_wire = value_to_wire(graph, *weights);
216                let leak_wire = value_to_wire(graph, *leak);
217                let gain_wire = value_to_wire(graph, *gain);
218                sv.push_str(&format!(
219                    "    sc_dense_layer_core #(\n\
220                     \x20       .N_INPUTS({}),\n\
221                     \x20       .N_NEURONS({}),\n\
222                     \x20       .DATA_WIDTH({})\n\
223                     \x20   ) u_dense_{} (\n\
224                     \x20       .clk(clk),\n\
225                     \x20       .rst_n(rst_n),\n\
226                     \x20       .start_pulse(1'b1),\n\
227                     \x20       .stream_len(32'd{}),\n\
228                     \x20       .x_input_fp({}),\n\
229                     \x20       .weight_fp({}),\n\
230                     \x20       .y_min_fp(16'd0),\n\
231                     \x20       .y_max_fp(16'd256),\n\
232                     \x20       .cfg_leak({}),\n\
233                     \x20       .cfg_gain({}),\n\
234                     \x20       .I_t(),\n\
235                     \x20       .spikes(v{}_spikes),\n\
236                     \x20       .step_valid(),\n\
237                     \x20       .run_done(v{}_done),\n\
238                     \x20       .running(v{}_running)\n\
239                     \x20   );\n\n",
240                    params.n_inputs,
241                    params.n_neurons,
242                    params.data_width,
243                    inst_idx,
244                    params.stream_length,
245                    inputs_wire,
246                    weights_wire,
247                    leak_wire,
248                    gain_wire,
249                    id.0,
250                    id.0,
251                    id.0
252                ));
253                inst_idx += 1;
254            }
255            ScOp::BitwiseXor { id, lhs, rhs } => {
256                let lhs_wire = value_to_wire(graph, *lhs);
257                let rhs_wire = value_to_wire(graph, *rhs);
258                sv.push_str(&format!(
259                    "    assign v{} = {} ^ {};\n",
260                    id.0, lhs_wire, rhs_wire
261                ));
262            }
263            ScOp::Reduce { id, input, mode } => {
264                let in_wire = value_to_wire(graph, *input);
265                let label = match mode {
266                    ReduceMode::Sum => "reduce_sum",
267                    ReduceMode::Max => "reduce_max",
268                };
269                sv.push_str(&format!(
270                    "    // {label}: passthrough for single-element; multi-element requires adder/comparator tree\n\
271                     \x20   assign v{id} = {wire};\n",
272                    label = label,
273                    id = id.0,
274                    wire = in_wire,
275                ));
276            }
277            ScOp::GraphForward {
278                id,
279                features: _,
280                adjacency: _,
281                n_nodes,
282                n_features,
283            } => {
284                return Err(format!(
285                    "GraphForward (v{}, {} nodes × {} features) has no synthesizable RTL implementation yet",
286                    id.0, n_nodes, n_features
287                ));
288            }
289            ScOp::SoftmaxAttention { id, dim_k, .. } => {
290                return Err(format!(
291                    "SoftmaxAttention (v{}, dim_k={}) has no synthesizable RTL implementation yet",
292                    id.0, dim_k
293                ));
294            }
295            ScOp::KuramotoStep { id, .. } => {
296                return Err(format!(
297                    "KuramotoStep (v{}) has no synthesizable RTL implementation yet",
298                    id.0
299                ));
300            }
301            ScOp::Output { name, source, .. } => {
302                let src_wire = value_to_wire(graph, *source);
303                sv.push_str(&format!("    assign {} = {};\n", name, src_wire));
304            }
305            ScOp::Scale { id, input, factor } => {
306                let in_wire = value_to_wire(graph, *input);
307                let scale_int = (*factor * 256.0) as i64; // Q8.8
308                sv.push_str(&format!(
309                    "    assign v{} = ({} * {}) >>> 8;\n",
310                    id.0, in_wire, scale_int
311                ));
312            }
313            ScOp::Offset { id, input, offset } => {
314                let in_wire = value_to_wire(graph, *input);
315                let offset_int = (*offset * 256.0) as i64;
316                sv.push_str(&format!(
317                    "    assign v{} = {} + {};\n",
318                    id.0, in_wire, offset_int
319                ));
320            }
321            ScOp::DivConst { id, input, divisor } => {
322                let in_wire = value_to_wire(graph, *input);
323                sv.push_str(&format!(
324                    "    assign v{} = {} / {};\n",
325                    id.0, in_wire, divisor
326                ));
327            }
328            ScOp::Popcount { id, input } => {
329                let in_wire = value_to_wire(graph, *input);
330                sv.push_str(&format!(
331                    "    // Combinatorial popcount for v{id}\n\
332                     \x20   always_comb begin\n\
333                     \x20       v{id} = 64'd0;\n\
334                     \x20       for (integer _pc_i = 0; _pc_i < 64; _pc_i = _pc_i + 1)\n\
335                     \x20           v{id} = v{id} + {{63'd0, {wire}[_pc_i]}};\n\
336                     \x20   end\n\n",
337                    id = id.0,
338                    wire = in_wire,
339                ));
340            }
341            _ => {}
342        }
343    }
344
345    sv.push_str("\nendmodule\n");
346    Ok(sv)
347}
348
349fn type_to_width(ty: &ScType) -> usize {
350    ty.bit_width()
351}
352
353fn find_value_width(graph: &ScGraph, id: ValueId) -> usize {
354    for op in &graph.ops {
355        if op.result_id() == id {
356            return match op {
357                ScOp::Input { ty, .. } => type_to_width(ty),
358                ScOp::Constant { ty, .. } => type_to_width(ty),
359                ScOp::Encode { .. } | ScOp::BitwiseAnd { .. } | ScOp::BitwiseXor { .. } => 1,
360                ScOp::Popcount { .. } | ScOp::Reduce { .. } => 64,
361                ScOp::LifStep { params, .. } => params.data_width as usize,
362                ScOp::DenseForward { params, .. } => params.n_neurons,
363                ScOp::GraphForward { n_features, .. } => *n_features,
364                ScOp::SoftmaxAttention { .. }
365                | ScOp::KuramotoStep { .. }
366                | ScOp::Scale { .. }
367                | ScOp::Offset { .. }
368                | ScOp::DivConst { .. } => 64,
369                ScOp::Output { source, .. } => find_value_width(graph, *source),
370            };
371        }
372    }
373    16
374}
375
376fn value_to_wire(graph: &ScGraph, id: ValueId) -> String {
377    for op in &graph.ops {
378        if op.result_id() == id {
379            return match op {
380                ScOp::Input { name, .. } => name.clone(),
381                ScOp::Constant { id, .. } => format!("c{}", id.0),
382                ScOp::LifStep { id, .. } => format!("v{}_spike", id.0),
383                ScOp::DenseForward { id, .. } => format!("v{}_spikes", id.0),
384                _ => format!("v{}", id.0),
385            };
386        }
387    }
388    format!("v{}", id.0)
389}
390
391fn emit_constant(sv: &mut String, id: ValueId, value: &ScConst) {
392    match value {
393        ScConst::F64(v) => {
394            let fp = (*v * 256.0) as i64; // Q8.8
395            sv.push_str(&format!(
396                "    localparam signed [15:0] c{} = 16'sd{};\n",
397                id.0, fp
398            ));
399        }
400        ScConst::I64(v) => {
401            sv.push_str(&format!(
402                "    localparam signed [15:0] c{} = 16'sd{};\n",
403                id.0, v
404            ));
405        }
406        ScConst::U64(v) => {
407            sv.push_str(&format!("    localparam [31:0] c{} = 32'd{};\n", id.0, v));
408        }
409        ScConst::F64Vec(vec) => {
410            let width = vec.len().saturating_mul(16);
411            if width == 0 {
412                sv.push_str(&format!("    wire [0:0] c{};\n", id.0));
413                return;
414            }
415            sv.push_str(&format!("    wire [{}:0] c{};\n", width - 1, id.0));
416            for (i, v) in vec.iter().enumerate() {
417                let fp = (*v * 256.0) as i64;
418                sv.push_str(&format!(
419                    "    assign c{}[{} +: 16] = 16'sd{};\n",
420                    id.0,
421                    i * 16,
422                    fp
423                ));
424            }
425        }
426        ScConst::I64Vec(vec) => {
427            let width = vec.len().saturating_mul(16);
428            if width == 0 {
429                sv.push_str(&format!("    wire [0:0] c{};\n", id.0));
430                return;
431            }
432            sv.push_str(&format!("    wire [{}:0] c{};\n", width - 1, id.0));
433            for (i, v) in vec.iter().enumerate() {
434                sv.push_str(&format!(
435                    "    assign c{}[{} +: 16] = 16'sd{};\n",
436                    id.0,
437                    i * 16,
438                    v
439                ));
440            }
441        }
442    }
443}