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_finite_f64(s: &str, line: usize) -> Result<f64, ParseError> {
196 let value: f64 = s
197 .parse()
198 .map_err(|e: std::num::ParseFloatError| make_err(line, e.to_string()))?;
199 if !value.is_finite() {
200 return Err(make_err(line, format!("non-finite float literal '{s}'")));
201 }
202 Ok(value)
203}
204
205fn parse_scalar_constant(val_str: &str, ty: &ScType, line: usize) -> Result<ScConst, ParseError> {
206 if val_str.contains('.') || matches!(ty, ScType::Rate) {
207 return parse_finite_f64(val_str, line).map(ScConst::F64);
208 }
209 match ty {
210 ScType::FixedPoint { .. } | ScType::SInt { .. } => val_str
211 .parse::<i64>()
212 .map(ScConst::I64)
213 .map_err(|e| make_err(line, e.to_string())),
214 _ => val_str
215 .parse::<u64>()
216 .map(ScConst::U64)
217 .map_err(|e| make_err(line, e.to_string())),
218 }
219}
220
221fn parse_vector_constant(val_str: &str, line: usize) -> Result<ScConst, ParseError> {
222 let inner = val_str
223 .strip_prefix('[')
224 .and_then(|s| s.strip_suffix(']'))
225 .ok_or_else(|| make_err(line, "malformed vector constant"))?;
226 if inner.trim().is_empty() {
227 return Ok(ScConst::I64Vec(Vec::new()));
228 }
229 let is_float = inner.split(',').any(|part| part.trim().contains('.'));
230 if is_float {
231 let mut out = Vec::new();
232 for token in inner.split(',') {
233 out.push(parse_finite_f64(token.trim(), line)?);
234 }
235 Ok(ScConst::F64Vec(out))
236 } else {
237 let mut out = Vec::new();
238 for token in inner.split(',') {
239 out.push(
240 token
241 .trim()
242 .parse::<i64>()
243 .map_err(|e| make_err(line, e.to_string()))?,
244 );
245 }
246 Ok(ScConst::I64Vec(out))
247 }
248}
249
250fn parse_input(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
253 let parts: Vec<&str> = text.splitn(2, "= sc.input").collect();
255 if parts.len() != 2 {
256 return Err(make_err(line, "malformed sc.input"));
257 }
258 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
259 let rest = parts[1].trim();
260
261 let name_start = rest
263 .find('"')
264 .ok_or_else(|| make_err(line, "missing name"))?;
265 let name_end = rest[name_start + 1..]
266 .find('"')
267 .ok_or_else(|| make_err(line, "unterminated name"))?;
268 let name = rest[name_start + 1..name_start + 1 + name_end].to_string();
269
270 let colon_pos = rest
272 .rfind(':')
273 .ok_or_else(|| make_err(line, "missing type"))?;
274 let ty = parse_type(&rest[colon_pos + 1..]).map_err(|e| make_err(line, e))?;
275
276 graph.next_id = graph.next_id.max(id.0 + 1);
277 graph.push(ScOp::Input { id, name, ty });
278 Ok(())
279}
280
281fn parse_output(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
282 let rest = text.strip_prefix("sc.output").unwrap_or(text).trim();
284 let name_start = rest
285 .find('"')
286 .ok_or_else(|| make_err(line, "missing name"))?;
287 let name_end = rest[name_start + 1..]
288 .find('"')
289 .ok_or_else(|| make_err(line, "unterminated name"))?;
290 let name = rest[name_start + 1..name_start + 1 + name_end].to_string();
291
292 let after_name = rest[name_start + 1 + name_end + 1..].trim();
293 let source = parse_value_id(after_name).map_err(|e| make_err(line, e))?;
294
295 let id = ValueId(graph.next_id);
296 graph.next_id += 1;
297 graph.push(ScOp::Output { id, name, source });
298 Ok(())
299}
300
301fn parse_constant(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
302 let parts: Vec<&str> = text.splitn(2, "= sc.constant").collect();
303 if parts.len() != 2 {
304 return Err(make_err(line, "malformed sc.constant"));
305 }
306 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
307 let rest = parts[1].trim();
308
309 let colon_pos = rest
310 .rfind(':')
311 .ok_or_else(|| make_err(line, "missing type"))?;
312 let val_str = rest[..colon_pos].trim();
313 let ty = parse_type(&rest[colon_pos + 1..]).map_err(|e| make_err(line, e))?;
314
315 let value = if val_str.starts_with('[') {
316 parse_vector_constant(val_str, line)?
317 } else {
318 parse_scalar_constant(val_str, &ty, line)?
319 };
320
321 graph.next_id = graph.next_id.max(id.0 + 1);
322 graph.push(ScOp::Constant { id, value, ty });
323 Ok(())
324}
325
326fn parse_encode(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
327 let parts: Vec<&str> = text.splitn(2, "= sc.encode").collect();
328 if parts.len() != 2 {
329 return Err(make_err(line, "malformed sc.encode"));
330 }
331 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
332 let rest = parts[1].trim();
333
334 let tokens: Vec<&str> = rest.split_whitespace().collect();
336 let prob = parse_value_id(
337 tokens
338 .first()
339 .ok_or_else(|| make_err(line, "missing prob"))?,
340 )
341 .map_err(|e| make_err(line, e))?;
342
343 let length_str = extract_kv(rest, "length").ok_or_else(|| make_err(line, "missing length"))?;
344 let length = length_str
345 .parse::<usize>()
346 .map_err(|e| make_err(line, e.to_string()))?;
347
348 let seed_str = extract_kv(rest, "seed").ok_or_else(|| make_err(line, "missing seed"))?;
349 let seed = if seed_str.starts_with("0x") || seed_str.starts_with("0X") {
350 u16::from_str_radix(&seed_str[2..], 16).map_err(|e| make_err(line, e.to_string()))?
351 } else {
352 seed_str
353 .parse::<u16>()
354 .map_err(|e| make_err(line, e.to_string()))?
355 };
356
357 graph.next_id = graph.next_id.max(id.0 + 1);
358 graph.push(ScOp::Encode {
359 id,
360 prob,
361 length,
362 seed,
363 });
364 Ok(())
365}
366
367fn parse_and(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
368 let parts: Vec<&str> = text.splitn(2, "= sc.and").collect();
369 if parts.len() != 2 {
370 return Err(make_err(line, "malformed sc.and"));
371 }
372 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
373 let rest = parts[1].trim();
374 let operands: Vec<&str> = rest.split(':').next().unwrap_or("").split(',').collect();
375 if operands.len() < 2 {
376 return Err(make_err(line, "sc.and needs 2 operands"));
377 }
378 let lhs = parse_value_id(operands[0]).map_err(|e| make_err(line, e))?;
379 let rhs = parse_value_id(operands[1]).map_err(|e| make_err(line, e))?;
380
381 graph.next_id = graph.next_id.max(id.0 + 1);
382 graph.push(ScOp::BitwiseAnd { id, lhs, rhs });
383 Ok(())
384}
385
386fn parse_popcount(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
387 let parts: Vec<&str> = text.splitn(2, "= sc.popcount").collect();
388 if parts.len() != 2 {
389 return Err(make_err(line, "malformed sc.popcount"));
390 }
391 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
392 let rest = parts[1].trim();
393 let input_str = rest.split(':').next().unwrap_or("").trim();
394 let input = parse_value_id(input_str).map_err(|e| make_err(line, e))?;
395
396 graph.next_id = graph.next_id.max(id.0 + 1);
397 graph.push(ScOp::Popcount { id, input });
398 Ok(())
399}
400
401fn parse_dense_forward(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
402 let parts: Vec<&str> = text.splitn(2, "= sc.dense_forward").collect();
403 if parts.len() != 2 {
404 return Err(make_err(line, "malformed sc.dense_forward"));
405 }
406 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
407 let rest = parts[1].trim();
408
409 let tokens: Vec<&str> = rest.split_whitespace().collect();
410 let inputs = parse_value_id(
411 tokens
412 .first()
413 .ok_or_else(|| make_err(line, "missing inputs"))?,
414 )
415 .map_err(|e| make_err(line, e))?;
416
417 let weights_str =
418 extract_kv(rest, "weights").ok_or_else(|| make_err(line, "missing weights"))?;
419 let weights = parse_value_id(&weights_str).map_err(|e| make_err(line, e))?;
420
421 let leak_str = extract_kv(rest, "leak").ok_or_else(|| make_err(line, "missing leak"))?;
422 let leak = parse_value_id(&leak_str).map_err(|e| make_err(line, e))?;
423
424 let gain_str = extract_kv(rest, "gain").ok_or_else(|| make_err(line, "missing gain"))?;
425 let gain = parse_value_id(&gain_str).map_err(|e| make_err(line, e))?;
426
427 let ni = extract_kv(rest, "ni")
428 .and_then(|s| s.parse::<usize>().ok())
429 .unwrap_or(3);
430 let nn = extract_kv(rest, "nn")
431 .and_then(|s| s.parse::<usize>().ok())
432 .unwrap_or(7);
433 let len = extract_kv(rest, "len")
434 .and_then(|s| s.parse::<usize>().ok())
435 .unwrap_or(1024);
436
437 let params = DenseParams {
438 n_inputs: ni,
439 n_neurons: nn,
440 stream_length: len,
441 ..DenseParams::default()
442 };
443
444 graph.next_id = graph.next_id.max(id.0 + 1);
445 graph.push(ScOp::DenseForward {
446 id,
447 inputs,
448 weights,
449 leak,
450 gain,
451 params,
452 });
453 Ok(())
454}
455
456fn parse_lif_step(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
457 let parts: Vec<&str> = text.splitn(2, "= sc.lif_step").collect();
458 if parts.len() != 2 {
459 return Err(make_err(line, "malformed sc.lif_step"));
460 }
461 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
462 let rest = parts[1].trim();
463
464 let tokens: Vec<&str> = rest.split_whitespace().collect();
465 let current = parse_value_id(
466 tokens
467 .first()
468 .ok_or_else(|| make_err(line, "missing current"))?,
469 )
470 .map_err(|e| make_err(line, e))?;
471
472 let leak_str = extract_kv(rest, "leak").ok_or_else(|| make_err(line, "missing leak"))?;
473 let leak = parse_value_id(&leak_str).map_err(|e| make_err(line, e))?;
474
475 let gain_str = extract_kv(rest, "gain").ok_or_else(|| make_err(line, "missing gain"))?;
476 let gain = parse_value_id(&gain_str).map_err(|e| make_err(line, e))?;
477
478 let noise_str = extract_kv(rest, "noise").ok_or_else(|| make_err(line, "missing noise"))?;
479 let noise = parse_value_id(&noise_str).map_err(|e| make_err(line, e))?;
480
481 let dw = extract_kv(rest, "dw")
482 .and_then(|s| s.parse::<u32>().ok())
483 .unwrap_or(16);
484 let frac = extract_kv(rest, "frac")
485 .and_then(|s| s.parse::<u32>().ok())
486 .unwrap_or(8);
487 let vt = extract_kv(rest, "vt")
488 .and_then(|s| s.parse::<i64>().ok())
489 .unwrap_or(256);
490 let rp = extract_kv(rest, "rp")
491 .and_then(|s| s.parse::<u32>().ok())
492 .unwrap_or(2);
493
494 let params = LifParams {
495 data_width: dw,
496 fraction: frac,
497 v_threshold: vt,
498 refractory_period: rp,
499 ..LifParams::default()
500 };
501
502 graph.next_id = graph.next_id.max(id.0 + 1);
503 graph.push(ScOp::LifStep {
504 id,
505 current,
506 leak,
507 gain,
508 noise,
509 params,
510 });
511 Ok(())
512}
513
514fn parse_scale(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
515 let parts: Vec<&str> = text.splitn(2, "= sc.scale").collect();
516 if parts.len() != 2 {
517 return Err(make_err(line, "malformed sc.scale"));
518 }
519 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
520 let rest = parts[1].trim();
521
522 let tokens: Vec<&str> = rest.split_whitespace().collect();
523 let input = parse_value_id(
524 tokens
525 .first()
526 .ok_or_else(|| make_err(line, "missing input"))?,
527 )
528 .map_err(|e| make_err(line, e))?;
529
530 let factor_str = extract_kv(rest, "factor").ok_or_else(|| make_err(line, "missing factor"))?;
531 let factor = parse_finite_f64(&factor_str, line)?;
532
533 graph.next_id = graph.next_id.max(id.0 + 1);
534 graph.push(ScOp::Scale { id, input, factor });
535 Ok(())
536}
537
538fn parse_offset(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
539 let parts: Vec<&str> = text.splitn(2, "= sc.offset").collect();
540 if parts.len() != 2 {
541 return Err(make_err(line, "malformed sc.offset"));
542 }
543 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
544 let rest = parts[1].trim();
545
546 let tokens: Vec<&str> = rest.split_whitespace().collect();
547 let input = parse_value_id(
548 tokens
549 .first()
550 .ok_or_else(|| make_err(line, "missing input"))?,
551 )
552 .map_err(|e| make_err(line, e))?;
553
554 let offset_str = extract_kv(rest, "offset").ok_or_else(|| make_err(line, "missing offset"))?;
555 let offset = parse_finite_f64(&offset_str, line)?;
556
557 graph.next_id = graph.next_id.max(id.0 + 1);
558 graph.push(ScOp::Offset { id, input, offset });
559 Ok(())
560}
561
562fn parse_xor(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
563 let parts: Vec<&str> = text.splitn(2, "= sc.xor").collect();
564 if parts.len() != 2 {
565 return Err(make_err(line, "malformed sc.xor"));
566 }
567 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
568 let rest = parts[1].trim();
569 let operands: Vec<&str> = rest.split(':').next().unwrap_or("").split(',').collect();
570 if operands.len() < 2 {
571 return Err(make_err(line, "sc.xor needs 2 operands"));
572 }
573 let lhs = parse_value_id(operands[0]).map_err(|e| make_err(line, e))?;
574 let rhs = parse_value_id(operands[1]).map_err(|e| make_err(line, e))?;
575
576 graph.next_id = graph.next_id.max(id.0 + 1);
577 graph.push(ScOp::BitwiseXor { id, lhs, rhs });
578 Ok(())
579}
580
581fn parse_reduce(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
582 let parts: Vec<&str> = text.splitn(2, "= sc.reduce").collect();
583 if parts.len() != 2 {
584 return Err(make_err(line, "malformed sc.reduce"));
585 }
586 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
587 let rest = parts[1].trim();
588
589 let tokens: Vec<&str> = rest.split_whitespace().collect();
590 let input = parse_value_id(
591 tokens
592 .first()
593 .ok_or_else(|| make_err(line, "missing input"))?,
594 )
595 .map_err(|e| make_err(line, e))?;
596
597 let mode_str = extract_kv(rest, "mode").unwrap_or_else(|| "sum".to_string());
598 let mode = match mode_str.as_str() {
599 "max" => ReduceMode::Max,
600 _ => ReduceMode::Sum,
601 };
602
603 graph.next_id = graph.next_id.max(id.0 + 1);
604 graph.push(ScOp::Reduce { id, input, mode });
605 Ok(())
606}
607
608fn parse_graph_forward(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
609 let parts: Vec<&str> = text.splitn(2, "= sc.graph_forward").collect();
610 if parts.len() != 2 {
611 return Err(make_err(line, "malformed sc.graph_forward"));
612 }
613 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
614 let rest = parts[1].trim();
615
616 let tokens: Vec<&str> = rest.split_whitespace().collect();
617 let features = parse_value_id(
618 tokens
619 .first()
620 .ok_or_else(|| make_err(line, "missing features"))?,
621 )
622 .map_err(|e| make_err(line, e))?;
623
624 let adj_str = extract_kv(rest, "adj").ok_or_else(|| make_err(line, "missing adj"))?;
625 let adjacency = parse_value_id(&adj_str).map_err(|e| make_err(line, e))?;
626
627 let n_nodes = extract_kv(rest, "nodes")
628 .and_then(|s| s.parse::<usize>().ok())
629 .unwrap_or(16);
630 let n_features = extract_kv(rest, "features")
631 .and_then(|s| s.parse::<usize>().ok())
632 .unwrap_or(4);
633
634 graph.next_id = graph.next_id.max(id.0 + 1);
635 graph.push(ScOp::GraphForward {
636 id,
637 features,
638 adjacency,
639 n_nodes,
640 n_features,
641 });
642 Ok(())
643}
644
645fn parse_softmax_attention(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
646 let parts: Vec<&str> = text.splitn(2, "= sc.softmax_attention").collect();
647 if parts.len() != 2 {
648 return Err(make_err(line, "malformed sc.softmax_attention"));
649 }
650 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
651 let rest = parts[1].trim();
652
653 let before_colon = rest.split(':').next().unwrap_or("");
654 let operands: Vec<&str> = before_colon.split(',').collect();
655 if operands.len() < 3 {
656 return Err(make_err(line, "sc.softmax_attention needs q, k, v"));
657 }
658 let q = parse_value_id(operands[0]).map_err(|e| make_err(line, e))?;
659 let k = parse_value_id(operands[1]).map_err(|e| make_err(line, e))?;
660
661 let v_token = operands[2].split_whitespace().next().unwrap_or("");
663 let v = parse_value_id(v_token).map_err(|e| make_err(line, e))?;
664
665 let dim_k = extract_kv(rest, "dim_k")
666 .and_then(|s| s.parse::<usize>().ok())
667 .unwrap_or(64);
668
669 graph.next_id = graph.next_id.max(id.0 + 1);
670 graph.push(ScOp::SoftmaxAttention { id, q, k, v, dim_k });
671 Ok(())
672}
673
674fn parse_kuramoto_step(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
675 let parts: Vec<&str> = text.splitn(2, "= sc.kuramoto_step").collect();
676 if parts.len() != 2 {
677 return Err(make_err(line, "malformed sc.kuramoto_step"));
678 }
679 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
680 let rest = parts[1].trim();
681
682 let tokens: Vec<&str> = rest.split_whitespace().collect();
683 let phases = parse_value_id(
684 tokens
685 .first()
686 .ok_or_else(|| make_err(line, "missing phases"))?,
687 )
688 .map_err(|e| make_err(line, e))?;
689
690 let omega_str = extract_kv(rest, "omega").ok_or_else(|| make_err(line, "missing omega"))?;
691 let omega = parse_value_id(&omega_str).map_err(|e| make_err(line, e))?;
692
693 let k_str = extract_kv(rest, "K").ok_or_else(|| make_err(line, "missing K"))?;
694 let coupling = parse_value_id(&k_str).map_err(|e| make_err(line, e))?;
695
696 let dt = match extract_kv(rest, "dt") {
697 Some(s) => parse_finite_f64(&s, line)?,
698 None => 0.01,
699 };
700
701 graph.next_id = graph.next_id.max(id.0 + 1);
702 graph.push(ScOp::KuramotoStep {
703 id,
704 phases,
705 omega,
706 coupling,
707 dt,
708 });
709 Ok(())
710}
711
712fn parse_div_const(text: &str, graph: &mut ScGraph, line: usize) -> Result<(), ParseError> {
713 let parts: Vec<&str> = text.splitn(2, "= sc.div_const").collect();
714 if parts.len() != 2 {
715 return Err(make_err(line, "malformed sc.div_const"));
716 }
717 let id = parse_value_id(parts[0]).map_err(|e| make_err(line, e))?;
718 let rest = parts[1].trim();
719
720 let tokens: Vec<&str> = rest.split_whitespace().collect();
721 let input = parse_value_id(
722 tokens
723 .first()
724 .ok_or_else(|| make_err(line, "missing input"))?,
725 )
726 .map_err(|e| make_err(line, e))?;
727
728 let divisor_str =
729 extract_kv(rest, "divisor").ok_or_else(|| make_err(line, "missing divisor"))?;
730 let divisor = divisor_str
731 .parse::<u64>()
732 .map_err(|e| make_err(line, e.to_string()))?;
733
734 graph.next_id = graph.next_id.max(id.0 + 1);
735 graph.push(ScOp::DivConst { id, input, divisor });
736 Ok(())
737}