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 { id, prob, .. } => {
59                let prob_wire = value_wire(graph, *prob);
60                mlir.push_str(&format!(
61                    "  %v{} = hw.instance \"enc_{}\" @sc_bitstream_encoder(\
62                     clk: %clk: i1, rst_n: %rst_n: i1, x_value: {}: i1) -> (bit_out: i1)\n",
63                    id.0, id.0, prob_wire
64                ));
65                last_output = format!("%v{}", id.0);
66            }
67            ScOp::BitwiseAnd { id, lhs, rhs } => {
68                let l = value_wire(graph, *lhs);
69                let r = value_wire(graph, *rhs);
70                mlir.push_str(&format!("  %v{} = comb.and {l}, {r} : i1\n", id.0));
71                last_output = format!("%v{}", id.0);
72            }
73            ScOp::BitwiseXor { id, lhs, rhs } => {
74                let l = value_wire(graph, *lhs);
75                let r = value_wire(graph, *rhs);
76                mlir.push_str(&format!("  %v{} = comb.xor {l}, {r} : i1\n", id.0));
77                last_output = format!("%v{}", id.0);
78            }
79            ScOp::Constant { id, value, .. } => {
80                let val = match value {
81                    ScConst::I64(v) => format!("{v}"),
82                    ScConst::U64(v) => format!("{v}"),
83                    ScConst::F64(v) => format!("{}", (*v * 256.0) as i64),
84                    _ => "0".to_string(),
85                };
86                mlir.push_str(&format!("  %c{} = hw.constant {} : i16\n", id.0, val));
87            }
88            ScOp::LifStep {
89                id,
90                current,
91                params,
92                ..
93            } => {
94                let cur = value_wire(graph, *current);
95                mlir.push_str(&format!(
96                    "  %v{id}_spike, %v{id}_v = hw.instance \"lif_{id}\" \
97                     @sc_lif_neuron<DATA_WIDTH: i32 = {dw}, V_THRESHOLD: i32 = {vt}>(\
98                     clk: %clk: i1, rst_n: %rst_n: i1, I_t: {cur}: i1) -> \
99                     (spike_out: i1, v_out: i{dw})\n",
100                    id = id.0,
101                    dw = params.data_width,
102                    vt = params.v_threshold,
103                    cur = cur,
104                ));
105                last_output = format!("%v{}_spike", id.0);
106            }
107            ScOp::DenseForward { id, params, .. } => {
108                mlir.push_str(&format!(
109                    "  %v{} = hw.instance \"dense_{}\" @sc_dense_layer_core<\
110                     N_INPUTS: i32 = {}, N_NEURONS: i32 = {}>(\
111                     clk: %clk: i1, rst_n: %rst_n: i1) -> (spikes: i{})\n",
112                    id.0, id.0, params.n_inputs, params.n_neurons, params.n_neurons
113                ));
114                last_output = format!("%v{}", id.0);
115            }
116            ScOp::DclsLayer { id, params, .. } => {
117                mlir.push_str(&format!(
118                    "  %v{} = hw.instance \"dcls_{}\" @sc_dcls_layer_core<\
119                     N_TAPS: i32 = {}, DATA_WIDTH: i32 = {}, FRACTION: i32 = {}>(\
120                     clk: %clk: i1, rst_n: %rst_n: i1) -> (weighted_sum_q88: i{})\n",
121                    id.0,
122                    id.0,
123                    params.n_taps,
124                    params.data_width,
125                    params.fraction,
126                    params.data_width
127                ));
128                last_output = format!("%v{}", id.0);
129            }
130            ScOp::Popcount { id, input } => {
131                let inp = value_wire(graph, *input);
132                mlir.push_str(&format!("  %v{} = comb.popcount {inp} : i64\n", id.0));
133            }
134            ScOp::GraphForward {
135                id,
136                features,
137                adjacency,
138                n_nodes,
139                n_features,
140            } => {
141                let features_w = value_wire(graph, *features);
142                let adjacency_w = value_wire(graph, *adjacency);
143                mlir.push_str(&format!(
144                    "  %v{id} = hw.instance \"graph_{id}\" @sc_graph_forward<\
145                     N_NODES: i32 = {n_nodes}, N_FEATURES: i32 = {n_features}>(\
146                     features: {features_w}: i64, adjacency: {adjacency_w}: i64) \
147                     -> (agg: i64)\n",
148                    id = id.0,
149                ));
150                last_output = format!("%v{}", id.0);
151            }
152            ScOp::SoftmaxAttention { id, q, k, v, dim_k } => {
153                let q_w = value_wire(graph, *q);
154                let k_w = value_wire(graph, *k);
155                let v_w = value_wire(graph, *v);
156                mlir.push_str(&format!(
157                    "  %v{id} = hw.instance \"softmax_{id}\" @sc_softmax_attention<\
158                     DIM_K: i32 = {dim_k}>(\
159                     q_in: {q_w}: i64, k_in: {k_w}: i64, v_in: {v_w}: i64) \
160                     -> (attn_out: i64)\n",
161                    id = id.0,
162                ));
163                last_output = format!("%v{}", id.0);
164            }
165            ScOp::KuramotoStep {
166                id,
167                phases,
168                omega,
169                coupling,
170                dt,
171            } => {
172                let phases_w = value_wire(graph, *phases);
173                let omega_w = value_wire(graph, *omega);
174                let coupling_w = value_wire(graph, *coupling);
175                let dt_fixed = (*dt * 65536.0).round() as i64;
176                mlir.push_str(&format!(
177                    "  %v{id} = hw.instance \"kuramoto_{id}\" @sc_kuramoto_step<\
178                     DT_FIXED: i32 = {dt}>(\
179                     phases_in: {phases_w}: i64, omega: {omega_w}: i64, \
180                     coupling: {coupling_w}: i64) -> (phases_out: i64)\n",
181                    id = id.0,
182                    dt = dt_fixed,
183                ));
184                last_output = format!("%v{}", id.0);
185            }
186            ScOp::Scale { id, input, factor } => {
187                let inp = value_wire(graph, *input);
188                let scale_int = (*factor * 256.0) as i64;
189                mlir.push_str(&format!(
190                    "  %v{} = comb.mul {inp}, %c_scale_{id} : i16\n",
191                    id.0,
192                    inp = inp,
193                    id = id.0,
194                ));
195                let _ = scale_int; // used in constant emission
196            }
197            ScOp::Offset { id, input, offset } => {
198                let inp = value_wire(graph, *input);
199                let _ = offset;
200                mlir.push_str(&format!(
201                    "  %v{} = comb.add {inp}, %c_off_{id} : i16\n",
202                    id.0,
203                    inp = inp,
204                    id = id.0,
205                ));
206            }
207            ScOp::DivConst { id, input, divisor } => {
208                let inp = value_wire(graph, *input);
209                let _ = divisor;
210                mlir.push_str(&format!(
211                    "  %v{} = comb.divu {inp}, %c_div_{id} : i16\n",
212                    id.0,
213                    inp = inp,
214                    id = id.0,
215                ));
216            }
217            ScOp::Reduce { id, input, mode } => {
218                let inp = value_wire(graph, *input);
219                let op_name = match mode {
220                    ReduceMode::Sum => "add",
221                    ReduceMode::Max => "max",
222                };
223                mlir.push_str(&format!("  %v{} = comb.{op_name} {inp} : i64\n", id.0,));
224            }
225        }
226    }
227
228    // Close module (output already emitted in the Output arm)
229    mlir.push_str("}\n");
230    let _ = last_output;
231    Ok(mlir)
232}
233
234fn value_wire(graph: &ScGraph, id: ValueId) -> String {
235    for op in &graph.ops {
236        if op.result_id() == id {
237            return match op {
238                ScOp::Input { name, .. } => format!("%{name}"),
239                ScOp::Constant { id, .. } => format!("%c{}", id.0),
240                ScOp::LifStep { id, .. } => format!("%v{}_spike", id.0),
241                ScOp::DenseForward { id, .. } => format!("%v{}", id.0),
242                _ => format!("%v{}", id.0),
243            };
244        }
245    }
246    format!("%v{}", id.0)
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::ir::builder::ScGraphBuilder;
253
254    #[test]
255    fn basic_and_gate_emits_mlir() {
256        let mut b = ScGraphBuilder::new("test_and");
257        let a = b.input("a", ScType::Bool);
258        let bv = b.input("b", ScType::Bool);
259        let c = b.bitwise_and(a, bv);
260        b.output("out", c);
261        let graph = b.build();
262
263        let mlir = emit(&graph).unwrap();
264        assert!(mlir.contains("hw.module @test_and"));
265        assert!(mlir.contains("comb.and"));
266        assert!(mlir.contains("hw.output"));
267    }
268
269    #[test]
270    fn module_wrapping() {
271        let mut b = ScGraphBuilder::new("wrapper");
272        let x = b.input("x", ScType::Bool);
273        b.output("y", x);
274        let graph = b.build();
275
276        let mlir = emit(&graph).unwrap();
277        assert!(mlir.starts_with("// Auto-generated"));
278        assert!(mlir.contains("hw.module @wrapper"));
279        assert!(mlir.ends_with("}\n"));
280    }
281
282    #[test]
283    fn xor_gate() {
284        let mut b = ScGraphBuilder::new("test_xor");
285        let a = b.input("a", ScType::Bool);
286        let bv = b.input("b", ScType::Bool);
287        let c = b.bitwise_xor(a, bv);
288        b.output("out", c);
289        let graph = b.build();
290
291        let mlir = emit(&graph).unwrap();
292        assert!(mlir.contains("comb.xor"));
293    }
294}