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