sc_neurocore_engine/ir/printer.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 — Text-format printer for SC IR graphs
8
9//! Text-format printer for SC IR graphs.
10//!
11//! # Format
12//!
13//! ```text
14//! sc.graph @module_name {
15//! %0 = sc.input "x_in" : rate
16//! %1 = sc.constant 0.5 : rate
17//! %2 = sc.encode %0, length=1024, seed=0xACE1 : bitstream<1024>
18//! %3 = sc.encode %1, length=1024, seed=0xBEEF : bitstream<1024>
19//! %4 = sc.and %2, %3 : bitstream<1024>
20//! %5 = sc.popcount %4 : u64
21//! sc.output "result" %5
22//! }
23//! ```
24
25use crate::ir::graph::*;
26
27/// Format an `f64` constant so the parser reads it back as a float, not an integer.
28///
29/// The scalar/vector constant parser decides the `ScConst` variant from a `.` in the text
30/// (or a `rate` type). A whole-number `f64` such as `5.0` formats as `"5"` via `{}`, which
31/// would re-parse to an integer variant (`U64`/`I64`), and an all-digit vector like
32/// `[5.0, 6.0]` prints `"[5, 6]"` and re-parses to `I64Vec` — either breaks the round trip.
33/// Appending `.0` when the shortest form carries no fractional/exponent marker keeps the
34/// value a float across `parse . print`. (`{}` never emits an exponent for `f64`, so a plain
35/// digit run — optionally signed — is exactly the whole-number case.)
36fn fmt_f64(v: f64) -> String {
37 let s = format!("{v}");
38 if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
39 format!("{s}.0")
40 } else {
41 s
42 }
43}
44
45/// Print a graph to its text representation.
46pub fn print(graph: &ScGraph) -> String {
47 let mut out = String::new();
48 out.push_str(&format!("sc.graph @{} {{\n", graph.name));
49
50 for op in &graph.ops {
51 out.push_str(" ");
52 match op {
53 ScOp::Input { id, name, ty } => {
54 out.push_str(&format!("{} = sc.input \"{}\" : {}\n", id, name, ty));
55 }
56 ScOp::Output { name, source, .. } => {
57 out.push_str(&format!("sc.output \"{}\" {}\n", name, source));
58 }
59 ScOp::Constant { id, value, ty } => {
60 let val_str = match value {
61 ScConst::F64(v) => fmt_f64(*v),
62 ScConst::I64(v) => format!("{v}"),
63 ScConst::U64(v) => format!("{v}"),
64 ScConst::F64Vec(v) => format!(
65 "[{}]",
66 v.iter().map(|x| fmt_f64(*x)).collect::<Vec<_>>().join(", ")
67 ),
68 ScConst::I64Vec(v) => format!(
69 "[{}]",
70 v.iter()
71 .map(|x| format!("{x}"))
72 .collect::<Vec<_>>()
73 .join(", ")
74 ),
75 };
76 out.push_str(&format!("{} = sc.constant {} : {}\n", id, val_str, ty));
77 }
78 ScOp::Encode {
79 id,
80 prob,
81 length,
82 seed,
83 } => {
84 out.push_str(&format!(
85 "{} = sc.encode {}, length={}, seed=0x{:04X} : bitstream<{}>\n",
86 id, prob, length, seed, length
87 ));
88 }
89 ScOp::BitwiseAnd { id, lhs, rhs } => {
90 out.push_str(&format!("{} = sc.and {}, {} : bitstream\n", id, lhs, rhs));
91 }
92 ScOp::BitwiseXor { id, lhs, rhs } => {
93 out.push_str(&format!("{} = sc.xor {}, {} : bitstream\n", id, lhs, rhs));
94 }
95 ScOp::Popcount { id, input } => {
96 out.push_str(&format!("{} = sc.popcount {} : u64\n", id, input));
97 }
98 ScOp::Reduce { id, input, mode } => {
99 out.push_str(&format!(
100 "{} = sc.reduce {}, mode={} : rate\n",
101 id, input, mode
102 ));
103 }
104 ScOp::LifStep {
105 id,
106 current,
107 leak,
108 gain,
109 noise,
110 params,
111 } => {
112 out.push_str(&format!(
113 "{} = sc.lif_step {}, leak={}, gain={}, noise={}, \
114 dw={}, frac={}, vt={}, rp={} : (bool, fixed<{},{}>)\n",
115 id,
116 current,
117 leak,
118 gain,
119 noise,
120 params.data_width,
121 params.fraction,
122 params.v_threshold,
123 params.refractory_period,
124 params.data_width,
125 params.fraction
126 ));
127 }
128 ScOp::DenseForward {
129 id,
130 inputs,
131 weights,
132 leak,
133 gain,
134 params,
135 } => {
136 out.push_str(&format!(
137 "{} = sc.dense_forward {}, weights={}, leak={}, gain={}, \
138 ni={}, nn={}, len={} : vec<bool,{}>\n",
139 id,
140 inputs,
141 weights,
142 leak,
143 gain,
144 params.n_inputs,
145 params.n_neurons,
146 params.stream_length,
147 params.n_neurons
148 ));
149 }
150 ScOp::DclsLayer {
151 id,
152 spike,
153 weights,
154 centre,
155 sigma,
156 params,
157 } => {
158 out.push_str(&format!(
159 "{} = sc.dcls_layer {}, weights={}, centre={}, sigma={}, \
160 taps={}, depth={}, dw={}, frac={} : fixed<{},{}>\n",
161 id,
162 spike,
163 weights,
164 centre,
165 sigma,
166 params.n_taps,
167 params.delay_depth,
168 params.data_width,
169 params.fraction,
170 params.data_width,
171 params.fraction
172 ));
173 }
174 ScOp::GraphForward {
175 id,
176 features,
177 adjacency,
178 n_nodes,
179 n_features,
180 } => {
181 out.push_str(&format!(
182 "{} = sc.graph_forward {}, adj={}, nodes={}, features={} : rate\n",
183 id, features, adjacency, n_nodes, n_features
184 ));
185 }
186 ScOp::SoftmaxAttention { id, q, k, v, dim_k } => {
187 out.push_str(&format!(
188 "{} = sc.softmax_attention {}, {}, {}, dim_k={} : rate\n",
189 id, q, k, v, dim_k
190 ));
191 }
192 ScOp::KuramotoStep {
193 id,
194 phases,
195 omega,
196 coupling,
197 dt,
198 } => {
199 out.push_str(&format!(
200 "{} = sc.kuramoto_step {}, omega={}, K={}, dt={} : rate\n",
201 id, phases, omega, coupling, dt
202 ));
203 }
204 ScOp::Scale { id, input, factor } => {
205 out.push_str(&format!(
206 "{} = sc.scale {}, factor={} : rate\n",
207 id, input, factor
208 ));
209 }
210 ScOp::Offset { id, input, offset } => {
211 out.push_str(&format!(
212 "{} = sc.offset {}, offset={} : rate\n",
213 id, input, offset
214 ));
215 }
216 ScOp::DivConst { id, input, divisor } => {
217 out.push_str(&format!(
218 "{} = sc.div_const {}, divisor={} : u64\n",
219 id, input, divisor
220 ));
221 }
222 }
223 }
224
225 out.push_str("}\n");
226 out
227}