Skip to main content

sc_neurocore_engine/ir/
emit_sv.rs

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