1use crate::ir::graph::*;
20use crate::ir::sv_target::{ResourceReport, SvTarget};
21
22pub fn emit(graph: &ScGraph) -> Result<String, String> {
26 emit_systemverilog_with_target(graph, SvTarget::Generic).map(|(systemverilog, _)| systemverilog)
27}
28
29pub fn emit_systemverilog_with_target(
31 graph: &ScGraph,
32 target: SvTarget,
33) -> Result<(String, ResourceReport), String> {
34 let mut sv = String::new();
35
36 sv.push_str(&format!(
38 "// Auto-generated by SC-NeuroCore IR Compiler v3.0\n\
39 // Source graph: {}\n\
40 // Do not edit — regenerate from IR source.\n\n",
41 graph.name
42 ));
43 sv.push_str(&target.header_comment());
44 sv.push_str("`timescale 1ns / 1ps\n\n");
45
46 sv.push_str(&format!("module {} (\n", graph.name));
48 sv.push_str(" input wire clk,\n");
49 sv.push_str(" input wire rst_n");
50
51 for op in &graph.ops {
53 match op {
54 ScOp::Input { name, ty, .. } => {
55 let port_width = type_to_width(ty);
56 if port_width == 1 {
57 sv.push_str(&format!(",\n input wire {}", name));
58 } else {
59 sv.push_str(&format!(
60 ",\n input wire [{}:0] {}",
61 port_width - 1,
62 name
63 ));
64 }
65 }
66 ScOp::Output { name, source, .. } => {
67 let width = find_value_width(graph, *source);
68 if width == 1 {
69 sv.push_str(&format!(",\n output wire {}", name));
70 } else {
71 sv.push_str(&format!(",\n output wire [{}:0] {}", width - 1, name));
72 }
73 }
74 _ => {}
75 }
76 }
77 sv.push_str("\n);\n\n");
78
79 for op in &graph.ops {
81 match op {
82 ScOp::Input { .. } | ScOp::Output { .. } => {}
83 ScOp::Constant { id, value, .. } => emit_constant(&mut sv, *id, value, &target),
84 ScOp::Encode { id, .. } => {
85 sv.push_str(&format!(" wire v{};\n", id.0));
86 }
87 ScOp::BitwiseAnd { id, .. } => {
88 sv.push_str(&format!(" wire v{};\n", id.0));
89 }
90 ScOp::Popcount { id, .. } => {
91 sv.push_str(&format!(" logic [63:0] v{};\n", id.0));
92 }
93 ScOp::LifStep { id, params, .. } => {
94 sv.push_str(&format!(
95 " wire v{}_spike;\n wire signed [{}:0] v{}_v_out;\n",
96 id.0,
97 params.data_width - 1,
98 id.0
99 ));
100 }
101 ScOp::DenseForward { id, params, .. } => {
102 sv.push_str(&format!(
103 " wire [{}:0] v{}_spikes;\n wire v{}_running;\n wire v{}_done;\n",
104 params.n_neurons - 1,
105 id.0,
106 id.0,
107 id.0
108 ));
109 }
110 ScOp::DclsLayer { id, params, .. } => {
111 sv.push_str(&format!(
112 " wire signed [{}:0] v{};\n\
113 \x20 wire signed [31:0] v{}_accumulator_q16_16;\n\
114 \x20 wire v{}_valid;\n\
115 \x20 wire v{}_overflow;\n\
116 \x20 wire v{}_invalid_sigma;\n",
117 params.data_width - 1,
118 id.0,
119 id.0,
120 id.0,
121 id.0,
122 id.0
123 ));
124 }
125 ScOp::BitwiseXor { id, .. } => {
126 sv.push_str(&format!(" wire v{};\n", id.0));
127 }
128 ScOp::Reduce { id, .. } => {
129 sv.push_str(&format!(" wire [63:0] v{};\n", id.0));
130 }
131 ScOp::GraphForward {
132 id,
133 n_nodes,
134 n_features,
135 ..
136 } => {
137 let width = n_nodes * n_features * GRAPH_DATA_WIDTH;
138 sv.push_str(&format!(
139 " wire signed [{}:0] v{};\n",
140 width.saturating_sub(1),
141 id.0
142 ));
143 }
144 ScOp::SoftmaxAttention { id, q, k, v, dim_k } => {
145 let width = softmax_attention_shape(graph, *q, *k, *v, *dim_k)
146 .map(|(qr, _, vc)| qr * vc * ATTN_DATA_WIDTH)
147 .unwrap_or(64);
148 sv.push_str(&format!(" wire signed [{}:0] v{};\n", width - 1, id.0));
149 }
150 ScOp::KuramotoStep { id, phases, .. } => {
151 let width = kuramoto_osc_count(graph, *phases)
152 .map(|n| n * KURAMOTO_DATA_WIDTH)
153 .unwrap_or(64);
154 sv.push_str(&format!(" wire signed [{}:0] v{};\n", width - 1, id.0));
155 }
156 ScOp::Scale { id, .. } | ScOp::Offset { id, .. } | ScOp::DivConst { id, .. } => {
157 sv.push_str(&format!(" wire [63:0] v{};\n", id.0));
158 }
159 }
160 }
161 sv.push('\n');
162
163 let mut inst_idx = 0_u32;
164
165 for op in &graph.ops {
167 match op {
168 ScOp::Encode { id, prob, seed, .. } => {
169 let prob_wire = value_to_wire(graph, *prob);
170 sv.push_str(&format!(
171 " sc_bitstream_encoder #(\n\
172 \x20 .DATA_WIDTH(16),\n\
173 \x20 .SEED_INIT(16'h{:04X})\n\
174 \x20 ) u_enc_{} (\n\
175 \x20 .clk(clk),\n\
176 \x20 .rst_n(rst_n),\n\
177 \x20 .x_value({}),\n\
178 \x20 .t_index(32'd0),\n\
179 \x20 .bit_out(v{})\n\
180 \x20 );\n\n",
181 seed, inst_idx, prob_wire, id.0
182 ));
183 inst_idx += 1;
184 }
185 ScOp::BitwiseAnd { id, lhs, rhs } => {
186 let lhs_wire = value_to_wire(graph, *lhs);
187 let rhs_wire = value_to_wire(graph, *rhs);
188 sv.push_str(&format!(
189 " sc_bitstream_synapse u_syn_{} (\n\
190 \x20 .pre_bit({}),\n\
191 \x20 .w_bit({}),\n\
192 \x20 .post_bit(v{})\n\
193 \x20 );\n\n",
194 inst_idx, lhs_wire, rhs_wire, id.0
195 ));
196 inst_idx += 1;
197 }
198 ScOp::LifStep {
199 id,
200 current,
201 leak,
202 gain,
203 noise,
204 params,
205 } => {
206 let current_wire = value_to_wire(graph, *current);
207 let leak_wire = value_to_wire(graph, *leak);
208 let gain_wire = value_to_wire(graph, *gain);
209 let noise_wire = value_to_wire(graph, *noise);
210 emit_target_dsp_attribute(&mut sv, &target);
211 sv.push_str(&format!(
212 " sc_lif_neuron #(\n\
213 \x20 .DATA_WIDTH({}),\n\
214 \x20 .FRACTION({}),\n\
215 \x20 .V_REST({}),\n\
216 \x20 .V_RESET({}),\n\
217 \x20 .V_THRESHOLD({}),\n\
218 \x20 .REFRACTORY_PERIOD({})\n\
219 \x20 ) u_lif_{} (\n\
220 \x20 .clk(clk),\n\
221 \x20 .rst_n(rst_n),\n\
222 \x20 .leak_k({}),\n\
223 \x20 .gain_k({}),\n\
224 \x20 .I_t({}),\n\
225 \x20 .noise_in({}),\n\
226 \x20 .spike_out(v{}_spike),\n\
227 \x20 .v_out(v{}_v_out)\n\
228 \x20 );\n\n",
229 params.data_width,
230 params.fraction,
231 params.v_rest,
232 params.v_reset,
233 params.v_threshold,
234 params.refractory_period,
235 inst_idx,
236 leak_wire,
237 gain_wire,
238 current_wire,
239 noise_wire,
240 id.0,
241 id.0
242 ));
243 inst_idx += 1;
244 }
245 ScOp::DenseForward {
246 id,
247 inputs,
248 weights,
249 leak,
250 gain,
251 params,
252 } => {
253 let inputs_wire = value_to_wire(graph, *inputs);
254 let weights_wire = value_to_wire(graph, *weights);
255 let leak_wire = value_to_wire(graph, *leak);
256 let gain_wire = value_to_wire(graph, *gain);
257 emit_dense_fold_plan_comment(&mut sv, &target, params);
258 emit_target_dsp_attribute(&mut sv, &target);
259 sv.push_str(&format!(
260 " sc_dense_layer_core #(\n\
261 \x20 .N_INPUTS({}),\n\
262 \x20 .N_NEURONS({}),\n\
263 \x20 .DATA_WIDTH({})\n\
264 \x20 ) u_dense_{} (\n\
265 \x20 .clk(clk),\n\
266 \x20 .rst_n(rst_n),\n\
267 \x20 .start_pulse(1'b1),\n\
268 \x20 .stream_len(32'd{}),\n\
269 \x20 .x_input_fp({}),\n\
270 \x20 .weight_fp({}),\n\
271 \x20 .y_min_fp(16'd0),\n\
272 \x20 .y_max_fp(16'd256),\n\
273 \x20 .cfg_leak({}),\n\
274 \x20 .cfg_gain({}),\n\
275 \x20 .I_t(),\n\
276 \x20 .spikes(v{}_spikes),\n\
277 \x20 .step_valid(),\n\
278 \x20 .run_done(v{}_done),\n\
279 \x20 .running(v{}_running)\n\
280 \x20 );\n\n",
281 params.n_inputs,
282 params.n_neurons,
283 params.data_width,
284 inst_idx,
285 params.stream_length,
286 inputs_wire,
287 weights_wire,
288 leak_wire,
289 gain_wire,
290 id.0,
291 id.0,
292 id.0
293 ));
294 inst_idx += 1;
295 }
296 ScOp::DclsLayer {
297 id,
298 spike,
299 weights,
300 centre,
301 sigma,
302 params,
303 } => {
304 if params.tap_offsets.len() != params.n_taps {
305 return Err(format!(
306 "DclsLayer (v{}) expected {} tap offsets, got {}",
307 id.0,
308 params.n_taps,
309 params.tap_offsets.len()
310 ));
311 }
312 let spike_wire = value_to_wire(graph, *spike);
313 let weights_wire = value_to_wire(graph, *weights);
314 let centre_wire = value_to_wire(graph, *centre);
315 let sigma_wire = value_to_wire(graph, *sigma);
316 let tap_offsets = emit_concat_u32(¶ms.tap_offsets, params.ptr_width)?;
317 emit_target_dsp_attribute(&mut sv, &target);
318 sv.push_str(&format!(
319 " sc_dcls_layer_core #(\n\
320 \x20 .N_TAPS({}),\n\
321 \x20 .DATA_WIDTH({}),\n\
322 \x20 .FRACTION({}),\n\
323 \x20 .DELAY_DEPTH({}),\n\
324 \x20 .PTR_WIDTH({})\n\
325 \x20 ) u_dcls_{} (\n\
326 \x20 .clk(clk),\n\
327 \x20 .rst_n(rst_n),\n\
328 \x20 .in_valid(1'b1),\n\
329 \x20 .spike_in({}),\n\
330 \x20 .tap_offsets({}),\n\
331 \x20 .tap_weights_q88({}),\n\
332 \x20 .centre_q88({}),\n\
333 \x20 .sigma_q88({}),\n\
334 \x20 .out_valid(v{}_valid),\n\
335 \x20 .weighted_sum_q88(v{}),\n\
336 \x20 .accumulator_q16_16(v{}_accumulator_q16_16),\n\
337 \x20 .overflow(v{}_overflow),\n\
338 \x20 .invalid_sigma(v{}_invalid_sigma)\n\
339 \x20 );\n\n",
340 params.n_taps,
341 params.data_width,
342 params.fraction,
343 params.delay_depth,
344 params.ptr_width,
345 inst_idx,
346 spike_wire,
347 tap_offsets,
348 weights_wire,
349 centre_wire,
350 sigma_wire,
351 id.0,
352 id.0,
353 id.0,
354 id.0,
355 id.0
356 ));
357 inst_idx += 1;
358 }
359 ScOp::BitwiseXor { id, lhs, rhs } => {
360 let lhs_wire = value_to_wire(graph, *lhs);
361 let rhs_wire = value_to_wire(graph, *rhs);
362 sv.push_str(&format!(
363 " assign v{} = {} ^ {};\n",
364 id.0, lhs_wire, rhs_wire
365 ));
366 }
367 ScOp::Reduce { id, input, mode } => {
368 let in_wire = value_to_wire(graph, *input);
369 let label = match mode {
370 ReduceMode::Sum => "reduce_sum",
371 ReduceMode::Max => "reduce_max",
372 };
373 sv.push_str(&format!(
374 " // {label}: passthrough for single-element; multi-element requires adder/comparator tree\n\
375 \x20 assign v{id} = {wire};\n",
376 label = label,
377 id = id.0,
378 wire = in_wire,
379 ));
380 }
381 ScOp::GraphForward {
382 id,
383 features,
384 adjacency,
385 n_nodes,
386 n_features,
387 } => {
388 emit_graph_forward(
389 &mut sv,
390 graph,
391 inst_idx,
392 *id,
393 *features,
394 *adjacency,
395 *n_nodes,
396 *n_features,
397 )?;
398 inst_idx += 1;
399 }
400 ScOp::SoftmaxAttention { id, q, k, v, dim_k } => {
401 emit_softmax_attention(&mut sv, graph, inst_idx, *id, *q, *k, *v, *dim_k)?;
402 inst_idx += 1;
403 }
404 ScOp::KuramotoStep {
405 id,
406 phases,
407 omega,
408 coupling,
409 dt,
410 } => {
411 emit_kuramoto_step(
412 &mut sv, graph, inst_idx, *id, *phases, *omega, *coupling, *dt,
413 )?;
414 inst_idx += 1;
415 }
416 ScOp::Output { name, source, .. } => {
417 let src_wire = value_to_wire(graph, *source);
418 sv.push_str(&format!(" assign {} = {};\n", name, src_wire));
419 }
420 ScOp::Scale { id, input, factor } => {
421 let in_wire = value_to_wire(graph, *input);
422 let scale_int = (*factor * 256.0) as i64; sv.push_str(&format!(
424 " assign v{} = ({} * {}) >>> 8;\n",
425 id.0, in_wire, scale_int
426 ));
427 }
428 ScOp::Offset { id, input, offset } => {
429 let in_wire = value_to_wire(graph, *input);
430 let offset_int = (*offset * 256.0) as i64;
431 sv.push_str(&format!(
432 " assign v{} = {} + {};\n",
433 id.0, in_wire, offset_int
434 ));
435 }
436 ScOp::DivConst { id, input, divisor } => {
437 let in_wire = value_to_wire(graph, *input);
438 sv.push_str(&format!(
439 " assign v{} = {} / {};\n",
440 id.0, in_wire, divisor
441 ));
442 }
443 ScOp::Popcount { id, input } => {
444 let in_wire = value_to_wire(graph, *input);
445 sv.push_str(&format!(
446 " // Combinatorial popcount for v{id}\n\
447 \x20 always_comb begin\n\
448 \x20 v{id} = 64'd0;\n\
449 \x20 for (integer _pc_i = 0; _pc_i < 64; _pc_i = _pc_i + 1)\n\
450 \x20 v{id} = v{id} + {{63'd0, {wire}[_pc_i]}};\n\
451 \x20 end\n\n",
452 id = id.0,
453 wire = in_wire,
454 ));
455 }
456 _ => {}
457 }
458 }
459
460 sv.push_str("\nendmodule\n");
461 let report = target.estimate_graph(graph);
462 Ok((sv, report))
463}
464
465fn type_to_width(ty: &ScType) -> usize {
466 ty.bit_width()
467}
468
469fn find_value_width(graph: &ScGraph, id: ValueId) -> usize {
470 for op in &graph.ops {
471 if op.result_id() == id {
472 return match op {
473 ScOp::Input { ty, .. } => type_to_width(ty),
474 ScOp::Constant { ty, .. } => type_to_width(ty),
475 ScOp::Encode { .. } | ScOp::BitwiseAnd { .. } | ScOp::BitwiseXor { .. } => 1,
476 ScOp::Popcount { .. } | ScOp::Reduce { .. } => 64,
477 ScOp::LifStep { params, .. } => params.data_width as usize,
478 ScOp::DenseForward { params, .. } => params.n_neurons,
479 ScOp::DclsLayer { params, .. } => params.data_width as usize,
480 ScOp::GraphForward {
481 n_nodes,
482 n_features,
483 ..
484 } => n_nodes * n_features * GRAPH_DATA_WIDTH,
485 ScOp::KuramotoStep { phases, .. } => kuramoto_osc_count(graph, *phases)
486 .map(|n| n * KURAMOTO_DATA_WIDTH)
487 .unwrap_or(64),
488 ScOp::SoftmaxAttention { q, k, v, dim_k, .. } => {
489 softmax_attention_shape(graph, *q, *k, *v, *dim_k)
490 .map(|(qr, _, vc)| qr * vc * ATTN_DATA_WIDTH)
491 .unwrap_or(64)
492 }
493 ScOp::Scale { .. } | ScOp::Offset { .. } | ScOp::DivConst { .. } => 64,
494 ScOp::Output { source, .. } => find_value_width(graph, *source),
495 };
496 }
497 }
498 16
499}
500
501fn value_to_wire(graph: &ScGraph, id: ValueId) -> String {
502 for op in &graph.ops {
503 if op.result_id() == id {
504 return match op {
505 ScOp::Input { name, .. } => name.clone(),
506 ScOp::Constant { id, .. } => format!("c{}", id.0),
507 ScOp::LifStep { id, .. } => format!("v{}_spike", id.0),
508 ScOp::DenseForward { id, .. } => format!("v{}_spikes", id.0),
509 _ => format!("v{}", id.0),
510 };
511 }
512 }
513 format!("v{}", id.0)
514}
515
516fn emit_concat_u32(values: &[u32], width: u32) -> Result<String, String> {
517 if width == 0 {
518 return Err("packed unsigned concatenation width must be positive".to_string());
519 }
520 let max_value = if width >= 32 {
521 u32::MAX
522 } else {
523 (1_u32 << width) - 1
524 };
525 let mut fields = Vec::with_capacity(values.len());
526 for value in values.iter().rev() {
527 if *value > max_value {
528 return Err(format!(
529 "packed unsigned value {} exceeds {}-bit field",
530 value, width
531 ));
532 }
533 fields.push(format!("{}'d{}", width, value));
534 }
535 Ok(format!("{{{}}}", fields.join(", ")))
536}
537
538const KURAMOTO_DATA_WIDTH: usize = 24;
541const KURAMOTO_FRACTION: usize = 16;
542const KURAMOTO_LUT_SIZE: usize = 64;
543
544const GRAPH_DATA_WIDTH: usize = 24;
547const GRAPH_FRACTION: usize = 16;
548
549const ATTN_DATA_WIDTH: usize = 24;
553const ATTN_FRACTION: usize = 16;
554
555fn q_fixed(value: f64, frac: usize, width: usize) -> Option<i64> {
558 if !value.is_finite() {
559 return None;
560 }
561 let scaled = (value * (1i64 << frac) as f64).round() as i64;
562 let min = -(1i64 << (width - 1));
563 let max = (1i64 << (width - 1)) - 1;
564 (min..=max).contains(&scaled).then_some(scaled)
565}
566
567fn kuramoto_phase_modulus() -> i64 {
569 (std::f64::consts::TAU * (1i64 << KURAMOTO_FRACTION) as f64).round() as i64
570}
571
572fn kuramoto_half_phase_modulus() -> i64 {
574 (std::f64::consts::PI * (1i64 << KURAMOTO_FRACTION) as f64).round() as i64
575}
576
577fn kuramoto_fixed(value: f64, name: &str, id: ValueId) -> Result<i64, String> {
579 q_fixed(value, KURAMOTO_FRACTION, KURAMOTO_DATA_WIDTH).ok_or_else(|| {
580 format!(
581 "KuramotoStep (v{}) {} value {} is not representable in signed Q8.16 (24-bit)",
582 id.0, name, value
583 )
584 })
585}
586
587fn graph_fixed(value: f64, name: &str, id: ValueId) -> Result<i64, String> {
589 q_fixed(value, GRAPH_FRACTION, GRAPH_DATA_WIDTH).ok_or_else(|| {
590 format!(
591 "GraphForward (v{}) {} value {} is not representable in signed Q8.16 (24-bit)",
592 id.0, name, value
593 )
594 })
595}
596
597fn attn_fixed(value: f64, name: &str, id: ValueId) -> Result<i64, String> {
599 q_fixed(value, ATTN_FRACTION, ATTN_DATA_WIDTH).ok_or_else(|| {
600 format!(
601 "SoftmaxAttention (v{}) {} value {} is not representable in signed Q8.16 (24-bit)",
602 id.0, name, value
603 )
604 })
605}
606
607fn pack_q_bus(values: &[i64], width: usize) -> String {
610 let mask = (1i64 << width) - 1;
611 let fields: Vec<String> = values
612 .iter()
613 .rev()
614 .map(|v| format!("{}'d{}", width, v & mask))
615 .collect();
616 format!("{{{}}}", fields.join(", "))
617}
618
619fn const_f64_vec(graph: &ScGraph, id: ValueId) -> Option<Vec<f64>> {
621 for op in &graph.ops {
622 if op.result_id() == id {
623 return match op {
624 ScOp::Constant {
625 value: ScConst::F64Vec(v),
626 ..
627 } => Some(v.clone()),
628 ScOp::Constant {
629 value: ScConst::I64Vec(v),
630 ..
631 } => Some(v.iter().map(|x| *x as f64).collect()),
632 _ => None,
633 };
634 }
635 }
636 None
637}
638
639fn kuramoto_osc_count(graph: &ScGraph, phases: ValueId) -> Option<usize> {
641 for op in &graph.ops {
642 if op.result_id() == phases {
643 return match op {
644 ScOp::Constant {
645 value: ScConst::F64Vec(v),
646 ..
647 } => Some(v.len()),
648 ScOp::Constant {
649 value: ScConst::I64Vec(v),
650 ..
651 } => Some(v.len()),
652 ScOp::Input {
653 ty: ScType::Vec { count, .. },
654 ..
655 }
656 | ScOp::Constant {
657 ty: ScType::Vec { count, .. },
658 ..
659 } => Some(*count),
660 _ => None,
661 };
662 }
663 }
664 None
665}
666
667#[allow(clippy::too_many_arguments)]
674fn emit_kuramoto_step(
675 sv: &mut String,
676 graph: &ScGraph,
677 inst_idx: u32,
678 id: ValueId,
679 phases: ValueId,
680 omega: ValueId,
681 coupling: ValueId,
682 dt: f64,
683) -> Result<(), String> {
684 let phase_vals = const_f64_vec(graph, phases)
685 .ok_or_else(|| format!("KuramotoStep (v{}) requires constant phase values", id.0))?;
686 let n = phase_vals.len();
687 if n == 0 {
688 return Err(format!(
689 "KuramotoStep (v{}) needs at least one oscillator",
690 id.0
691 ));
692 }
693 let omega_vals = const_f64_vec(graph, omega)
694 .ok_or_else(|| format!("KuramotoStep (v{}) requires constant omega values", id.0))?;
695 if omega_vals.len() != n {
696 return Err(format!(
697 "KuramotoStep (v{}) omega length {} does not match {} oscillators",
698 id.0,
699 omega_vals.len(),
700 n
701 ));
702 }
703 let coupling_vals = const_f64_vec(graph, coupling).ok_or_else(|| {
704 format!(
705 "KuramotoStep (v{}) requires a constant coupling matrix",
706 id.0
707 )
708 })?;
709 if coupling_vals.len() != n * n {
710 return Err(format!(
711 "KuramotoStep (v{}) coupling length {} is not {n}×{n}",
712 id.0,
713 coupling_vals.len()
714 ));
715 }
716
717 let two_pi = std::f64::consts::TAU;
718 let phase_fixed = phase_vals
719 .iter()
720 .map(|theta| kuramoto_fixed(theta.rem_euclid(two_pi), "phase", id))
721 .collect::<Result<Vec<_>, _>>()?;
722 let omega_fixed = omega_vals
723 .iter()
724 .map(|w| kuramoto_fixed(*w, "omega", id))
725 .collect::<Result<Vec<_>, _>>()?;
726 let coupling_fixed = coupling_vals
727 .iter()
728 .map(|k| kuramoto_fixed(*k, "coupling", id))
729 .collect::<Result<Vec<_>, _>>()?;
730 let dt_fixed = kuramoto_fixed(dt, "dt", id)?;
731
732 let dw = KURAMOTO_DATA_WIDTH;
733 sv.push_str(&format!(
734 " sc_kuramoto_step #(\n\
735 \x20 .N_OSC({n}),\n\
736 \x20 .DATA_WIDTH({dw}),\n\
737 \x20 .FRACTION({frac}),\n\
738 \x20 .LUT_SIZE({lut}),\n\
739 \x20 .DT_FIXED({dw}'sd{dt_fixed}),\n\
740 \x20 .PHASE_MODULUS({dw}'sd{modulus}),\n\
741 \x20 .HALF_PHASE_MODULUS({dw}'sd{half})\n\
742 \x20 ) u_kuramoto_{inst_idx} (\n\
743 \x20 .phases_in({phases_bus}),\n\
744 \x20 .omega({omega_bus}),\n\
745 \x20 .coupling({coupling_bus}),\n\
746 \x20 .phases_out(v{result})\n\
747 \x20 );\n\n",
748 frac = KURAMOTO_FRACTION,
749 lut = KURAMOTO_LUT_SIZE,
750 modulus = kuramoto_phase_modulus(),
751 half = kuramoto_half_phase_modulus(),
752 phases_bus = pack_q_bus(&phase_fixed, KURAMOTO_DATA_WIDTH),
753 omega_bus = pack_q_bus(&omega_fixed, KURAMOTO_DATA_WIDTH),
754 coupling_bus = pack_q_bus(&coupling_fixed, KURAMOTO_DATA_WIDTH),
755 result = id.0,
756 ));
757 Ok(())
758}
759
760fn emit_graph_forward(
767 sv: &mut String,
768 graph: &ScGraph,
769 inst_idx: u32,
770 id: ValueId,
771 features: ValueId,
772 adjacency: ValueId,
773 n_nodes: usize,
774 n_features: usize,
775) -> Result<(), String> {
776 if n_nodes == 0 || n_features == 0 {
777 return Err(format!(
778 "GraphForward (v{}) needs at least one node and one feature",
779 id.0
780 ));
781 }
782 let feat_vals = const_f64_vec(graph, features)
783 .ok_or_else(|| format!("GraphForward (v{}) requires constant feature values", id.0))?;
784 if feat_vals.len() != n_nodes * n_features {
785 return Err(format!(
786 "GraphForward (v{}) feature length {} is not {n_nodes}×{n_features}",
787 id.0,
788 feat_vals.len()
789 ));
790 }
791 let adj_vals = const_f64_vec(graph, adjacency).ok_or_else(|| {
792 format!(
793 "GraphForward (v{}) requires a constant adjacency matrix",
794 id.0
795 )
796 })?;
797 if adj_vals.len() != n_nodes * n_nodes {
798 return Err(format!(
799 "GraphForward (v{}) adjacency length {} is not {n_nodes}×{n_nodes}",
800 id.0,
801 adj_vals.len()
802 ));
803 }
804
805 let feat_fixed = feat_vals
806 .iter()
807 .map(|x| graph_fixed(*x, "feature", id))
808 .collect::<Result<Vec<_>, _>>()?;
809 let adj_fixed = adj_vals
810 .iter()
811 .map(|x| graph_fixed(*x, "adjacency", id))
812 .collect::<Result<Vec<_>, _>>()?;
813
814 sv.push_str(&format!(
815 " sc_graph_forward #(\n\
816 \x20 .N_NODES({n_nodes}),\n\
817 \x20 .N_FEATURES({n_features}),\n\
818 \x20 .DATA_WIDTH({dw}),\n\
819 \x20 .FRACTION({frac})\n\
820 \x20 ) u_graph_{inst_idx} (\n\
821 \x20 .features({feat_bus}),\n\
822 \x20 .adjacency({adj_bus}),\n\
823 \x20 .agg(v{result})\n\
824 \x20 );\n\n",
825 dw = GRAPH_DATA_WIDTH,
826 frac = GRAPH_FRACTION,
827 feat_bus = pack_q_bus(&feat_fixed, GRAPH_DATA_WIDTH),
828 adj_bus = pack_q_bus(&adj_fixed, GRAPH_DATA_WIDTH),
829 result = id.0,
830 ));
831 Ok(())
832}
833
834fn softmax_attention_shape(
839 graph: &ScGraph,
840 q: ValueId,
841 k: ValueId,
842 v: ValueId,
843 dim_k: usize,
844) -> Option<(usize, usize, usize)> {
845 if dim_k == 0 {
846 return None;
847 }
848 let q_vals = const_f64_vec(graph, q)?;
849 let k_vals = const_f64_vec(graph, k)?;
850 let v_vals = const_f64_vec(graph, v)?;
851 if q_vals.len() % dim_k != 0 || k_vals.len() % dim_k != 0 {
852 return None;
853 }
854 let q_rows = q_vals.len() / dim_k;
855 let k_rows = k_vals.len() / dim_k;
856 if k_rows == 0 || v_vals.len() % k_rows != 0 {
857 return None;
858 }
859 let v_cols = v_vals.len() / k_rows;
860 if q_rows == 0 || v_cols == 0 {
861 return None;
862 }
863 Some((q_rows, k_rows, v_cols))
864}
865
866fn emit_softmax_attention(
873 sv: &mut String,
874 graph: &ScGraph,
875 inst_idx: u32,
876 id: ValueId,
877 q: ValueId,
878 k: ValueId,
879 v: ValueId,
880 dim_k: usize,
881) -> Result<(), String> {
882 if dim_k == 0 {
883 return Err(format!(
884 "SoftmaxAttention (v{}) needs a positive dim_k",
885 id.0
886 ));
887 }
888 let q_vals = const_f64_vec(graph, q).ok_or_else(|| {
889 format!(
890 "SoftmaxAttention (v{}) requires constant query values",
891 id.0
892 )
893 })?;
894 let k_vals = const_f64_vec(graph, k)
895 .ok_or_else(|| format!("SoftmaxAttention (v{}) requires constant key values", id.0))?;
896 let v_vals = const_f64_vec(graph, v).ok_or_else(|| {
897 format!(
898 "SoftmaxAttention (v{}) requires constant value values",
899 id.0
900 )
901 })?;
902 if q_vals.len() % dim_k != 0 {
903 return Err(format!(
904 "SoftmaxAttention (v{}) query length {} is not a multiple of dim_k {dim_k}",
905 id.0,
906 q_vals.len()
907 ));
908 }
909 if k_vals.len() % dim_k != 0 {
910 return Err(format!(
911 "SoftmaxAttention (v{}) key length {} is not a multiple of dim_k {dim_k}",
912 id.0,
913 k_vals.len()
914 ));
915 }
916 let q_rows = q_vals.len() / dim_k;
917 let k_rows = k_vals.len() / dim_k;
918 if k_rows == 0 {
919 return Err(format!(
920 "SoftmaxAttention (v{}) needs at least one key row",
921 id.0
922 ));
923 }
924 if v_vals.len() % k_rows != 0 {
925 return Err(format!(
926 "SoftmaxAttention (v{}) value length {} is not {k_rows} rows",
927 id.0,
928 v_vals.len()
929 ));
930 }
931 let v_cols = v_vals.len() / k_rows;
932 if q_rows == 0 || v_cols == 0 {
933 return Err(format!(
934 "SoftmaxAttention (v{}) needs at least one query row and value column",
935 id.0
936 ));
937 }
938
939 let inv_temp = 1.0 / (dim_k as f64).sqrt();
940 let q_fixed = q_vals
941 .iter()
942 .map(|x| attn_fixed(*x, "query", id))
943 .collect::<Result<Vec<_>, _>>()?;
944 let k_fixed = k_vals
945 .iter()
946 .map(|x| attn_fixed(*x, "key", id))
947 .collect::<Result<Vec<_>, _>>()?;
948 let v_fixed = v_vals
949 .iter()
950 .map(|x| attn_fixed(*x, "value", id))
951 .collect::<Result<Vec<_>, _>>()?;
952 let inv_temp_fixed = attn_fixed(inv_temp, "inv_temp", id)?;
953
954 let exp_shift = ATTN_FRACTION - 3;
956 let exp_min_abs = (16.0 * (1i64 << ATTN_FRACTION) as f64).round() as i64;
957
958 sv.push_str(&format!(
959 " sc_softmax_attention #(\n\
960 \x20 .Q_ROWS({q_rows}),\n\
961 \x20 .K_ROWS({k_rows}),\n\
962 \x20 .DIM_K({dim_k}),\n\
963 \x20 .V_COLS({v_cols}),\n\
964 \x20 .DATA_WIDTH({dw}),\n\
965 \x20 .FRACTION({frac}),\n\
966 \x20 .INV_TEMP({inv_temp_lit}),\n\
967 \x20 .EXP_SHIFT({exp_shift}),\n\
968 \x20 .EXP_MIN_ABS({exp_min_abs})\n\
969 \x20 ) u_softmax_{inst_idx} (\n\
970 \x20 .q_in({q_bus}),\n\
971 \x20 .k_in({k_bus}),\n\
972 \x20 .v_in({v_bus}),\n\
973 \x20 .attn_out(v{result})\n\
974 \x20 );\n\n",
975 dw = ATTN_DATA_WIDTH,
976 frac = ATTN_FRACTION,
977 inv_temp_lit = signed_q_literal(inv_temp_fixed, ATTN_DATA_WIDTH),
978 q_bus = pack_q_bus(&q_fixed, ATTN_DATA_WIDTH),
979 k_bus = pack_q_bus(&k_fixed, ATTN_DATA_WIDTH),
980 v_bus = pack_q_bus(&v_fixed, ATTN_DATA_WIDTH),
981 result = id.0,
982 ));
983 Ok(())
984}
985
986fn emit_target_dsp_attribute(sv: &mut String, target: &SvTarget) {
987 if let Some(attribute) = target.dsp_attribute() {
988 sv.push_str(" ");
989 sv.push_str(attribute);
990 sv.push('\n');
991 }
992}
993
994fn emit_dense_fold_plan_comment(sv: &mut String, target: &SvTarget, params: &DenseParams) {
995 let Some(plan) = target.dense_fold_plan(params.n_inputs, params.n_neurons) else {
996 return;
997 };
998 if !plan.fold_required {
999 return;
1000 }
1001 sv.push_str(&format!(
1002 " // Dense fold plan: unfurled_macs={}, dsp_budget={}, dsp_per_cycle={}, output_parallelism={}, input_parallelism={}, compute_cycles={}\n",
1003 plan.mac_count,
1004 plan.dsp_budget,
1005 plan.dsp_per_cycle,
1006 plan.output_parallelism,
1007 plan.input_parallelism,
1008 plan.compute_cycles
1009 ));
1010}
1011
1012fn emit_ram_style_attribute(sv: &mut String, target: &SvTarget, bits: u64) {
1013 if let Some(style) = target.ram_style_for_bits(bits) {
1014 sv.push_str(&format!(" (* ram_style = \"{}\" *)\n", style));
1015 }
1016}
1017
1018fn signed_q_literal(value: i64, width: usize) -> String {
1023 if value < 0 {
1024 format!("-{}'sd{}", width, value.unsigned_abs())
1025 } else {
1026 format!("{}'sd{}", width, value)
1027 }
1028}
1029
1030fn emit_constant(sv: &mut String, id: ValueId, value: &ScConst, target: &SvTarget) {
1031 match value {
1032 ScConst::F64(v) => {
1033 let fp = (*v * 256.0) as i64; sv.push_str(&format!(
1035 " localparam signed [15:0] c{} = {};\n",
1036 id.0,
1037 signed_q_literal(fp, 16)
1038 ));
1039 }
1040 ScConst::I64(v) => {
1041 sv.push_str(&format!(
1042 " localparam signed [15:0] c{} = {};\n",
1043 id.0,
1044 signed_q_literal(*v, 16)
1045 ));
1046 }
1047 ScConst::U64(v) => {
1048 sv.push_str(&format!(" localparam [31:0] c{} = 32'd{};\n", id.0, v));
1049 }
1050 ScConst::F64Vec(vec) => {
1051 let width = vec.len().saturating_mul(16);
1052 if width == 0 {
1053 sv.push_str(&format!(" wire [0:0] c{};\n", id.0));
1054 return;
1055 }
1056 emit_ram_style_attribute(sv, target, width as u64);
1057 sv.push_str(&format!(" wire [{}:0] c{};\n", width - 1, id.0));
1058 for (i, v) in vec.iter().enumerate() {
1059 let fp = (*v * 256.0) as i64;
1060 sv.push_str(&format!(
1061 " assign c{}[{} +: 16] = {};\n",
1062 id.0,
1063 i * 16,
1064 signed_q_literal(fp, 16)
1065 ));
1066 }
1067 }
1068 ScConst::I64Vec(vec) => {
1069 let width = vec.len().saturating_mul(16);
1070 if width == 0 {
1071 sv.push_str(&format!(" wire [0:0] c{};\n", id.0));
1072 return;
1073 }
1074 emit_ram_style_attribute(sv, target, width as u64);
1075 sv.push_str(&format!(" wire [{}:0] c{};\n", width - 1, id.0));
1076 for (i, v) in vec.iter().enumerate() {
1077 sv.push_str(&format!(
1078 " assign c{}[{} +: 16] = {};\n",
1079 id.0,
1080 i * 16,
1081 signed_q_literal(*v, 16)
1082 ));
1083 }
1084 }
1085 }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use super::*;
1091 use crate::ir::builder::ScGraphBuilder;
1092 use crate::ir::sv_target::{SkuKind, SvTarget};
1093
1094 #[test]
1095 fn dcls_layer_emits_core_with_q88_contract_ports() {
1096 let mut builder = ScGraphBuilder::new("dcls_contract");
1097 let spike = builder.input("spike_in", ScType::Bool);
1098 let weights = builder.constant(
1099 ScConst::I64Vec(vec![256, 128, -64]),
1100 ScType::Vec {
1101 element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
1102 count: 3,
1103 },
1104 );
1105 let centre = builder.constant(ScConst::I64(256), ScType::FixedPoint { width: 16, frac: 8 });
1106 let sigma = builder.constant(ScConst::I64(512), ScType::FixedPoint { width: 16, frac: 8 });
1107 let result = builder.dcls_layer(
1108 spike,
1109 weights,
1110 centre,
1111 sigma,
1112 DclsParams {
1113 n_taps: 3,
1114 data_width: 16,
1115 fraction: 8,
1116 delay_depth: 31,
1117 ptr_width: 5,
1118 tap_offsets: vec![0, 1, 2],
1119 },
1120 );
1121 builder.output("weighted_sum", result);
1122
1123 let sv = emit(&builder.build()).expect("DCLS layer should emit synthesizable RTL");
1124 assert!(sv.contains("sc_dcls_layer_core"));
1125 assert!(sv.contains(".tap_offsets({5'd2, 5'd1, 5'd0})"));
1126 assert!(sv.contains(".accumulator_q16_16(v4_accumulator_q16_16)"));
1127 assert!(sv.contains(".overflow(v4_overflow)"));
1128 assert!(sv.contains(".invalid_sigma(v4_invalid_sigma)"));
1129 assert!(sv.contains("assign weighted_sum = v4;"));
1130 }
1131
1132 #[test]
1133 fn ultrascale_plus_target_emits_dsp48e2_metadata_and_resource_report() {
1134 let mut builder = ScGraphBuilder::new("ultrascale_dense");
1135 let inputs = builder.input(
1136 "inputs",
1137 ScType::Vec {
1138 element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
1139 count: 4,
1140 },
1141 );
1142 let weights = builder.constant(
1143 ScConst::I64Vec(vec![128; 12]),
1144 ScType::Vec {
1145 element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
1146 count: 12,
1147 },
1148 );
1149 let leak = builder.constant(ScConst::I64(16), ScType::FixedPoint { width: 16, frac: 8 });
1150 let gain = builder.constant(ScConst::I64(1), ScType::FixedPoint { width: 16, frac: 8 });
1151 let result = builder.dense_forward(
1152 inputs,
1153 weights,
1154 leak,
1155 gain,
1156 DenseParams {
1157 n_inputs: 4,
1158 n_neurons: 3,
1159 ..DenseParams::default()
1160 },
1161 );
1162 builder.output("spikes", result);
1163
1164 let (sv, report) = emit_systemverilog_with_target(
1165 &builder.build(),
1166 SvTarget::zynq_ultrascale_plus(SkuKind::Zu3eg, 250),
1167 )
1168 .expect("UltraScale+ target emission should succeed");
1169
1170 assert!(sv.contains("Target: Zynq UltraScale+ MPSoC ZU3EG"));
1171 assert!(sv.contains("sc_target_dsp = \"DSP48E2\""));
1172 assert!(sv.contains("(* ram_style = \"distributed\" *)"));
1173 assert_eq!(report.device_part, "xczu3eg-sbva484-1-e");
1174 assert!(report.dsp_estimated >= 12);
1175 assert!(report.fits_dsp_budget);
1176 }
1177
1178 #[test]
1179 fn ultrascale_plus_over_budget_dense_emits_fold_plan_comment() {
1180 let mut builder = ScGraphBuilder::new("ultrascale_fold_dense");
1181 let inputs = builder.input(
1182 "inputs",
1183 ScType::Vec {
1184 element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
1185 count: 64,
1186 },
1187 );
1188 let weights = builder.constant(
1189 ScConst::I64Vec(vec![128; 64 * 32]),
1190 ScType::Vec {
1191 element: Box::new(ScType::FixedPoint { width: 16, frac: 8 }),
1192 count: 64 * 32,
1193 },
1194 );
1195 let leak = builder.constant(ScConst::I64(16), ScType::FixedPoint { width: 16, frac: 8 });
1196 let gain = builder.constant(ScConst::I64(1), ScType::FixedPoint { width: 16, frac: 8 });
1197 let result = builder.dense_forward(
1198 inputs,
1199 weights,
1200 leak,
1201 gain,
1202 DenseParams {
1203 n_inputs: 64,
1204 n_neurons: 32,
1205 ..DenseParams::default()
1206 },
1207 );
1208 builder.output("spikes", result);
1209
1210 let (sv, report) = emit_systemverilog_with_target(
1211 &builder.build(),
1212 SvTarget::zynq_ultrascale_plus(SkuKind::Zu3eg, 250),
1213 )
1214 .expect("UltraScale+ target emission should produce fold-plan metadata");
1215
1216 assert!(sv.contains("Dense fold plan: unfurled_macs=2048"));
1217 assert!(sv.contains("dsp_per_cycle=320"));
1218 assert!(sv.contains("compute_cycles=7"));
1219 assert!(report.dense_fold_plan.is_some());
1220 }
1221}