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