1use crate::ir::graph::*;
19
20pub fn emit(graph: &ScGraph) -> Result<String, String> {
24 let mut sv = String::new();
25
26 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 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 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 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 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; 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; 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}