1use crate::ir::graph::*;
16
17#[derive(Debug, Clone)]
19pub struct ParseError {
20 pub line: usize,
21 pub message: String,
22}
23
24impl std::fmt::Display for ParseError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "line {}: {}", self.line, self.message)
27 }
28}
29
30pub fn parse(text: &str) -> Result<ScGraph, ParseError> {
39 let lines: Vec<&str> = text.lines().collect();
40 if lines.is_empty() {
41 return Err(ParseError {
42 line: 0,
43 message: "empty input".to_string(),
44 });
45 }
46
47 let first = lines[0].trim();
49 let name = first
50 .strip_prefix("sc.graph @")
51 .and_then(|s| s.strip_suffix(" {"))
52 .ok_or_else(|| ParseError {
53 line: 1,
54 message: "expected 'sc.graph @name {'".to_string(),
55 })?
56 .to_string();
57
58 let mut graph = ScGraph::new(name);
59
60 for (line_idx, line) in lines.iter().enumerate().skip(1) {
61 let trimmed = line.trim();
62 if trimmed == "}" || trimmed.is_empty() {
63 continue;
64 }
65
66 if trimmed.contains("= sc.input") {
67 parse_input(trimmed, &mut graph, line_idx + 1)?;
68 } else if trimmed.starts_with("sc.output") {
69 parse_output(trimmed, &mut graph, line_idx + 1)?;
70 } else if trimmed.contains("= sc.constant") {
71 parse_constant(trimmed, &mut graph, line_idx + 1)?;
72 } else if trimmed.contains("= sc.encode") {
73 parse_encode(trimmed, &mut graph, line_idx + 1)?;
74 } else if trimmed.contains("= sc.xor") {
75 parse_xor(trimmed, &mut graph, line_idx + 1)?;
76 } else if trimmed.contains("= sc.and") {
77 parse_and(trimmed, &mut graph, line_idx + 1)?;
78 } else if trimmed.contains("= sc.reduce") {
79 parse_reduce(trimmed, &mut graph, line_idx + 1)?;
80 } else if trimmed.contains("= sc.popcount") {
81 parse_popcount(trimmed, &mut graph, line_idx + 1)?;
82 } else if trimmed.contains("= sc.dense_forward") {
83 parse_dense_forward(trimmed, &mut graph, line_idx + 1)?;
84 } else if trimmed.contains("= sc.graph_forward") {
85 parse_graph_forward(trimmed, &mut graph, line_idx + 1)?;
86 } else if trimmed.contains("= sc.softmax_attention") {
87 parse_softmax_attention(trimmed, &mut graph, line_idx + 1)?;
88 } else if trimmed.contains("= sc.kuramoto_step") {
89 parse_kuramoto_step(trimmed, &mut graph, line_idx + 1)?;
90 } else if trimmed.contains("= sc.lif_step") {
91 parse_lif_step(trimmed, &mut graph, line_idx + 1)?;
92 } else if trimmed.contains("= sc.scale") {
93 parse_scale(trimmed, &mut graph, line_idx + 1)?;
94 } else if trimmed.contains("= sc.offset") {
95 parse_offset(trimmed, &mut graph, line_idx + 1)?;
96 } else if trimmed.contains("= sc.div_const") {
97 parse_div_const(trimmed, &mut graph, line_idx + 1)?;
98 } else {
99 return Err(ParseError {
100 line: line_idx + 1,
101 message: format!("unrecognised op: {}", trimmed),
102 });
103 }
104 }
105
106 Ok(graph)
107}
108
109fn parse_value_id(s: &str) -> Result<ValueId, String> {
112 let s = s.trim().trim_matches(',');
113 s.strip_prefix('%')
114 .and_then(|n| n.parse::<u32>().ok())
115 .map(ValueId)
116 .ok_or_else(|| format!("invalid ValueId: '{}'", s))
117}
118
119fn parse_type(s: &str) -> Result<ScType, String> {
120 let s = s.trim();
121 if s == "rate" {
122 return Ok(ScType::Rate);
123 }
124 if s == "bool" {
125 return Ok(ScType::Bool);
126 }
127 if s == "u64" {
128 return Ok(ScType::UInt { width: 64 });
129 }
130 if let Some(w) = s.strip_prefix('u') {
131 if let Ok(width) = w.parse::<u32>() {
132 return Ok(ScType::UInt { width });
133 }
134 }
135 if let Some(w) = s.strip_prefix('i') {
136 if let Ok(width) = w.parse::<u32>() {
137 return Ok(ScType::SInt { width });
138 }
139 }
140 if let Some(inner) = s
141 .strip_prefix("bitstream<")
142 .and_then(|r| r.strip_suffix('>'))
143 {
144 let length = inner.parse::<usize>().map_err(|e| e.to_string())?;
145 return Ok(ScType::Bitstream { length });
146 }
147 if s == "bitstream" {
148 return Ok(ScType::Bitstream { length: 0 }); }
150 if let Some(inner) = s.strip_prefix("fixed<").and_then(|r| r.strip_suffix('>')) {
151 let parts: Vec<&str> = inner.split(',').collect();
152 if parts.len() == 2 {
153 let width = parts[0].trim().parse::<u32>().map_err(|e| e.to_string())?;
154 let frac = parts[1].trim().parse::<u32>().map_err(|e| e.to_string())?;
155 return Ok(ScType::FixedPoint { width, frac });
156 }
157 }
158 if let Some(inner) = s.strip_prefix("vec<").and_then(|r| r.strip_suffix('>')) {
159 if let Some(comma_pos) = inner.rfind(',') {
161 let elem_str = &inner[..comma_pos];
162 let count_str = inner[comma_pos + 1..].trim();
163 let element = parse_type(elem_str)?;
164 let count = count_str.parse::<usize>().map_err(|e| e.to_string())?;
165 return Ok(ScType::Vec {
166 element: Box::new(element),
167 count,
168 });
169 }
170 }
171 Err(format!("unrecognised type: '{}'", s))
172}
173
174fn extract_kv(text: &str, key: &str) -> Option<String> {
175 text.find(&format!("{}=", key)).map(|start| {
176 let rest = &text[start + key.len() + 1..];
177 let end = rest.find([',', ' ', ':']).unwrap_or(rest.len());
178 rest[..end].to_string()
179 })
180}
181
182fn make_err(line: usize, msg: impl Into<String>) -> ParseError {
183 ParseError {
184 line,
185 message: msg.into(),
186 }
187}
188
189fn parse_scalar_constant(val_str: &str, ty: &ScType, line: usize) -> Result<ScConst, ParseError> {
190 if val_str.contains('.') || matches!(ty, ScType::Rate) {
191 return val_str
192 .parse::<f64>()
193 .map(ScConst::F64)
194 .map_err(|e| make_err(line, e.to_string()));
195 }
196 match ty {
197 ScType::FixedPoint { .. } | ScType::SInt { .. } => val_str
198 .parse::<i64>()
199 .map(ScConst::I64)
200 .map_err(|e| make_err(line, e.to_string())),
201 _ => val_str
202 .parse::<u64>()
203 .map(ScConst::U64)
204 .map_err(|e| make_err(line, e.to_string())),
205 }
206}
207
208fn parse_vector_constant(val_str: &str, line: usize) -> Result<ScConst, ParseError> {
209 let inner = val_str
210 .strip_prefix('[')
211 .and_then(|s| s.strip_suffix(']'))
212 .ok_or_else(|| make_err(line, "malformed vector constant"))?;
213 if inner.trim().is_empty() {
214 return Ok(ScConst::I64Vec(Vec::new()));
215 }
216 let is_float = inner.split(',').any(|part| part.trim().contains('.'));
217 if is_float {
218 let mut out = Vec::new();
219 for token in inner.split(',') {
220 out.push(
221 token
222 .trim()
223 .parse::<f64>()
224 .map_err(|e| make_err(line, e.to_string()))?,
225 );
226 }
227 Ok(ScConst::F64Vec(out))
228 } else {
229 let mut out = Vec::new();
230 for token in inner.split(',') {
231 out.push(
232 token
233 .trim()
234 .parse::<i64>()
235 .map_err(|e| make_err(line, e.to_string()))?,
236 );
237 }
238 Ok(ScConst::I64Vec(out))
239 }
240}
241
242fn parse_input(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
245 let parts: Vec<&str> = text.splitn(2, "= sc.input").collect();
247 if parts.len() != 2 {
248 return Err(make_err(line, "malformed sc.input"));
249 }
250 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
251 let rest = parts[1].trim();
252
253 let name_start = rest
255 .find('"')
256 .ok_or_else(|| make_err(line, "missing name"))?;
257 let name_end = rest[name_start + 1..]
258 .find('"')
259 .ok_or_else(|| make_err(line, "unterminated name"))?;
260 let name = rest[name_start + 1..name_start + 1 + name_end].to_string();
261
262 let colon_pos = rest
264 .rfind(':')
265 .ok_or_else(|| make_err(line, "missing type"))?;
266 let ty = parse_type(&rest[colon_pos + 1..]).map_err(|e| make_err(line, e))?;
267
268 graph.next_id = graph.next_id.max(id.0 + 1);
269 graph.push(ScOp::Input { id, name, ty });
270 Ok(())
271}
272
273fn parse_output(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
274 let rest = text.strip_prefix("sc.output").unwrap_or(text).trim();
276 let name_start = rest
277 .find('"')
278 .ok_or_else(|| make_err(line, "missing name"))?;
279 let name_end = rest[name_start + 1..]
280 .find('"')
281 .ok_or_else(|| make_err(line, "unterminated name"))?;
282 let name = rest[name_start + 1..name_start + 1 + name_end].to_string();
283
284 let after_name = rest[name_start + 1 + name_end + 1..].trim();
285 let source = parse_value_id(after_name).map_err(|e| make_err(line, e))?;
286
287 let id = ValueId(graph.next_id);
288 graph.next_id += 1;
289 graph.push(ScOp::Output { id, name, source });
290 Ok(())
291}
292
293fn parse_constant(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
294 let parts: Vec<&str> = text.splitn(2, "= sc.constant").collect();
295 if parts.len() != 2 {
296 return Err(make_err(line, "malformed sc.constant"));
297 }
298 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
299 let rest = parts[1].trim();
300
301 let colon_pos = rest
302 .rfind(':')
303 .ok_or_else(|| make_err(line, "missing type"))?;
304 let val_str = rest[..colon_pos].trim();
305 let ty = parse_type(&rest[colon_pos + 1..]).map_err(|e| make_err(line, e))?;
306
307 let value = if val_str.starts_with('[') {
308 parse_vector_constant(val_str, line)?
309 } else {
310 parse_scalar_constant(val_str, &ty, line)?
311 };
312
313 graph.next_id = graph.next_id.max(id.0 + 1);
314 graph.push(ScOp::Constant { id, value, ty });
315 Ok(())
316}
317
318fn parse_encode(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
319 let parts: Vec<&str> = text.splitn(2, "= sc.encode").collect();
320 if parts.len() != 2 {
321 return Err(make_err(line, "malformed sc.encode"));
322 }
323 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
324 let rest = parts[1].trim();
325
326 let tokens: Vec<&str> = rest.split_whitespace().collect();
328 let prob = parse_value_id(
329 tokens
330 .first()
331 .ok_or_else(|| make_err(line, "missing prob"))?,
332 )
333 .map_err(|e| make_err(line, e))?;
334
335 let length_str = extract_kv(rest, "length").ok_or_else(|| make_err(line, "missing length"))?;
336 let length = length_str
337 .parse::<usize>()
338 .map_err(|e| make_err(line, e.to_string()))?;
339
340 let seed_str = extract_kv(rest, "seed").ok_or_else(|| make_err(line, "missing seed"))?;
341 let seed = if seed_str.starts_with("0x") || seed_str.starts_with("0X") {
342 u16::from_str_radix(&seed_str[2..], 16).map_err(|e| make_err(line, e.to_string()))?
343 } else {
344 seed_str
345 .parse::<u16>()
346 .map_err(|e| make_err(line, e.to_string()))?
347 };
348
349 graph.next_id = graph.next_id.max(id.0 + 1);
350 graph.push(ScOp::Encode {
351 id,
352 prob,
353 length,
354 seed,
355 });
356 Ok(())
357}
358
359fn parse_and(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
360 let parts: Vec<&str> = text.splitn(2, "= sc.and").collect();
361 if parts.len() != 2 {
362 return Err(make_err(line, "malformed sc.and"));
363 }
364 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
365 let rest = parts[1].trim();
366 let operands: Vec<&str> = rest.split(':').next().unwrap_or("").split(',').collect();
367 if operands.len() < 2 {
368 return Err(make_err(line, "sc.and needs 2 operands"));
369 }
370 let lhs = parse_value_id(operands[0]).map_err(|e| make_err(line, e))?;
371 let rhs = parse_value_id(operands[1]).map_err(|e| make_err(line, e))?;
372
373 graph.next_id = graph.next_id.max(id.0 + 1);
374 graph.push(ScOp::BitwiseAnd { id, lhs, rhs });
375 Ok(())
376}
377
378fn parse_popcount(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
379 let parts: Vec<&str> = text.splitn(2, "= sc.popcount").collect();
380 if parts.len() != 2 {
381 return Err(make_err(line, "malformed sc.popcount"));
382 }
383 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
384 let rest = parts[1].trim();
385 let input_str = rest.split(':').next().unwrap_or("").trim();
386 let input = parse_value_id(input_str).map_err(|e| make_err(line, e))?;
387
388 graph.next_id = graph.next_id.max(id.0 + 1);
389 graph.push(ScOp::Popcount { id, input });
390 Ok(())
391}
392
393fn parse_dense_forward(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
394 let parts: Vec<&str> = text.splitn(2, "= sc.dense_forward").collect();
395 if parts.len() != 2 {
396 return Err(make_err(line, "malformed sc.dense_forward"));
397 }
398 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
399 let rest = parts[1].trim();
400
401 let tokens: Vec<&str> = rest.split_whitespace().collect();
402 let inputs = parse_value_id(
403 tokens
404 .first()
405 .ok_or_else(|| make_err(line, "missing inputs"))?,
406 )
407 .map_err(|e| make_err(line, e))?;
408
409 let weights_str =
410 extract_kv(rest, "weights").ok_or_else(|| make_err(line, "missing weights"))?;
411 let weights = parse_value_id(&weights_str).map_err(|e| make_err(line, e))?;
412
413 let leak_str = extract_kv(rest, "leak").ok_or_else(|| make_err(line, "missing leak"))?;
414 let leak = parse_value_id(&leak_str).map_err(|e| make_err(line, e))?;
415
416 let gain_str = extract_kv(rest, "gain").ok_or_else(|| make_err(line, "missing gain"))?;
417 let gain = parse_value_id(&gain_str).map_err(|e| make_err(line, e))?;
418
419 let ni = extract_kv(rest, "ni")
420 .and_then(|s| s.parse::<usize>().ok())
421 .unwrap_or(3);
422 let nn = extract_kv(rest, "nn")
423 .and_then(|s| s.parse::<usize>().ok())
424 .unwrap_or(7);
425 let len = extract_kv(rest, "len")
426 .and_then(|s| s.parse::<usize>().ok())
427 .unwrap_or(1024);
428
429 let params = DenseParams {
430 n_inputs: ni,
431 n_neurons: nn,
432 stream_length: len,
433 ..DenseParams::default()
434 };
435
436 graph.next_id = graph.next_id.max(id.0 + 1);
437 graph.push(ScOp::DenseForward {
438 id,
439 inputs,
440 weights,
441 leak,
442 gain,
443 params,
444 });
445 Ok(())
446}
447
448fn parse_lif_step(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
449 let parts: Vec<&str> = text.splitn(2, "= sc.lif_step").collect();
450 if parts.len() != 2 {
451 return Err(make_err(line, "malformed sc.lif_step"));
452 }
453 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
454 let rest = parts[1].trim();
455
456 let tokens: Vec<&str> = rest.split_whitespace().collect();
457 let current = parse_value_id(
458 tokens
459 .first()
460 .ok_or_else(|| make_err(line, "missing current"))?,
461 )
462 .map_err(|e| make_err(line, e))?;
463
464 let leak_str = extract_kv(rest, "leak").ok_or_else(|| make_err(line, "missing leak"))?;
465 let leak = parse_value_id(&leak_str).map_err(|e| make_err(line, e))?;
466
467 let gain_str = extract_kv(rest, "gain").ok_or_else(|| make_err(line, "missing gain"))?;
468 let gain = parse_value_id(&gain_str).map_err(|e| make_err(line, e))?;
469
470 let noise_str = extract_kv(rest, "noise").ok_or_else(|| make_err(line, "missing noise"))?;
471 let noise = parse_value_id(&noise_str).map_err(|e| make_err(line, e))?;
472
473 let dw = extract_kv(rest, "dw")
474 .and_then(|s| s.parse::<u32>().ok())
475 .unwrap_or(16);
476 let frac = extract_kv(rest, "frac")
477 .and_then(|s| s.parse::<u32>().ok())
478 .unwrap_or(8);
479 let vt = extract_kv(rest, "vt")
480 .and_then(|s| s.parse::<i64>().ok())
481 .unwrap_or(256);
482 let rp = extract_kv(rest, "rp")
483 .and_then(|s| s.parse::<u32>().ok())
484 .unwrap_or(2);
485
486 let params = LifParams {
487 data_width: dw,
488 fraction: frac,
489 v_threshold: vt,
490 refractory_period: rp,
491 ..LifParams::default()
492 };
493
494 graph.next_id = graph.next_id.max(id.0 + 1);
495 graph.push(ScOp::LifStep {
496 id,
497 current,
498 leak,
499 gain,
500 noise,
501 params,
502 });
503 Ok(())
504}
505
506fn parse_scale(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
507 let parts: Vec<&str> = text.splitn(2, "= sc.scale").collect();
508 if parts.len() != 2 {
509 return Err(make_err(line, "malformed sc.scale"));
510 }
511 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
512 let rest = parts[1].trim();
513
514 let tokens: Vec<&str> = rest.split_whitespace().collect();
515 let input = parse_value_id(
516 tokens
517 .first()
518 .ok_or_else(|| make_err(line, "missing input"))?,
519 )
520 .map_err(|e| make_err(line, e))?;
521
522 let factor_str = extract_kv(rest, "factor").ok_or_else(|| make_err(line, "missing factor"))?;
523 let factor = factor_str
524 .parse::<f64>()
525 .map_err(|e| make_err(line, e.to_string()))?;
526
527 graph.next_id = graph.next_id.max(id.0 + 1);
528 graph.push(ScOp::Scale { id, input, factor });
529 Ok(())
530}
531
532fn parse_offset(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
533 let parts: Vec<&str> = text.splitn(2, "= sc.offset").collect();
534 if parts.len() != 2 {
535 return Err(make_err(line, "malformed sc.offset"));
536 }
537 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
538 let rest = parts[1].trim();
539
540 let tokens: Vec<&str> = rest.split_whitespace().collect();
541 let input = parse_value_id(
542 tokens
543 .first()
544 .ok_or_else(|| make_err(line, "missing input"))?,
545 )
546 .map_err(|e| make_err(line, e))?;
547
548 let offset_str = extract_kv(rest, "offset").ok_or_else(|| make_err(line, "missing offset"))?;
549 let offset = offset_str
550 .parse::<f64>()
551 .map_err(|e| make_err(line, e.to_string()))?;
552
553 graph.next_id = graph.next_id.max(id.0 + 1);
554 graph.push(ScOp::Offset { id, input, offset });
555 Ok(())
556}
557
558fn parse_xor(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
559 let parts: Vec<&str> = text.splitn(2, "= sc.xor").collect();
560 if parts.len() != 2 {
561 return Err(make_err(line, "malformed sc.xor"));
562 }
563 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
564 let rest = parts[1].trim();
565 let operands: Vec<&str> = rest.split(':').next().unwrap_or("").split(',').collect();
566 if operands.len() < 2 {
567 return Err(make_err(line, "sc.xor needs 2 operands"));
568 }
569 let lhs = parse_value_id(operands[0]).map_err(|e| make_err(line, e))?;
570 let rhs = parse_value_id(operands[1]).map_err(|e| make_err(line, e))?;
571
572 graph.next_id = graph.next_id.max(id.0 + 1);
573 graph.push(ScOp::BitwiseXor { id, lhs, rhs });
574 Ok(())
575}
576
577fn parse_reduce(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
578 let parts: Vec<&str> = text.splitn(2, "= sc.reduce").collect();
579 if parts.len() != 2 {
580 return Err(make_err(line, "malformed sc.reduce"));
581 }
582 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
583 let rest = parts[1].trim();
584
585 let tokens: Vec<&str> = rest.split_whitespace().collect();
586 let input = parse_value_id(
587 tokens
588 .first()
589 .ok_or_else(|| make_err(line, "missing input"))?,
590 )
591 .map_err(|e| make_err(line, e))?;
592
593 let mode_str = extract_kv(rest, "mode").unwrap_or_else(|| "sum".to_string());
594 let mode = match mode_str.as_str() {
595 "max" => ReduceMode::Max,
596 _ => ReduceMode::Sum,
597 };
598
599 graph.next_id = graph.next_id.max(id.0 + 1);
600 graph.push(ScOp::Reduce { id, input, mode });
601 Ok(())
602}
603
604fn parse_graph_forward(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
605 let parts: Vec<&str> = text.splitn(2, "= sc.graph_forward").collect();
606 if parts.len() != 2 {
607 return Err(make_err(line, "malformed sc.graph_forward"));
608 }
609 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
610 let rest = parts[1].trim();
611
612 let tokens: Vec<&str> = rest.split_whitespace().collect();
613 let features = parse_value_id(
614 tokens
615 .first()
616 .ok_or_else(|| make_err(line, "missing features"))?,
617 )
618 .map_err(|e| make_err(line, e))?;
619
620 let adj_str = extract_kv(rest, "adj").ok_or_else(|| make_err(line, "missing adj"))?;
621 let adjacency = parse_value_id(&adj_str).map_err(|e| make_err(line, e))?;
622
623 let n_nodes = extract_kv(rest, "nodes")
624 .and_then(|s| s.parse::<usize>().ok())
625 .unwrap_or(16);
626 let n_features = extract_kv(rest, "features")
627 .and_then(|s| s.parse::<usize>().ok())
628 .unwrap_or(4);
629
630 graph.next_id = graph.next_id.max(id.0 + 1);
631 graph.push(ScOp::GraphForward {
632 id,
633 features,
634 adjacency,
635 n_nodes,
636 n_features,
637 });
638 Ok(())
639}
640
641fn parse_softmax_attention(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
642 let parts: Vec<&str> = text.splitn(2, "= sc.softmax_attention").collect();
643 if parts.len() != 2 {
644 return Err(make_err(line, "malformed sc.softmax_attention"));
645 }
646 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
647 let rest = parts[1].trim();
648
649 let before_colon = rest.split(':').next().unwrap_or("");
650 let operands: Vec<&str> = before_colon.split(',').collect();
651 if operands.len() < 3 {
652 return Err(make_err(line, "sc.softmax_attention needs q, k, v"));
653 }
654 let q = parse_value_id(operands[0]).map_err(|e| make_err(line, e))?;
655 let k = parse_value_id(operands[1]).map_err(|e| make_err(line, e))?;
656
657 let v_token = operands[2].split_whitespace().next().unwrap_or("");
659 let v = parse_value_id(v_token).map_err(|e| make_err(line, e))?;
660
661 let dim_k = extract_kv(rest, "dim_k")
662 .and_then(|s| s.parse::<usize>().ok())
663 .unwrap_or(64);
664
665 graph.next_id = graph.next_id.max(id.0 + 1);
666 graph.push(ScOp::SoftmaxAttention { id, q, k, v, dim_k });
667 Ok(())
668}
669
670fn parse_kuramoto_step(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
671 let parts: Vec<&str> = text.splitn(2, "= sc.kuramoto_step").collect();
672 if parts.len() != 2 {
673 return Err(make_err(line, "malformed sc.kuramoto_step"));
674 }
675 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
676 let rest = parts[1].trim();
677
678 let tokens: Vec<&str> = rest.split_whitespace().collect();
679 let phases = parse_value_id(
680 tokens
681 .first()
682 .ok_or_else(|| make_err(line, "missing phases"))?,
683 )
684 .map_err(|e| make_err(line, e))?;
685
686 let omega_str = extract_kv(rest, "omega").ok_or_else(|| make_err(line, "missing omega"))?;
687 let omega = parse_value_id(&omega_str).map_err(|e| make_err(line, e))?;
688
689 let k_str = extract_kv(rest, "K").ok_or_else(|| make_err(line, "missing K"))?;
690 let coupling = parse_value_id(&k_str).map_err(|e| make_err(line, e))?;
691
692 let dt = extract_kv(rest, "dt")
693 .and_then(|s| s.parse::<f64>().ok())
694 .unwrap_or(0.01);
695
696 graph.next_id = graph.next_id.max(id.0 + 1);
697 graph.push(ScOp::KuramotoStep {
698 id,
699 phases,
700 omega,
701 coupling,
702 dt,
703 });
704 Ok(())
705}
706
707fn parse_div_const(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
708 let parts: Vec<&str> = text.splitn(2, "= sc.div_const").collect();
709 if parts.len() != 2 {
710 return Err(make_err(line, "malformed sc.div_const"));
711 }
712 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
713 let rest = parts[1].trim();
714
715 let tokens: Vec<&str> = rest.split_whitespace().collect();
716 let input = parse_value_id(
717 tokens
718 .first()
719 .ok_or_else(|| make_err(line, "missing input"))?,
720 )
721 .map_err(|e| make_err(line, e))?;
722
723 let divisor_str =
724 extract_kv(rest, "divisor").ok_or_else(|| make_err(line, "missing divisor"))?;
725 let divisor = divisor_str
726 .parse::<u64>()
727 .map_err(|e| make_err(line, e.to_string()))?;
728
729 graph.next_id = graph.next_id.max(id.0 + 1);
730 graph.push(ScOp::DivConst { id, input, divisor });
731 Ok(())
732}