Skip to main content

sc_neurocore_engine/ir/
emit_mlir.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 — MLIR CIRCT emitter for SC IR graphs
8
9//! MLIR CIRCT emitter for SC IR graphs.
10//!
11//! Produces CIRCT-compatible MLIR text using hw/comb/seq dialects.
12
13use crate::ir::graph::*;
14
15/// Emit CIRCT-compatible MLIR from an SC graph.
16pub fn emit(graph: &ScGraph) -> Result<String, String> {
17    let mut mlir = String::new();
18
19    mlir.push_str(&format!(
20        "// Auto-generated by SC-NeuroCore MLIR Emitter v3.11\n\
21         // Source graph: {}\n\n",
22        graph.name
23    ));
24
25    // Module header with ports
26    let mut ports = Vec::new();
27    ports.push("in %clk: i1".to_string());
28    ports.push("in %rst_n: i1".to_string());
29
30    for op in &graph.ops {
31        match op {
32            ScOp::Input { name, .. } => {
33                ports.push(format!("in %{name}: i1"));
34            }
35            ScOp::Output { name, .. } => {
36                ports.push(format!("out {name}: i1"));
37            }
38            _ => {}
39        }
40    }
41
42    mlir.push_str(&format!(
43        "hw.module @{}({}) {{\n",
44        graph.name,
45        ports.join(", ")
46    ));
47
48    let mut last_output = String::from("%clk");
49
50    for op in &graph.ops {
51        match op {
52            ScOp::Input { .. } => {}
53            ScOp::Output { name, source, .. } => {
54                let src = value_wire(graph, *source);
55                mlir.push_str(&format!("  hw.output {src} : i1\n"));
56                last_output = format!("%{name}");
57            }
58            ScOp::Encode {
59                id, prob, seed: _, ..
60            } => {
61                let prob_wire = value_wire(graph, *prob);
62                mlir.push_str(&format!(
63                    "  %v{} = hw.instance \"enc_{}\" @sc_bitstream_encoder(\
64                     clk: %clk: i1, rst_n: %rst_n: i1, x_value: {}: i1) -> (bit_out: i1)\n",
65                    id.0, id.0, prob_wire
66                ));
67                last_output = format!("%v{}", id.0);
68            }
69            ScOp::BitwiseAnd { id, lhs, rhs } => {
70                let l = value_wire(graph, *lhs);
71                let r = value_wire(graph, *rhs);
72                mlir.push_str(&format!("  %v{} = comb.and {l}, {r} : i1\n", id.0));
73                last_output = format!("%v{}", id.0);
74            }
75            ScOp::BitwiseXor { id, lhs, rhs } => {
76                let l = value_wire(graph, *lhs);
77                let r = value_wire(graph, *rhs);
78                mlir.push_str(&format!("  %v{} = comb.xor {l}, {r} : i1\n", id.0));
79                last_output = format!("%v{}", id.0);
80            }
81            ScOp::Constant { id, value, .. } => {
82                let val = match value {
83                    ScConst::I64(v) => format!("{v}"),
84                    ScConst::U64(v) => format!("{v}"),
85                    ScConst::F64(v) => format!("{}", (*v * 256.0) as i64),
86                    _ => "0".to_string(),
87                };
88                mlir.push_str(&format!("  %c{} = hw.constant {} : i16\n", id.0, val));
89            }
90            ScOp::LifStep {
91                id,
92                current,
93                params,
94                ..
95            } => {
96                let cur = value_wire(graph, *current);
97                mlir.push_str(&format!(
98                    "  %v{id}_spike, %v{id}_v = hw.instance \"lif_{id}\" \
99                     @sc_lif_neuron<DATA_WIDTH: i32 = {dw}, V_THRESHOLD: i32 = {vt}>(\
100                     clk: %clk: i1, rst_n: %rst_n: i1, I_t: {cur}: i1) -> \
101                     (spike_out: i1, v_out: i{dw})\n",
102                    id = id.0,
103                    dw = params.data_width,
104                    vt = params.v_threshold,
105                    cur = cur,
106                ));
107                last_output = format!("%v{}_spike", id.0);
108            }
109            ScOp::DenseForward { id, params, .. } => {
110                mlir.push_str(&format!(
111                    "  %v{} = hw.instance \"dense_{}\" @sc_dense_layer_core<\
112                     N_INPUTS: i32 = {}, N_NEURONS: i32 = {}>(\
113                     clk: %clk: i1, rst_n: %rst_n: i1) -> (spikes: i{})\n",
114                    id.0, id.0, params.n_inputs, params.n_neurons, params.n_neurons
115                ));
116                last_output = format!("%v{}", id.0);
117            }
118            ScOp::Popcount { id, input } => {
119                let inp = value_wire(graph, *input);
120                mlir.push_str(&format!("  %v{} = comb.popcount {inp} : i64\n", id.0));
121            }
122            ScOp::GraphForward { id, features, .. } => {
123                let inp = value_wire(graph, *features);
124                mlir.push_str(&format!(
125                    "  // GraphForward: SC AND-popcount aggregation\n  %v{} = {inp} : i64\n",
126                    id.0
127                ));
128            }
129            ScOp::SoftmaxAttention { id, v, .. } => {
130                let inp = value_wire(graph, *v);
131                mlir.push_str(&format!(
132                    "  // SoftmaxAttention: SC bitstream attention\n  %v{} = {inp} : i64\n",
133                    id.0
134                ));
135            }
136            ScOp::KuramotoStep { id, phases, .. } => {
137                let inp = value_wire(graph, *phases);
138                mlir.push_str(&format!(
139                    "  // KuramotoStep: phase accumulator with coupling\n  %v{} = {inp} : i64\n",
140                    id.0
141                ));
142            }
143            ScOp::Scale { id, input, factor } => {
144                let inp = value_wire(graph, *input);
145                let scale_int = (*factor * 256.0) as i64;
146                mlir.push_str(&format!(
147                    "  %v{} = comb.mul {inp}, %c_scale_{id} : i16\n",
148                    id.0,
149                    inp = inp,
150                    id = id.0,
151                ));
152                let _ = scale_int; // used in constant emission
153            }
154            ScOp::Offset { id, input, offset } => {
155                let inp = value_wire(graph, *input);
156                let _ = offset;
157                mlir.push_str(&format!(
158                    "  %v{} = comb.add {inp}, %c_off_{id} : i16\n",
159                    id.0,
160                    inp = inp,
161                    id = id.0,
162                ));
163            }
164            ScOp::DivConst { id, input, divisor } => {
165                let inp = value_wire(graph, *input);
166                let _ = divisor;
167                mlir.push_str(&format!(
168                    "  %v{} = comb.divu {inp}, %c_div_{id} : i16\n",
169                    id.0,
170                    inp = inp,
171                    id = id.0,
172                ));
173            }
174            ScOp::Reduce { id, input, mode } => {
175                let inp = value_wire(graph, *input);
176                let op_name = match mode {
177                    ReduceMode::Sum => "add",
178                    ReduceMode::Max => "max",
179                };
180                mlir.push_str(&format!("  %v{} = comb.{op_name} {inp} : i64\n", id.0,));
181            }
182        }
183    }
184
185    // Close module (output already emitted in the Output arm)
186    mlir.push_str("}\n");
187    let _ = last_output;
188    Ok(mlir)
189}
190
191fn value_wire(graph: &ScGraph, id: ValueId) -> String {
192    for op in &graph.ops {
193        if op.result_id() == id {
194            return match op {
195                ScOp::Input { name, .. } => format!("%{name}"),
196                ScOp::Constant { id, .. } => format!("%c{}", id.0),
197                ScOp::LifStep { id, .. } => format!("%v{}_spike", id.0),
198                ScOp::DenseForward { id, .. } => format!("%v{}", id.0),
199                _ => format!("%v{}", id.0),
200            };
201        }
202    }
203    format!("%v{}", id.0)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::ir::builder::ScGraphBuilder;
210
211    #[test]
212    fn basic_and_gate_emits_mlir() {
213        let mut b = ScGraphBuilder::new("test_and");
214        let a = b.input("a", ScType::Bool);
215        let bv = b.input("b", ScType::Bool);
216        let c = b.bitwise_and(a, bv);
217        b.output("out", c);
218        let graph = b.build();
219
220        let mlir = emit(&graph).unwrap();
221        assert!(mlir.contains("hw.module @test_and"));
222        assert!(mlir.contains("comb.and"));
223        assert!(mlir.contains("hw.output"));
224    }
225
226    #[test]
227    fn module_wrapping() {
228        let mut b = ScGraphBuilder::new("wrapper");
229        let x = b.input("x", ScType::Bool);
230        b.output("y", x);
231        let graph = b.build();
232
233        let mlir = emit(&graph).unwrap();
234        assert!(mlir.starts_with("// Auto-generated"));
235        assert!(mlir.contains("hw.module @wrapper"));
236        assert!(mlir.ends_with("}\n"));
237    }
238
239    #[test]
240    fn xor_gate() {
241        let mut b = ScGraphBuilder::new("test_xor");
242        let a = b.input("a", ScType::Bool);
243        let bv = b.input("b", ScType::Bool);
244        let c = b.bitwise_xor(a, bv);
245        b.output("out", c);
246        let graph = b.build();
247
248        let mlir = emit(&graph).unwrap();
249        assert!(mlir.contains("comb.xor"));
250    }
251}