Skip to main content

sc_neurocore_engine/ir/
emit_mlir.rs

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