bloqade_lanes_bytecode_core/bytecode/
text.rs

1//! SST text assembly format — human-readable parse and print for bytecode programs.
2//!
3//! The text format uses one instruction per line with a `.version` directive:
4//! ```text
5//! .version 1.0
6//! const_loc 0x00010002
7//! initial_fill 1
8//! halt
9//! ```
10
11use std::fmt;
12
13use super::instruction::{
14    ArrayInstruction, AtomArrangementInstruction, CpuInstruction, DetectorObservableInstruction,
15    Instruction, LaneConstInstruction, MeasurementInstruction, QuantumGateInstruction,
16};
17use super::program::Program;
18use crate::version::Version;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ParseError {
22    MissingVersion,
23    InvalidVersion(String),
24    UnknownMnemonic { line: usize, mnemonic: String },
25    MissingOperand { line: usize, mnemonic: String },
26    InvalidOperand { line: usize, message: String },
27}
28
29impl fmt::Display for ParseError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            ParseError::MissingVersion => write!(f, "missing .version directive"),
33            ParseError::InvalidVersion(s) => write!(f, "invalid version: {}", s),
34            ParseError::UnknownMnemonic { line, mnemonic } => {
35                write!(f, "line {}: unknown mnemonic '{}'", line, mnemonic)
36            }
37            ParseError::MissingOperand { line, mnemonic } => {
38                write!(f, "line {}: missing operand for '{}'", line, mnemonic)
39            }
40            ParseError::InvalidOperand { line, message } => {
41                write!(f, "line {}: {}", line, message)
42            }
43        }
44    }
45}
46
47impl std::error::Error for ParseError {}
48
49/// Parse assembly text into a Program.
50pub fn parse(source: &str) -> Result<Program, ParseError> {
51    let mut version: Option<Version> = None;
52    let mut instructions = Vec::new();
53
54    for (line_idx, raw_line) in source.lines().enumerate() {
55        let line_num = line_idx + 1;
56
57        // Strip comments
58        let line = match raw_line.find(';') {
59            Some(pos) => &raw_line[..pos],
60            None => raw_line,
61        };
62        let line = line.trim();
63
64        if line.is_empty() {
65            continue;
66        }
67
68        // Directive
69        if let Some(rest) = line.strip_prefix('.') {
70            let mut parts = rest.split_whitespace();
71            let directive = parts.next().unwrap_or("");
72            if directive == "version" {
73                let val = parts
74                    .next()
75                    .ok_or(ParseError::InvalidVersion("missing value".to_string()))?;
76                version = Some(parse_version(val)?);
77            }
78            continue;
79        }
80
81        // Instruction
82        let mut parts = line.split_whitespace();
83        let mnemonic = parts.next().unwrap();
84        let instr = parse_instruction(mnemonic, &mut parts, line_num)?;
85        instructions.push(instr);
86    }
87
88    let version = version.ok_or(ParseError::MissingVersion)?;
89    Ok(Program {
90        version,
91        instructions,
92    })
93}
94
95fn parse_instruction<'a>(
96    mnemonic: &str,
97    operands: &mut impl Iterator<Item = &'a str>,
98    line: usize,
99) -> Result<Instruction, ParseError> {
100    match mnemonic {
101        "const_float" => {
102            let val = require_operand(operands, line, mnemonic)?;
103            let f: f64 =
104                val.parse()
105                    .map_err(|e: std::num::ParseFloatError| ParseError::InvalidOperand {
106                        line,
107                        message: e.to_string(),
108                    })?;
109            Ok(Instruction::Cpu(CpuInstruction::ConstFloat(f)))
110        }
111        "const_int" => {
112            let val = require_operand(operands, line, mnemonic)?;
113            let n = parse_i64(val, line)?;
114            Ok(Instruction::Cpu(CpuInstruction::ConstInt(n)))
115        }
116        "const_loc" => {
117            let val = require_operand(operands, line, mnemonic)?;
118            let n = parse_hex_u32(val, line)?;
119            Ok(Instruction::LaneConst(LaneConstInstruction::ConstLoc(n)))
120        }
121        "const_lane" => {
122            let val = require_operand(operands, line, mnemonic)?;
123            let n = parse_hex_u64(val, line)?;
124            let data0 = n as u32;
125            let data1 = (n >> 32) as u32;
126            Ok(Instruction::LaneConst(LaneConstInstruction::ConstLane(
127                data0, data1,
128            )))
129        }
130        "const_zone" => {
131            let val = require_operand(operands, line, mnemonic)?;
132            let n = parse_hex_u32(val, line)?;
133            Ok(Instruction::LaneConst(LaneConstInstruction::ConstZone(n)))
134        }
135        "pop" => Ok(Instruction::Cpu(CpuInstruction::Pop)),
136        "dup" => Ok(Instruction::Cpu(CpuInstruction::Dup)),
137        "swap" => Ok(Instruction::Cpu(CpuInstruction::Swap)),
138        "initial_fill" => {
139            let val = require_operand(operands, line, mnemonic)?;
140            let arity = parse_u32(val, line)?;
141            Ok(Instruction::AtomArrangement(
142                AtomArrangementInstruction::InitialFill { arity },
143            ))
144        }
145        "fill" => {
146            let val = require_operand(operands, line, mnemonic)?;
147            let arity = parse_u32(val, line)?;
148            Ok(Instruction::AtomArrangement(
149                AtomArrangementInstruction::Fill { arity },
150            ))
151        }
152        "move" => {
153            let val = require_operand(operands, line, mnemonic)?;
154            let arity = parse_u32(val, line)?;
155            Ok(Instruction::AtomArrangement(
156                AtomArrangementInstruction::Move { arity },
157            ))
158        }
159        "local_r" => {
160            let val = require_operand(operands, line, mnemonic)?;
161            let arity = parse_u32(val, line)?;
162            Ok(Instruction::QuantumGate(QuantumGateInstruction::LocalR {
163                arity,
164            }))
165        }
166        "local_rz" => {
167            let val = require_operand(operands, line, mnemonic)?;
168            let arity = parse_u32(val, line)?;
169            Ok(Instruction::QuantumGate(QuantumGateInstruction::LocalRz {
170                arity,
171            }))
172        }
173        "global_r" => Ok(Instruction::QuantumGate(QuantumGateInstruction::GlobalR)),
174        "global_rz" => Ok(Instruction::QuantumGate(QuantumGateInstruction::GlobalRz)),
175        "cz" => Ok(Instruction::QuantumGate(QuantumGateInstruction::CZ)),
176        "measure" => {
177            let val = require_operand(operands, line, mnemonic)?;
178            let arity = parse_u32(val, line)?;
179            Ok(Instruction::Measurement(MeasurementInstruction::Measure {
180                arity,
181            }))
182        }
183        "await_measure" => Ok(Instruction::Measurement(
184            MeasurementInstruction::AwaitMeasure,
185        )),
186        "new_array" => {
187            let type_str = require_operand(operands, line, mnemonic)?;
188            let type_tag = parse_u8(type_str, line)?;
189            let dim0_str = require_operand(operands, line, "new_array dim0")?;
190            let dim0 = parse_u16(dim0_str, line)?;
191            let dim1 = match operands.next() {
192                Some(s) => parse_u16(s, line)?,
193                None => 0,
194            };
195            Ok(Instruction::Array(ArrayInstruction::NewArray {
196                type_tag,
197                dim0,
198                dim1,
199            }))
200        }
201        "get_item" => {
202            let val = require_operand(operands, line, mnemonic)?;
203            let ndims = parse_u16(val, line)?;
204            Ok(Instruction::Array(ArrayInstruction::GetItem { ndims }))
205        }
206        "set_detector" => Ok(Instruction::DetectorObservable(
207            DetectorObservableInstruction::SetDetector,
208        )),
209        "set_observable" => Ok(Instruction::DetectorObservable(
210            DetectorObservableInstruction::SetObservable,
211        )),
212        "return" => Ok(Instruction::Cpu(CpuInstruction::Return)),
213        "halt" => Ok(Instruction::Cpu(CpuInstruction::Halt)),
214        _ => Err(ParseError::UnknownMnemonic {
215            line,
216            mnemonic: mnemonic.to_string(),
217        }),
218    }
219}
220
221fn require_operand<'a>(
222    operands: &mut impl Iterator<Item = &'a str>,
223    line: usize,
224    mnemonic: &str,
225) -> Result<&'a str, ParseError> {
226    operands.next().ok_or(ParseError::MissingOperand {
227        line,
228        mnemonic: mnemonic.to_string(),
229    })
230}
231
232fn parse_hex_u32(s: &str, line: usize) -> Result<u32, ParseError> {
233    let s = s
234        .strip_prefix("0x")
235        .or_else(|| s.strip_prefix("0X"))
236        .unwrap_or(s);
237    u32::from_str_radix(s, 16).map_err(|e| ParseError::InvalidOperand {
238        line,
239        message: format!("invalid hex u32: {}", e),
240    })
241}
242
243fn parse_i64(s: &str, line: usize) -> Result<i64, ParseError> {
244    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
245        u64::from_str_radix(hex, 16).map(|v| v as i64)
246    } else {
247        s.parse::<i64>()
248    }
249    .map_err(|e| ParseError::InvalidOperand {
250        line,
251        message: format!("invalid i64: {}", e),
252    })
253}
254
255fn parse_hex_u64(s: &str, line: usize) -> Result<u64, ParseError> {
256    let s = s
257        .strip_prefix("0x")
258        .or_else(|| s.strip_prefix("0X"))
259        .unwrap_or(s);
260    u64::from_str_radix(s, 16).map_err(|e| ParseError::InvalidOperand {
261        line,
262        message: format!("invalid hex u64: {}", e),
263    })
264}
265
266fn parse_u16(s: &str, line: usize) -> Result<u16, ParseError> {
267    s.parse::<u16>().map_err(|e| ParseError::InvalidOperand {
268        line,
269        message: format!("invalid u16: {}", e),
270    })
271}
272
273fn parse_u32(s: &str, line: usize) -> Result<u32, ParseError> {
274    s.parse::<u32>().map_err(|e| ParseError::InvalidOperand {
275        line,
276        message: format!("invalid u32: {}", e),
277    })
278}
279
280/// Parse a version string: either "major.minor" (e.g. "1.0") or just "major" (e.g. "1" → Version { major: 1, minor: 0 }).
281fn parse_version(s: &str) -> Result<Version, ParseError> {
282    if let Some((major_str, minor_str)) = s.split_once('.') {
283        let major = major_str
284            .parse::<u16>()
285            .map_err(|e| ParseError::InvalidVersion(e.to_string()))?;
286        let minor = minor_str
287            .parse::<u16>()
288            .map_err(|e| ParseError::InvalidVersion(e.to_string()))?;
289        Ok(Version::new(major, minor))
290    } else {
291        let major = s
292            .parse::<u16>()
293            .map_err(|e| ParseError::InvalidVersion(e.to_string()))?;
294        Ok(Version::new(major, 0))
295    }
296}
297
298fn parse_u8(s: &str, line: usize) -> Result<u8, ParseError> {
299    s.parse::<u8>().map_err(|e| ParseError::InvalidOperand {
300        line,
301        message: format!("invalid u8: {}", e),
302    })
303}
304
305/// Print a Program as assembly text.
306pub fn print(program: &Program) -> String {
307    let mut out = format!(
308        ".version {}.{}\n",
309        program.version.major, program.version.minor
310    );
311
312    for instr in &program.instructions {
313        out.push_str(&print_instruction(instr));
314        out.push('\n');
315    }
316
317    out
318}
319
320fn print_instruction(instr: &Instruction) -> String {
321    match instr {
322        Instruction::Cpu(cpu) => match cpu {
323            CpuInstruction::ConstFloat(f) => format!("const_float {}", format_float(*f)),
324            CpuInstruction::ConstInt(n) => format!("const_int {}", n),
325            CpuInstruction::Pop => "pop".to_string(),
326            CpuInstruction::Dup => "dup".to_string(),
327            CpuInstruction::Swap => "swap".to_string(),
328            CpuInstruction::Return => "return".to_string(),
329            CpuInstruction::Halt => "halt".to_string(),
330        },
331        Instruction::LaneConst(lc) => match lc {
332            LaneConstInstruction::ConstLoc(v) => format!("const_loc 0x{:08x}", v),
333            LaneConstInstruction::ConstLane(d0, d1) => {
334                let combined = (*d0 as u64) | ((*d1 as u64) << 32);
335                format!("const_lane 0x{:016x}", combined)
336            }
337            LaneConstInstruction::ConstZone(v) => format!("const_zone 0x{:08x}", v),
338        },
339        Instruction::AtomArrangement(aa) => match aa {
340            AtomArrangementInstruction::InitialFill { arity } => {
341                format!("initial_fill {}", arity)
342            }
343            AtomArrangementInstruction::Fill { arity } => format!("fill {}", arity),
344            AtomArrangementInstruction::Move { arity } => format!("move {}", arity),
345        },
346        Instruction::QuantumGate(qg) => match qg {
347            QuantumGateInstruction::LocalR { arity } => format!("local_r {}", arity),
348            QuantumGateInstruction::LocalRz { arity } => format!("local_rz {}", arity),
349            QuantumGateInstruction::GlobalR => "global_r".to_string(),
350            QuantumGateInstruction::GlobalRz => "global_rz".to_string(),
351            QuantumGateInstruction::CZ => "cz".to_string(),
352        },
353        Instruction::Measurement(m) => match m {
354            MeasurementInstruction::Measure { arity } => format!("measure {}", arity),
355            MeasurementInstruction::AwaitMeasure => "await_measure".to_string(),
356        },
357        Instruction::Array(arr) => match arr {
358            ArrayInstruction::NewArray {
359                type_tag,
360                dim0,
361                dim1,
362            } => {
363                if *dim1 == 0 {
364                    format!("new_array {} {}", type_tag, dim0)
365                } else {
366                    format!("new_array {} {} {}", type_tag, dim0, dim1)
367                }
368            }
369            ArrayInstruction::GetItem { ndims } => format!("get_item {}", ndims),
370        },
371        Instruction::DetectorObservable(dob) => match dob {
372            DetectorObservableInstruction::SetDetector => "set_detector".to_string(),
373            DetectorObservableInstruction::SetObservable => "set_observable".to_string(),
374        },
375    }
376}
377
378/// Format a float for text output, ensuring it always has a decimal point.
379fn format_float(f: f64) -> String {
380    let s = format!("{}", f);
381    if s.contains('.') || s.contains('e') || s.contains('E') {
382        s
383    } else {
384        format!("{}.0", s)
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_parse_minimal_program() {
394        let source = ".version 1\nhalt\n";
395        let program = parse(source).unwrap();
396        assert_eq!(program.version, Version::new(1, 0));
397        assert_eq!(program.instructions.len(), 1);
398        assert_eq!(
399            program.instructions[0],
400            Instruction::Cpu(CpuInstruction::Halt)
401        );
402    }
403
404    #[test]
405    fn test_parse_with_comments_and_blanks() {
406        let source = r#"
407; This is a comment
408.version 2
409
410const_int 42  ; inline comment
411halt
412"#;
413        let program = parse(source).unwrap();
414        assert_eq!(program.version, Version::new(2, 0));
415        assert_eq!(program.instructions.len(), 2);
416        assert_eq!(
417            program.instructions[0],
418            Instruction::Cpu(CpuInstruction::ConstInt(42))
419        );
420    }
421
422    #[test]
423    fn test_parse_all_mnemonics() {
424        let source = r#".version 1
425const_float 1.5
426const_int 42
427const_loc 0x00010002
428const_lane 0x0000000080010200
429const_zone 0x00000003
430pop
431dup
432swap
433initial_fill 3
434fill 2
435move 1
436local_r 4
437local_rz 2
438global_r
439global_rz
440cz
441measure 1
442await_measure
443new_array 2 10 20
444get_item 2
445set_detector
446set_observable
447return
448halt
449"#;
450        let program = parse(source).unwrap();
451        assert_eq!(program.instructions.len(), 24);
452    }
453
454    #[test]
455    fn test_parse_new_array_1d() {
456        let source = ".version 1\nnew_array 1 10\n";
457        let program = parse(source).unwrap();
458        assert_eq!(
459            program.instructions[0],
460            Instruction::Array(ArrayInstruction::NewArray {
461                type_tag: 1,
462                dim0: 10,
463                dim1: 0,
464            })
465        );
466    }
467
468    #[test]
469    fn test_print_round_trip() {
470        let program = Program {
471            version: Version::new(1, 0),
472            instructions: vec![
473                Instruction::Cpu(CpuInstruction::ConstFloat(1.5)),
474                Instruction::Cpu(CpuInstruction::ConstInt(42)),
475                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0x00010002)),
476                Instruction::LaneConst(LaneConstInstruction::ConstLane(0x80010200, 0x00000000)),
477                Instruction::LaneConst(LaneConstInstruction::ConstZone(3)),
478                Instruction::Cpu(CpuInstruction::Pop),
479                Instruction::Cpu(CpuInstruction::Dup),
480                Instruction::Cpu(CpuInstruction::Swap),
481                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 3 }),
482                Instruction::AtomArrangement(AtomArrangementInstruction::Fill { arity: 2 }),
483                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 1 }),
484                Instruction::QuantumGate(QuantumGateInstruction::LocalR { arity: 4 }),
485                Instruction::QuantumGate(QuantumGateInstruction::LocalRz { arity: 2 }),
486                Instruction::QuantumGate(QuantumGateInstruction::GlobalR),
487                Instruction::QuantumGate(QuantumGateInstruction::GlobalRz),
488                Instruction::QuantumGate(QuantumGateInstruction::CZ),
489                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
490                Instruction::Measurement(MeasurementInstruction::AwaitMeasure),
491                Instruction::Array(ArrayInstruction::NewArray {
492                    type_tag: 2,
493                    dim0: 10,
494                    dim1: 20,
495                }),
496                Instruction::Array(ArrayInstruction::GetItem { ndims: 2 }),
497                Instruction::DetectorObservable(DetectorObservableInstruction::SetDetector),
498                Instruction::DetectorObservable(DetectorObservableInstruction::SetObservable),
499                Instruction::Cpu(CpuInstruction::Return),
500                Instruction::Cpu(CpuInstruction::Halt),
501            ],
502        };
503
504        let text = print(&program);
505        let parsed = parse(&text).unwrap();
506        assert_eq!(program, parsed);
507    }
508
509    #[test]
510    fn test_text_binary_round_trip() {
511        let source = r#".version 1
512const_loc 0x00000000
513const_loc 0x00000001
514initial_fill 2
515const_lane 0x0000000000000100
516move 1
517const_float 1.5708
518global_rz
519const_zone 0x00000000
520cz
521measure 1
522await_measure
523return
524"#;
525        let program = parse(source).unwrap();
526        let binary = program.to_binary();
527        let from_binary = Program::from_binary(&binary).unwrap();
528        let text_again = print(&from_binary);
529        let reparsed = parse(&text_again).unwrap();
530        assert_eq!(program, reparsed);
531    }
532
533    #[test]
534    fn test_missing_version() {
535        let source = "halt\n";
536        assert_eq!(parse(source), Err(ParseError::MissingVersion));
537    }
538
539    #[test]
540    fn test_unknown_mnemonic() {
541        let source = ".version 1\nfoobar\n";
542        assert!(matches!(
543            parse(source),
544            Err(ParseError::UnknownMnemonic { line: 2, .. })
545        ));
546    }
547
548    #[test]
549    fn test_missing_operand() {
550        let source = ".version 1\nconst_int\n";
551        assert!(matches!(
552            parse(source),
553            Err(ParseError::MissingOperand { line: 2, .. })
554        ));
555    }
556
557    #[test]
558    fn test_invalid_operand() {
559        let source = ".version 1\nconst_int abc\n";
560        assert!(matches!(
561            parse(source),
562            Err(ParseError::InvalidOperand { line: 2, .. })
563        ));
564    }
565
566    #[test]
567    fn test_print_new_array_1d() {
568        let program = Program {
569            version: Version::new(1, 0),
570            instructions: vec![Instruction::Array(ArrayInstruction::NewArray {
571                type_tag: 1,
572                dim0: 5,
573                dim1: 0,
574            })],
575        };
576        let text = print(&program);
577        assert!(text.contains("new_array 1 5\n"));
578        assert!(!text.contains("new_array 1 5 0"));
579    }
580
581    #[test]
582    fn test_print_new_array_2d() {
583        let program = Program {
584            version: Version::new(1, 0),
585            instructions: vec![Instruction::Array(ArrayInstruction::NewArray {
586                type_tag: 2,
587                dim0: 10,
588                dim1: 20,
589            })],
590        };
591        let text = print(&program);
592        assert!(text.contains("new_array 2 10 20"));
593    }
594
595    #[test]
596    fn test_design_doc_example() {
597        let source = r#".version 1
598
599const_loc 0x00000000    ; word 0, site 0
600const_loc 0x00000001    ; word 0, site 1
601initial_fill 2
602const_lane 0x0000000000010000   ; fwd, site_bus, word 0, site 1, bus 0
603move 1
604const_loc 0x00000000    ; word 0, site 0
605const_float 1.5708      ; axis_angle
606const_float 3.14159     ; rotation_angle
607local_r 1
608const_float 0.7854      ; rotation_angle
609global_rz
610const_zone 0x00000000
611cz
612const_zone 0x00000000
613measure 1
614await_measure
615return
616"#;
617        let program = parse(source).unwrap();
618        assert_eq!(program.version, Version::new(1, 0));
619        assert_eq!(program.instructions.len(), 17);
620    }
621
622    #[test]
623    fn test_parse_version_major_minor() {
624        let source = ".version 2.3\nhalt\n";
625        let program = parse(source).unwrap();
626        assert_eq!(program.version, Version::new(2, 3));
627    }
628
629    #[test]
630    fn test_print_version_format() {
631        let program = Program {
632            version: Version::new(2, 3),
633            instructions: vec![Instruction::Cpu(CpuInstruction::Halt)],
634        };
635        let text = print(&program);
636        assert!(text.starts_with(".version 2.3\n"));
637    }
638
639    #[test]
640    fn test_const_int_negative() {
641        let source = ".version 1\nconst_int -42\nhalt\n";
642        let program = parse(source).unwrap();
643        assert_eq!(
644            program.instructions[0],
645            Instruction::Cpu(CpuInstruction::ConstInt(-42))
646        );
647        let text = print(&program);
648        assert!(text.contains("const_int -42"));
649    }
650}