Skip to main content

bloq_ir/text/
parse.rs

1//! Parser half of the text exchange format. See the module doc in [`super`]
2//! for the grammar; [`super::write`] is the writer half, and the two must
3//! agree token for token.
4//!
5//! Hand-rolled and line-oriented: every statement is one line and blocks open
6//! with a trailing `{` and close with a lone `}`, so no lookahead or
7//! backtracking is ever needed.
8
9use std::cell::Cell;
10use std::collections::BTreeMap;
11use std::fmt;
12use std::str::FromStr;
13
14use bloq_circuit::{
15    BodyId, CircuitBody, ConditionalCorrection, CoordCircuit, DetectorParity, DetectorTerm, Flow,
16    FlowMarker, GateType, LoopStateId, MeasRecord, Op, Pauli, PauliBasis, PauliMap,
17    checked_translate_coordinate,
18};
19use glam::{IVec2, IVec3};
20use rustc_hash::FxHashMap;
21
22use crate::{
23    Basis, Bloq, BloqEdge, BloqNode, BloqNodeId, BloqNodeKind, BloqTemplate, BloqTemplatePool,
24    BoundaryFace, BundleDetector, BundleMeasurement, ClassicalExpr, ClassicalNode, DetectorBundle,
25    DetectorBundleId, DetectorBundlePool, DetectorBundleUse, InstanceBoundaryOperator,
26    InstanceMeasurement, MetadataValue, NodeDetector, NodeProvenance, NodeRestart,
27    ObservableOutput, PipePadding, PipeSeam, QuantumEdge, QuantumNode, QuantumTimeline, RegionNode,
28    SourceBlockRef, SubGraph, TemplateDetector, TemplateDetectorScope, TemplateId,
29    TemplateInstance, TemplateInstanceId, TemplateRepeatState, TemplateRestart, TemporalPipeRef,
30    ValueRef, ValueRole,
31};
32
33use super::{BLOQ_TEXT_VERSION, decode_metadata_token};
34
35/// A syntax or structure error in Bloq IR text, with its 1-based line number.
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37#[error("bloq text line {line}: {message}")]
38pub struct TextParseError {
39    /// One-based source line.
40    pub line: usize,
41    message: String,
42}
43
44impl TextParseError {
45    fn new(line: usize, message: impl fmt::Display) -> Self {
46        Self {
47            line,
48            message: message.to_string(),
49        }
50    }
51}
52
53type Result<T> = std::result::Result<T, TextParseError>;
54
55pub(super) fn parse_bloq_text(text: &str) -> Result<Bloq> {
56    Parser::new(text).parse_program()
57}
58
59/// The `(measurement id, qubit)` pairs a circuit's ops imply: one per
60/// `Measure` target, one per `MPP` product (its representative coordinate).
61/// The registry is reconstructed from these plus explicit `meas` lines, and
62/// the writer emits a `meas` line exactly for the records not implied here.
63pub(super) fn implied_measurement_qubits<'a>(
64    ops: impl Iterator<Item = &'a Op>,
65) -> FxHashMap<u32, IVec2> {
66    let mut implied = FxHashMap::default();
67    for op in ops {
68        for_each_implied_measurement(op, |measurement, qubit| {
69            implied.insert(measurement, qubit);
70        });
71    }
72    implied
73}
74
75fn for_each_implied_measurement(op: &Op, mut f: impl FnMut(u32, IVec2)) {
76    match op {
77        Op::Measure {
78            qubits,
79            measurements,
80            ..
81        } => {
82            for (&qubit, &measurement) in qubits.iter().zip(measurements) {
83                f(measurement, qubit);
84            }
85        }
86        Op::MPP {
87            products,
88            measurements,
89        } => {
90            for (product, &measurement) in products.iter().zip(measurements) {
91                if let Some(qubit) = product.representative_coord() {
92                    f(measurement, qubit);
93                }
94            }
95        }
96        Op::Gate { .. }
97        | Op::Tick
98        | Op::Repeat { .. }
99        | Op::ConditionalPauli(_)
100        | Op::Depolarize1 { .. }
101        | Op::Depolarize2 { .. }
102        | Op::PauliError { .. } => {}
103    }
104}
105
106fn register_implied_measurements(
107    op: &Op,
108    line: usize,
109    implied: &mut FxHashMap<u32, IVec2>,
110) -> Result<()> {
111    let mut conflict = None;
112    for_each_implied_measurement(op, |measurement, qubit| match implied.get(&measurement) {
113        Some(&previous) if previous != qubit => {
114            conflict.get_or_insert((measurement, previous, qubit));
115        }
116        Some(_) => {}
117        None => {
118            implied.insert(measurement, qubit);
119        }
120    });
121    let Some((measurement, first, second)) = conflict else {
122        return Ok(());
123    };
124    Err(TextParseError::new(
125        line,
126        format!(
127            "measurement m{measurement} is bound to two different qubits: \
128             ({},{}) and ({},{})",
129            first.x, first.y, second.x, second.y
130        ),
131    ))
132}
133
134/// Text is an untrusted boundary. Reject translations that would overflow the
135/// `i32` lattice before later layout/flow code performs ordinary `IVec2`
136/// addition (which panics in debug builds).
137fn validate_text_coordinate_translations(bloq: &Bloq, line: usize) -> Result<()> {
138    for (_, level) in bloq.levels() {
139        for (_, quantum) in level.quantum_nodes() {
140            for instance in &quantum.instances {
141                validate_template_translation(bloq, instance.template_id, instance.offset, line)?;
142            }
143        }
144    }
145    for padding in bloq.pipe_padding() {
146        for template in [padding.one_round, padding.looped] {
147            validate_template_translation(bloq, template, padding.offset, line)?;
148        }
149    }
150    Ok(())
151}
152
153fn validate_template_translation(
154    bloq: &Bloq,
155    template_id: TemplateId,
156    offset: IVec2,
157    line: usize,
158) -> Result<()> {
159    let Some(template) = bloq.templates().get(template_id) else {
160        // Unknown template ids are diagnosed by semantic validation.
161        return Ok(());
162    };
163    for &coord in template.qubits() {
164        validate_translated_coord(coord, offset, template_id, line)?;
165    }
166    for flow in &template.boundary_flows {
167        for (coord, _) in flow.start.iter().chain(flow.end.iter()) {
168            validate_translated_coord(*coord, offset, template_id, line)?;
169        }
170        if let Some(center) = flow.center {
171            validate_translated_coord(center, offset, template_id, line)?;
172        }
173    }
174    Ok(())
175}
176
177fn validate_translated_coord(
178    coord: IVec2,
179    offset: IVec2,
180    template: TemplateId,
181    line: usize,
182) -> Result<()> {
183    if checked_translate_coordinate(coord, offset).is_err() {
184        return Err(TextParseError::new(
185            line,
186            format!(
187                "template t{} coordinate ({},{}) overflows after offset ({},{})",
188                template.0, coord.x, coord.y, offset.x, offset.y
189            ),
190        ));
191    }
192    Ok(())
193}
194
195// ==============================================================================
196// Line source
197// ==============================================================================
198
199struct Parser<'a> {
200    /// Raw lines, split on `\n`.
201    lines: Vec<&'a str>,
202    /// Index of the next unread line.
203    pos: usize,
204}
205
206/// One statement line, pre-split into whitespace tokens. The cursor is a
207/// `Cell` so consuming a token needs only `&self` — several parsers pass the
208/// statement to helpers while also advancing it in the same expression.
209struct Stmt<'a> {
210    line: usize,
211    tokens: Vec<&'a str>,
212    next: Cell<usize>,
213}
214
215impl<'a> Parser<'a> {
216    fn new(text: &'a str) -> Self {
217        Self {
218            lines: text.split('\n').collect(),
219            pos: 0,
220        }
221    }
222
223    /// The next statement line — comments stripped, blank lines skipped.
224    /// `None` at end of input.
225    fn next_stmt(&mut self) -> Option<Stmt<'a>> {
226        while self.pos < self.lines.len() {
227            let line_number = self.pos + 1;
228            let line = self.lines[self.pos];
229            self.pos += 1;
230            let code = line.split('#').next().unwrap_or("");
231            let tokens: Vec<&str> = code.split_whitespace().collect();
232            if !tokens.is_empty() {
233                return Some(Stmt {
234                    line: line_number,
235                    tokens,
236                    next: Cell::new(0),
237                });
238            }
239        }
240        None
241    }
242
243    fn expect_stmt(&mut self, expected: &str) -> Result<Stmt<'a>> {
244        let end = self.lines.len();
245        self.next_stmt().ok_or_else(|| {
246            TextParseError::new(end, format!("expected {expected}, found end of input"))
247        })
248    }
249}
250
251impl<'a> Stmt<'a> {
252    fn err(&self, message: impl fmt::Display) -> TextParseError {
253        TextParseError::new(self.line, message)
254    }
255
256    fn peek(&self) -> Option<&'a str> {
257        self.tokens.get(self.next.get()).copied()
258    }
259
260    fn advance(&self) {
261        self.next.set(self.next.get() + 1);
262    }
263
264    fn next_token(&self, expected: &str) -> Result<&'a str> {
265        let token = self
266            .peek()
267            .ok_or_else(|| self.err(format!("expected {expected}, found end of line")))?;
268        self.advance();
269        Ok(token)
270    }
271
272    fn expect(&self, literal: &str) -> Result<()> {
273        let token = self.next_token(&format!("`{literal}`"))?;
274        if token != literal {
275            return Err(self.err(format!("expected `{literal}`, found `{token}`")));
276        }
277        Ok(())
278    }
279
280    fn finish(&self) -> Result<()> {
281        if let Some(extra) = self.peek() {
282            return Err(self.err(format!("unexpected trailing `{extra}`")));
283        }
284        Ok(())
285    }
286
287    /// Whether this statement is the lone `}` that closes a block.
288    fn is_close(&self) -> bool {
289        self.tokens == ["}"]
290    }
291}
292
293// ==============================================================================
294// Program structure
295// ==============================================================================
296
297impl<'a> Parser<'a> {
298    fn parse_program(&mut self) -> Result<Bloq> {
299        let header = self.expect_stmt("`BLOQIR <version>` header")?;
300        header.expect("BLOQIR")?;
301        let version: u32 = parse_int(&header, header.next_token("format version")?)?;
302        header.finish()?;
303        if version != BLOQ_TEXT_VERSION {
304            return Err(header.err(format!(
305                "unsupported bloq text version {version} (this reader supports only \
306                 version {BLOQ_TEXT_VERSION}; there is no cross-version compatibility)"
307            )));
308        }
309
310        let mut metadata = BTreeMap::new();
311        let mut logical_inputs = Vec::new();
312        let mut logical_outputs = Vec::new();
313        let mut templates = BloqTemplatePool::new();
314        let mut detector_bundles = DetectorBundlePool::new();
315        let mut top = None;
316
317        while let Some(stmt) = self.next_stmt() {
318            match stmt.next_token(
319                "`metadata`, `logical-input`, `logical-output`, `template`, `bundle`, or `graph`",
320            )? {
321                "metadata" => {
322                    let key_token = stmt.next_token("metadata key")?;
323                    let key = decode_metadata_token(key_token).map_err(|message| {
324                        stmt.err(format!("invalid metadata key `{key_token}`: {message}"))
325                    })?;
326                    let kind = stmt.next_token("metadata value type")?;
327                    let value_token = stmt.next_token("metadata value")?;
328                    let value = match kind {
329                        "u64" => MetadataValue::U64(parse_int(&stmt, value_token)?),
330                        "string" => MetadataValue::String(
331                            decode_metadata_token(value_token).map_err(|message| {
332                                stmt.err(format!(
333                                    "invalid metadata string `{value_token}`: {message}"
334                                ))
335                            })?,
336                        ),
337                        other => {
338                            return Err(stmt.err(format!("unknown metadata value type `{other}`")));
339                        }
340                    };
341                    stmt.finish()?;
342                    if metadata.insert(key.clone(), value).is_some() {
343                        return Err(stmt.err(format!("duplicate metadata key `{key}`")));
344                    }
345                }
346                "logical-input" => {
347                    let port = parse_coord3(&stmt, stmt.next_token("input port")?)?;
348                    stmt.expect("instance")?;
349                    let instance = crate::lowering::TemplateInstanceId(parse_prefixed(
350                        &stmt,
351                        stmt.next_token("input Port instance")?,
352                        'i',
353                    )?);
354                    stmt.expect("x")?;
355                    let x = parse_pauli_map(&stmt, stmt.next_token("logical X operator")?)?;
356                    stmt.expect("z")?;
357                    let z = parse_pauli_map(&stmt, stmt.next_token("logical Z operator")?)?;
358                    stmt.finish()?;
359                    logical_inputs.push(crate::LogicalInput {
360                        port,
361                        instance,
362                        x,
363                        z,
364                    });
365                }
366                "logical-output" => {
367                    let port = parse_coord3(&stmt, stmt.next_token("output port")?)?;
368                    stmt.expect("instance")?;
369                    let instance = crate::lowering::TemplateInstanceId(parse_prefixed(
370                        &stmt,
371                        stmt.next_token("output instance")?,
372                        'i',
373                    )?);
374                    stmt.expect("x")?;
375                    let x = parse_pauli_map(&stmt, stmt.next_token("logical X operator")?)?;
376                    stmt.expect("z")?;
377                    let z = parse_pauli_map(&stmt, stmt.next_token("logical Z operator")?)?;
378                    stmt.finish()?;
379                    logical_outputs.push(crate::LogicalOutput {
380                        port,
381                        instance,
382                        x,
383                        z,
384                    });
385                }
386                "template" => {
387                    let id = parse_prefixed(&stmt, stmt.next_token("template id")?, 't')?;
388                    if id as usize != templates.len() {
389                        return Err(stmt.err(format!(
390                            "template t{id} out of order: expected t{}",
391                            templates.len()
392                        )));
393                    }
394                    stmt.expect("{")?;
395                    stmt.finish()?;
396                    templates.insert(self.parse_template()?);
397                }
398                "bundle" => {
399                    let id = parse_prefixed(&stmt, stmt.next_token("bundle id")?, 'b')?;
400                    if id as usize != detector_bundles.len() {
401                        return Err(stmt.err(format!(
402                            "bundle b{id} out of order: expected b{}",
403                            detector_bundles.len()
404                        )));
405                    }
406                    stmt.expect("owners")?;
407                    let mut owner_templates = Vec::new();
408                    while stmt.peek() != Some("{") {
409                        owner_templates.push(TemplateId(parse_prefixed(
410                            &stmt,
411                            stmt.next_token("owner template or `{`")?,
412                            't',
413                        )?));
414                    }
415                    stmt.expect("{")?;
416                    stmt.finish()?;
417                    detector_bundles.insert(self.parse_bundle(owner_templates)?);
418                }
419                "graph" => {
420                    if top.is_some() {
421                        return Err(stmt.err("duplicate graph block"));
422                    }
423                    stmt.expect("{")?;
424                    stmt.finish()?;
425                    top = Some(self.parse_level()?);
426                }
427                other => return Err(stmt.err(format!("unknown section `{other}`"))),
428            }
429        }
430
431        let top =
432            top.ok_or_else(|| TextParseError::new(self.lines.len(), "missing `graph` section"))?;
433        let bloq = Bloq::from_parts(
434            templates,
435            detector_bundles,
436            top,
437            metadata,
438            logical_inputs,
439            logical_outputs,
440        );
441        validate_text_coordinate_translations(&bloq, self.lines.len())?;
442        Ok(bloq)
443    }
444}
445
446impl<'a> Parser<'a> {
447    fn parse_bundle(&mut self, owner_templates: Vec<TemplateId>) -> Result<DetectorBundle> {
448        let mut detectors = Vec::new();
449        loop {
450            let stmt = self.expect_stmt("a bundle detector or `}`")?;
451            if stmt.is_close() {
452                break;
453            }
454            stmt.expect("detector")?;
455            let parity = parse_parity(
456                &stmt,
457                stmt.next_token("bundle detector parity")?,
458                parse_bundle_measurement,
459            )?;
460            let coords = parse_opt_detector_coords(&stmt)?;
461            stmt.finish()?;
462            detectors.push(BundleDetector { parity, coords });
463        }
464        Ok(DetectorBundle::new(owner_templates, detectors))
465    }
466}
467
468// ==============================================================================
469// Templates
470// ==============================================================================
471
472impl<'a> Parser<'a> {
473    fn parse_template(&mut self) -> Result<BloqTemplate> {
474        // Op lines per body id; bare `circuit` is shorthand for entry b0.
475        let mut entry = None;
476        let mut bodies: FxHashMap<u32, (Vec<Op>, usize)> = FxHashMap::default();
477        // Explicit `meas` records with their line numbers for conflict errors.
478        let mut extra_measurements: Vec<(MeasRecord, usize)> = Vec::new();
479        let mut implied_measurements = FxHashMap::default();
480        let mut detectors = Vec::new();
481        let mut repeat_states = Vec::new();
482        let mut boundary_flows = Vec::new();
483        let mut restarts = Vec::new();
484
485        loop {
486            let stmt = self.expect_stmt("a template statement or `}`")?;
487            if stmt.is_close() {
488                break;
489            }
490            let keyword = stmt.next_token("template statement")?;
491            match keyword {
492                "circuit" | "body" => {
493                    let body = if keyword == "circuit" && stmt.peek() == Some("{") {
494                        0
495                    } else {
496                        parse_prefixed(&stmt, stmt.next_token("body id")?, 'b')?
497                    };
498                    if keyword == "circuit" && entry.replace(BodyId(body)).is_some() {
499                        return Err(stmt.err("duplicate circuit entry"));
500                    }
501                    stmt.expect("{")?;
502                    stmt.finish()?;
503                    if bodies
504                        .insert(
505                            body,
506                            (self.parse_ops(&mut implied_measurements)?, stmt.line),
507                        )
508                        .is_some()
509                    {
510                        return Err(stmt.err(format!("duplicate body b{body}")));
511                    }
512                }
513                "meas" => {
514                    let id = parse_measurement_output(&stmt, stmt.next_token("measurement id")?)?;
515                    stmt.expect("@")?;
516                    let qubit = parse_coord2(&stmt, stmt.next_token("qubit coordinate")?)?;
517                    stmt.finish()?;
518                    extra_measurements.push((MeasRecord { id, qubit }, stmt.line));
519                }
520                "detector" => {
521                    let scope = if let Some(token) = stmt.peek()
522                        && let Some(args) = call_args(token, "body")
523                    {
524                        stmt.advance();
525                        let [body] = parse_call_args(&stmt, args)?;
526                        TemplateDetectorScope::RepeatBody {
527                            body: BodyId(parse_prefixed(&stmt, body, 'b')?),
528                        }
529                    } else {
530                        TemplateDetectorScope::TopLevel
531                    };
532                    let parity =
533                        parse_parity(&stmt, stmt.next_token("detector parity")?, parse_meas_term)?;
534                    let coords = parse_opt_detector_coords(&stmt)?;
535                    stmt.finish()?;
536                    detectors.push(TemplateDetector {
537                        scope,
538                        parity,
539                        coords,
540                    });
541                }
542                token if call_args(token, "loop").is_some() => {
543                    let args = call_args(token, "loop").expect("guard matched");
544                    let [body, state] = parse_call_args(&stmt, args)?;
545                    let body = BodyId(parse_prefixed(&stmt, body, 'b')?);
546                    let state = LoopStateId(parse_prefixed(&stmt, state, 's')?);
547                    stmt.expect("init")?;
548                    let initial =
549                        parse_parity(&stmt, stmt.next_token("initial parity")?, parse_meas_term)?;
550                    stmt.expect("next")?;
551                    let next =
552                        parse_parity(&stmt, stmt.next_token("next parity")?, parse_meas_term)?;
553                    stmt.finish()?;
554                    repeat_states.push(TemplateRepeatState {
555                        body,
556                        state,
557                        initial,
558                        next,
559                    });
560                }
561                "flow" => {
562                    boundary_flows.push(parse_flow(&stmt)?);
563                }
564                "restart" => {
565                    let parity =
566                        parse_parity(&stmt, stmt.next_token("restart parity")?, parse_meas_term)?;
567                    stmt.finish()?;
568                    restarts.push(TemplateRestart { parity });
569                }
570                other => return Err(stmt.err(format!("unknown template statement `{other}`"))),
571            }
572        }
573
574        Ok(BloqTemplate::with_parts(
575            build_circuit(
576                bodies,
577                entry.unwrap_or(BodyId(0)),
578                implied_measurements,
579                extra_measurements,
580            )?,
581            detectors,
582            repeat_states,
583            boundary_flows,
584            restarts,
585        ))
586    }
587
588    fn parse_ops(&mut self, implied: &mut FxHashMap<u32, IVec2>) -> Result<Vec<Op>> {
589        let mut ops = Vec::new();
590        loop {
591            let stmt = self.expect_stmt("a circuit op or `}`")?;
592            if stmt.is_close() {
593                return Ok(ops);
594            }
595            let op = parse_op(&stmt)?;
596            register_implied_measurements(&op, stmt.line, implied)?;
597            ops.push(op);
598        }
599    }
600}
601
602/// Assemble a [`CoordCircuit`] from per-body op lists and explicit registry
603/// records. Body ids must be dense because [`CoordCircuit`] stores them in a
604/// vector; the measurement registry is the ops' implied records plus the
605/// explicit ones.
606fn build_circuit(
607    bodies: FxHashMap<u32, (Vec<Op>, usize)>,
608    entry: BodyId,
609    implied: FxHashMap<u32, IVec2>,
610    extra: Vec<(MeasRecord, usize)>,
611) -> Result<CoordCircuit> {
612    if let Some((max_body, (_, line))) = bodies.iter().max_by_key(|(body, _)| **body)
613        && *max_body as usize >= bodies.len()
614    {
615        return Err(TextParseError::new(
616            *line,
617            format!("body b{max_body} is sparse: body ids must be contiguous from b0"),
618        ));
619    }
620
621    let mut circuit = CoordCircuit::new();
622    for _ in 1..bodies.len() {
623        circuit.add_body(CircuitBody::new());
624    }
625    // `MeasRegistry::reserve` panics when one id is bound to two different
626    // qubits, so reject that conflict here with the offending line instead of
627    // aborting on malformed input. Implied/implied conflicts were rejected at
628    // their op line while parsing the body.
629    let mut seen = implied.clone();
630    for (record, line) in &extra {
631        match seen.insert(record.id, record.qubit) {
632            Some(qubit) if qubit != record.qubit => {
633                return Err(TextParseError::new(
634                    *line,
635                    format!(
636                        "measurement m{} is bound to two different qubits: \
637                         ({},{}) and ({},{})",
638                        record.id, qubit.x, qubit.y, record.qubit.x, record.qubit.y
639                    ),
640                ));
641            }
642            _ => {}
643        }
644    }
645
646    let mut records: Vec<MeasRecord> = implied
647        .into_iter()
648        .map(|(id, qubit)| MeasRecord { id, qubit })
649        .collect();
650    records.extend(extra.into_iter().map(|(record, _)| record));
651    for (body_id, (ops, _)) in bodies {
652        *circuit
653            .body_mut(BodyId(body_id))
654            .expect("dense body ids were allocated above")
655            .ops_mut() = ops;
656    }
657    circuit.register_measurement_records(&mut records);
658    circuit
659        .set_entry_body(entry)
660        .expect("dense parsed bodies contain the entry");
661    Ok(circuit)
662}
663
664fn parse_op(stmt: &Stmt<'_>) -> Result<Op> {
665    let token = stmt.next_token("circuit op")?;
666    let (head, probability) = parse_instruction_head(stmt, token)?;
667    let op = match head {
668        "TICK" if probability.is_none() => Op::Tick,
669        "REPEAT" => {
670            reject_probability_argument(stmt, token, probability)?;
671            let repetitions = parse_int(stmt, stmt.next_token("repetition count")?)?;
672            let body = BodyId(parse_prefixed(stmt, stmt.next_token("body id")?, 'b')?);
673            Op::Repeat { body, repetitions }
674        }
675        "CPAULI" => {
676            reject_probability_argument(stmt, token, probability)?;
677            let mut corrections = Vec::new();
678            while let Some(token) = stmt.peek() {
679                stmt.advance();
680                corrections.push(parse_correction(stmt, token)?);
681            }
682            Op::ConditionalPauli(corrections)
683        }
684        "MPP" => {
685            reject_probability_argument(stmt, token, probability)?;
686            let mut products = Vec::new();
687            let mut measurements = Vec::new();
688            while let Some(token) = stmt.peek() {
689                stmt.advance();
690                let (product, measurement) = token.rsplit_once(":").ok_or_else(|| {
691                    stmt.err(format!("MPP product `{token}` is missing its `:mN` record"))
692                })?;
693                let product = parse_pauli_map(stmt, product)?;
694                if product.is_empty() {
695                    return Err(stmt.err("MPP product must contain a non-identity Pauli target"));
696                }
697                products.push(product);
698                measurements.push(parse_measurement_output(stmt, measurement)?);
699            }
700            if products.is_empty() {
701                return Err(stmt.err("MPP requires at least one product"));
702            }
703            Op::MPP {
704                products,
705                measurements,
706            }
707        }
708        "M" | "MX" | "MY" => {
709            let basis = match head {
710                "MX" => PauliBasis::X,
711                "MY" => PauliBasis::Y,
712                _ => PauliBasis::Z,
713            };
714            let mut qubits = Vec::new();
715            let mut measurements = Vec::new();
716            while let Some(token) = stmt.peek() {
717                stmt.advance();
718                let (qubit, measurement) = token.split_once(":").ok_or_else(|| {
719                    stmt.err(format!(
720                        "measure target `{token}` is missing its `:mN` record"
721                    ))
722                })?;
723                qubits.push(parse_coord2(stmt, qubit)?);
724                measurements.push(parse_measurement_output(stmt, measurement)?);
725            }
726            Op::Measure {
727                basis,
728                qubits,
729                measurements,
730                flip_probability: probability.unwrap_or(0.0),
731            }
732        }
733        "DEPOLARIZE1" => Op::Depolarize1 {
734            probability: require_probability_argument(stmt, token, probability)?,
735            qubits: parse_qubit_targets(stmt)?,
736        },
737        "DEPOLARIZE2" => {
738            let qubits = parse_qubit_targets(stmt)?;
739            if qubits.len() % 2 != 0 {
740                return Err(stmt.err(format!(
741                    "DEPOLARIZE2 requires an even target count, got {}",
742                    qubits.len()
743                )));
744            }
745            Op::Depolarize2 {
746                probability: require_probability_argument(stmt, token, probability)?,
747                qubits,
748            }
749        }
750        "X_ERROR" | "Y_ERROR" | "Z_ERROR" => Op::PauliError {
751            probability: require_probability_argument(stmt, token, probability)?,
752            pauli: match head {
753                "X_ERROR" => PauliBasis::X,
754                "Y_ERROR" => PauliBasis::Y,
755                _ => PauliBasis::Z,
756            },
757            qubits: parse_qubit_targets(stmt)?,
758        },
759        gate => {
760            reject_probability_argument(stmt, token, probability)?;
761            let gate =
762                GateType::from_str(gate).map_err(|_| stmt.err(format!("unknown gate `{gate}`")))?;
763            let mut qubits = Vec::new();
764            while let Some(token) = stmt.peek() {
765                stmt.advance();
766                qubits.push(parse_coord2(stmt, token)?);
767            }
768            Op::Gate { gate, qubits }
769        }
770    };
771    stmt.finish()?;
772    Ok(op)
773}
774
775fn parse_instruction_head<'a>(stmt: &Stmt<'_>, token: &'a str) -> Result<(&'a str, Option<f64>)> {
776    let Some((name, argument)) = token.split_once('(') else {
777        return Ok((token, None));
778    };
779    let argument = argument
780        .strip_suffix(')')
781        .ok_or_else(|| stmt.err(format!("malformed instruction argument in `{token}`")))?;
782    if name.is_empty() || argument.is_empty() || argument.contains(['(', ')']) {
783        return Err(stmt.err(format!("malformed instruction argument in `{token}`")));
784    }
785    let probability = argument
786        .parse::<f64>()
787        .map_err(|_| stmt.err(format!("invalid probability `{argument}`")))?;
788    if !(0.0..=1.0).contains(&probability) {
789        return Err(stmt.err(format!(
790            "probability must be finite and in [0, 1], got `{argument}`"
791        )));
792    }
793    Ok((name, Some(probability)))
794}
795
796fn reject_probability_argument(
797    stmt: &Stmt<'_>,
798    token: &str,
799    probability: Option<f64>,
800) -> Result<()> {
801    if probability.is_some() {
802        return Err(stmt.err(format!("instruction `{token}` does not take a probability")));
803    }
804    Ok(())
805}
806
807fn require_probability_argument(
808    stmt: &Stmt<'_>,
809    token: &str,
810    probability: Option<f64>,
811) -> Result<f64> {
812    probability.ok_or_else(|| stmt.err(format!("instruction `{token}` requires a probability")))
813}
814
815fn parse_qubit_targets(stmt: &Stmt<'_>) -> Result<Vec<IVec2>> {
816    let mut qubits = Vec::new();
817    while let Some(token) = stmt.peek() {
818        stmt.advance();
819        qubits.push(parse_coord2(stmt, token)?);
820    }
821    Ok(qubits)
822}
823
824/// One `CPAULI` correction: `P[control]@(x,y)` with control `mN` or `vN`.
825fn parse_correction(stmt: &Stmt<'_>, token: &str) -> Result<ConditionalCorrection> {
826    let error = || {
827        stmt.err(format!(
828            "malformed correction `{token}`, expected `P[c]@(x,y)`"
829        ))
830    };
831    let (pauli, rest) = token.split_once('[').ok_or_else(error)?;
832    let (control, target) = rest.split_once("]@").ok_or_else(error)?;
833    let control = parse_prefixed(stmt, control, 'm')?;
834    Ok(ConditionalCorrection {
835        pauli: parse_pauli_basis(stmt, pauli)?,
836        control,
837        target: parse_coord2(stmt, target)?,
838    })
839}
840
841fn parse_flow(stmt: &Stmt<'_>) -> Result<Flow> {
842    let start = parse_pauli_map(stmt, stmt.next_token("flow start")?)?;
843    stmt.expect("->")?;
844    let end = parse_pauli_map(stmt, stmt.next_token("flow end")?)?;
845    let mut flow = Flow::new(start, end);
846    while let Some(token) = stmt.peek() {
847        stmt.advance();
848        match token {
849            "meas" => {
850                let list = stmt.next_token("flow measurements")?;
851                let mut measurements = Vec::new();
852                for entry in list.split('*') {
853                    measurements.push(parse_prefixed(stmt, entry, 'm')?);
854                }
855                flow = flow.with_measurements(measurements);
856            }
857            "center" => {
858                let center = parse_coord2(stmt, stmt.next_token("flow center")?)?;
859                flow = flow.with_center(center);
860            }
861            "sign" => {
862                stmt.expect("-1")?;
863                flow = flow.with_sign(true);
864            }
865            "discard" => flow = flow.with_marker(FlowMarker::Discard),
866            "restart" => flow = flow.with_marker(FlowMarker::Restart),
867            other => return Err(stmt.err(format!("unknown flow attribute `{other}`"))),
868        }
869    }
870    Ok(flow)
871}
872
873// ==============================================================================
874// Graph levels
875// ==============================================================================
876
877impl<'a> Parser<'a> {
878    /// Parse the statements of one graph level up to its closing `}` and
879    /// materialize the level with exact node ids: ids missing below the last
880    /// live one become vacant slots (placeholder nodes added, then removed).
881    fn parse_level(&mut self) -> Result<SubGraph> {
882        let mut nodes: Vec<(u32, BloqNode)> = Vec::new();
883        // Line numbers parallel to `nodes`/`edges`, to span rebuild errors.
884        let mut node_lines: Vec<(u32, usize)> = Vec::new();
885        let mut edges: Vec<(u32, u32, BloqEdge)> = Vec::new();
886        let mut edge_lines: Vec<usize> = Vec::new();
887        let mut value_output = None;
888        let mut boundary_outputs = None;
889
890        loop {
891            let stmt = self.expect_stmt("a node, an edge, or `}`")?;
892            if stmt.is_close() {
893                break;
894            }
895            let head = stmt.next_token("node or edge")?;
896            if head == "result" {
897                if value_output.is_some() {
898                    return Err(stmt.err("duplicate result declaration"));
899                }
900                let output = stmt.next_token("result node or false")?;
901                value_output = Some(if output == "false" {
902                    None
903                } else {
904                    Some(parse_value_ref(&stmt, output)?)
905                });
906                stmt.finish()?;
907                continue;
908            }
909            if head == "bindings" {
910                if boundary_outputs.is_some() {
911                    return Err(stmt.err("duplicate bindings declaration"));
912                }
913                let mut outputs = Vec::new();
914                while let Some(output) = stmt.peek() {
915                    outputs.push(BloqNodeId(parse_prefixed(&stmt, output, 'n')?));
916                    stmt.advance();
917                }
918                boundary_outputs = Some(outputs);
919                continue;
920            }
921            let id = parse_prefixed(&stmt, head, 'n')?;
922            if stmt.peek() == Some("->") {
923                stmt.advance();
924                let target = parse_prefixed(&stmt, stmt.next_token("edge target")?, 'n')?;
925                edge_lines.push(stmt.line);
926                edges.push((id, target, parse_edge(&stmt)?));
927            } else {
928                node_lines.push((id, stmt.line));
929                let node = self.parse_node(&stmt)?;
930                nodes.push((id, node));
931            }
932        }
933
934        // The shared rebuild owns validation (duplicate ids, edge endpoints,
935        // the vacancy cap); map its errors back to source lines here.
936        let mut level = crate::ir::rebuild_level(nodes, edges).map_err(|error| {
937            use crate::ir::LevelRebuildError;
938            let line = match error {
939                // The second occurrence is the offending statement.
940                LevelRebuildError::DuplicateNode(id) => node_lines
941                    .iter()
942                    .filter(|(node, _)| *node == id)
943                    .map(|&(_, line)| line)
944                    .nth(1),
945                LevelRebuildError::MissingEndpoint { index, .. } => edge_lines.get(index).copied(),
946                // The node with the largest id is what stretched the id space.
947                LevelRebuildError::ExcessiveVacancy { .. } => node_lines
948                    .iter()
949                    .max_by_key(|(node, _)| *node)
950                    .map(|&(_, line)| line),
951            };
952            let line = line.unwrap_or(self.lines.len());
953            TextParseError::new(line, error)
954        })?;
955        level.set_value_output(value_output.flatten());
956        level.set_boundary_outputs(boundary_outputs.unwrap_or_default());
957        Ok(level)
958    }
959
960    fn parse_node(&mut self, stmt: &Stmt<'_>) -> Result<BloqNode> {
961        let kind = stmt.next_token("node kind")?;
962        match kind {
963            "quantum" => {
964                stmt.expect("{")?;
965                stmt.finish()?;
966                self.parse_quantum_node()
967            }
968            "rus" => self.parse_region_node(stmt),
969            _ => parse_classical_node(stmt, kind),
970        }
971    }
972
973    fn parse_quantum_node(&mut self) -> Result<BloqNode> {
974        let mut quantum = QuantumNode::default();
975        let mut provenance = NodeProvenance::None;
976        loop {
977            let stmt = self.expect_stmt("a quantum-node statement or `}`")?;
978            if stmt.is_close() {
979                break;
980            }
981            let keyword = stmt.next_token("quantum-node statement")?;
982            match keyword {
983                "instance" => {
984                    let id = parse_prefixed(&stmt, stmt.next_token("instance id")?, 'i')?;
985                    let template = parse_prefixed(&stmt, stmt.next_token("template id")?, 't')?;
986                    stmt.expect("@")?;
987                    let offset = parse_coord2(&stmt, stmt.next_token("instance offset")?)?;
988                    let provenance = if stmt.peek() == Some("from") {
989                        stmt.advance();
990                        match stmt.next_token("instance source")? {
991                            "block" => crate::InstanceProvenance::Block {
992                                source: parse_coord3(&stmt, stmt.next_token("source block")?)?,
993                            },
994                            "pipe" => crate::InstanceProvenance::Pipe {
995                                src: parse_coord3(&stmt, stmt.next_token("pipe source")?)?,
996                                dst: parse_coord3(&stmt, stmt.next_token("pipe destination")?)?,
997                            },
998                            "spatial-port" => {
999                                let role = parse_port_role(&stmt)?;
1000                                let part = match stmt
1001                                    .next_token("spatial Port substitution part")?
1002                                {
1003                                    "cube" => crate::SpatialPortPart::Cube,
1004                                    "temporal-port" => crate::SpatialPortPart::TemporalPort,
1005                                    other => {
1006                                        return Err(stmt
1007                                            .err(format!("unknown spatial Port part `{other}`")));
1008                                    }
1009                                };
1010                                let source =
1011                                    parse_coord3(&stmt, stmt.next_token("source spatial Port")?)?;
1012                                crate::InstanceProvenance::SpatialPortSubstitution {
1013                                    source,
1014                                    role,
1015                                    part,
1016                                }
1017                            }
1018                            other => {
1019                                return Err(stmt.err(format!("unknown instance source `{other}`")));
1020                            }
1021                        }
1022                    } else {
1023                        crate::InstanceProvenance::Source
1024                    };
1025                    stmt.finish()?;
1026                    quantum.instances.push(TemplateInstance {
1027                        id: TemplateInstanceId(id),
1028                        template_id: TemplateId(template),
1029                        offset,
1030                        provenance,
1031                    });
1032                }
1033                "detector" => {
1034                    let parity = parse_parity(
1035                        &stmt,
1036                        stmt.next_token("detector parity")?,
1037                        parse_instance_measurement,
1038                    )?;
1039                    let coords = parse_opt_detector_coords(&stmt)?;
1040                    stmt.finish()?;
1041                    quantum.detectors.push(NodeDetector { parity, coords });
1042                }
1043                "use" => {
1044                    let bundle = DetectorBundleId(parse_prefixed(
1045                        &stmt,
1046                        stmt.next_token("bundle id")?,
1047                        'b',
1048                    )?);
1049                    let mut instances = Vec::new();
1050                    while stmt.peek() != Some("@") {
1051                        instances.push(TemplateInstanceId(parse_prefixed(
1052                            &stmt,
1053                            stmt.next_token("owner instance or `@`")?,
1054                            'i',
1055                        )?));
1056                    }
1057                    stmt.expect("@")?;
1058                    let offset = parse_coord2(&stmt, stmt.next_token("bundle offset")?)?;
1059                    stmt.finish()?;
1060                    quantum.detector_bundles.push(DetectorBundleUse {
1061                        bundle,
1062                        instances,
1063                        offset,
1064                    });
1065                }
1066                "restart" => {
1067                    let parity = parse_parity(
1068                        &stmt,
1069                        stmt.next_token("restart parity")?,
1070                        parse_instance_measurement,
1071                    )?;
1072                    stmt.finish()?;
1073                    quantum.restarts.push(NodeRestart { parity });
1074                }
1075                "timeline" => {
1076                    stmt.expect("rounds")?;
1077                    let mut layer_round_ends = Vec::new();
1078                    while let Some(end) = stmt.peek() {
1079                        stmt.advance();
1080                        layer_round_ends.push(parse_int(&stmt, end)?);
1081                    }
1082                    if quantum
1083                        .timeline
1084                        .replace(QuantumTimeline { layer_round_ends })
1085                        .is_some()
1086                    {
1087                        return Err(stmt.err("duplicate quantum timeline"));
1088                    }
1089                    stmt.finish()?;
1090                }
1091                "guard" => {
1092                    let input = parse_int(&stmt, stmt.next_token("membership input slot")?)?;
1093                    let mut guard = crate::QuantumGuard {
1094                        input,
1095                        instances: Vec::new(),
1096                        ..Default::default()
1097                    };
1098                    while let Some(token) = stmt.peek() {
1099                        stmt.advance();
1100                        match token.as_bytes().first() {
1101                            Some(b'i') => guard
1102                                .instances
1103                                .push(TemplateInstanceId(parse_prefixed(&stmt, token, 'i')?)),
1104                            Some(b'd') => guard.detectors.push(parse_prefixed(&stmt, token, 'd')?),
1105                            Some(b'b') => guard
1106                                .detector_bundles
1107                                .push(parse_prefixed(&stmt, token, 'b')?),
1108                            Some(b'r') => guard.restarts.push(parse_prefixed(&stmt, token, 'r')?),
1109                            Some(b'x') => {
1110                                let (target, parity) = token[1..]
1111                                    .split_once('=')
1112                                    .ok_or_else(|| stmt.err("expected xdN=parity or xrN=parity"))?;
1113                                let parity =
1114                                    parse_parity(&stmt, parity, parse_instance_measurement)?;
1115                                if target.starts_with('d') {
1116                                    guard
1117                                        .detector_parities
1118                                        .push((parse_prefixed(&stmt, target, 'd')?, parity));
1119                                } else {
1120                                    guard
1121                                        .restart_parities
1122                                        .push((parse_prefixed(&stmt, target, 'r')?, parity));
1123                                }
1124                            }
1125                            _ => {
1126                                return Err(stmt.err(
1127                                    "expected guarded instance iN, detector dN, bundle use bN, or restart rN",
1128                                ));
1129                            }
1130                        }
1131                    }
1132                    stmt.finish()?;
1133                    quantum.guards.push(guard);
1134                }
1135                "from" => {
1136                    provenance = parse_provenance(&stmt)?;
1137                    stmt.finish()?;
1138                }
1139                other => {
1140                    return Err(stmt.err(format!("unknown quantum-node statement `{other}`")));
1141                }
1142            }
1143        }
1144        Ok(BloqNode::quantum(quantum).with_provenance(provenance))
1145    }
1146
1147    fn parse_region_node(&mut self, stmt: &Stmt<'_>) -> Result<BloqNode> {
1148        // Header: `<expr> [source nK] {` — the expression runs until a
1149        // terminator keyword. `source` binds body-local restart inputs.
1150        let expr = parse_expr_until(stmt, &["source", "when", "{"])?;
1151        let restart_source = if stmt.peek() == Some("source") {
1152            stmt.advance();
1153            Some(parse_value_ref(
1154                stmt,
1155                stmt.next_token("restart source node")?,
1156            )?)
1157        } else {
1158            None
1159        };
1160        let activation = if stmt.peek() == Some("when") {
1161            stmt.advance();
1162            Some(parse_prefixed(
1163                stmt,
1164                stmt.next_token("activation input")?,
1165                'v',
1166            )?)
1167        } else {
1168            None
1169        };
1170        stmt.expect("{")?;
1171        stmt.finish()?;
1172
1173        let mut provenance = NodeProvenance::None;
1174        let mut next = self.expect_stmt("a region body or `}`")?;
1175        if next.peek() == Some("from") {
1176            next.advance();
1177            provenance = parse_provenance(&next)?;
1178            next.finish()?;
1179            next = self.expect_stmt("a region body or `}`")?;
1180        }
1181
1182        let region = RegionNode::RepeatUntilSuccess {
1183            body: self.parse_body_block(&next, "body")?,
1184            restart_condition: expr,
1185            restart_source,
1186        };
1187
1188        let close = self.expect_stmt("`}` closing the region node")?;
1189        if !close.is_close() {
1190            return Err(close.err("expected `}` closing the region node"));
1191        }
1192        Ok(BloqNode {
1193            activation,
1194            kind: BloqNodeKind::Region(region),
1195            provenance,
1196        })
1197    }
1198
1199    /// One region body block: `<name> { <level> }`.
1200    fn parse_body_block(&mut self, stmt: &Stmt<'_>, name: &str) -> Result<SubGraph> {
1201        stmt.expect(name)?;
1202        stmt.expect("{")?;
1203        stmt.finish()?;
1204        self.parse_level()
1205    }
1206}
1207
1208fn parse_classical_node(stmt: &Stmt<'_>, kind: &str) -> Result<BloqNode> {
1209    let classical = match kind {
1210        "compute" => ClassicalNode::Compute {
1211            expr: parse_expr_until(stmt, &["when", "from"])?,
1212        },
1213        "observable" => {
1214            let token = stmt.next_token("observable index or fragment")?;
1215            let index = if token == "fragment" {
1216                None
1217            } else {
1218                Some(parse_int(stmt, token)?)
1219            };
1220            let measurements = if stmt.peek() == Some("measurements") {
1221                stmt.advance();
1222                parse_classical_measurements(stmt)?
1223            } else {
1224                Vec::new()
1225            };
1226            let operators = if stmt.peek() == Some("operators") {
1227                stmt.advance();
1228                parse_classical_operators(stmt)?
1229            } else {
1230                Vec::new()
1231            };
1232            ClassicalNode::Observable {
1233                index,
1234                measurements,
1235                operators,
1236            }
1237        }
1238        "discard" => ClassicalNode::Discard {
1239            condition: parse_expr_until(stmt, &["when", "from"])?,
1240        },
1241        other => return Err(stmt.err(format!("unknown node kind `{other}`"))),
1242    };
1243
1244    let activation = if stmt.peek() == Some("when") {
1245        stmt.advance();
1246        Some(parse_prefixed(stmt, stmt.next_token("guard input")?, 'v')?)
1247    } else {
1248        None
1249    };
1250    let provenance = if stmt.peek() == Some("from") {
1251        stmt.advance();
1252        parse_provenance(stmt)?
1253    } else {
1254        NodeProvenance::None
1255    };
1256    stmt.finish()?;
1257    Ok(BloqNode {
1258        activation,
1259        kind: BloqNodeKind::Classical(classical.into()),
1260        provenance,
1261    })
1262}
1263
1264fn parse_classical_measurements(stmt: &Stmt<'_>) -> Result<Vec<InstanceMeasurement>> {
1265    let Some(token) = stmt.peek() else {
1266        return Ok(Vec::new());
1267    };
1268    if matches!(token, "operators" | "from" | "when") {
1269        return Ok(Vec::new());
1270    }
1271    stmt.advance();
1272    token
1273        .split('*')
1274        .map(|entry| parse_instance_measurement(stmt, entry))
1275        .collect()
1276}
1277
1278fn parse_classical_operators(stmt: &Stmt<'_>) -> Result<Vec<InstanceBoundaryOperator>> {
1279    // A trailing comma on a Pauli-map token means another operator follows.
1280    let mut operators = Vec::new();
1281    while !matches!(stmt.peek(), None | Some("from" | "when")) {
1282        let instance =
1283            TemplateInstanceId(parse_prefixed(stmt, stmt.next_token("instance id")?, 'i')?);
1284        let face = match stmt.next_token("`input` or `output`")? {
1285            "input" => BoundaryFace::Input,
1286            "output" => BoundaryFace::Output,
1287            other => return Err(stmt.err(format!("unknown boundary face `{other}`"))),
1288        };
1289        let token = stmt.next_token("boundary operator")?;
1290        let (map_token, more) = token
1291            .strip_suffix(',')
1292            .map_or((token, false), |rest| (rest, true));
1293        operators.push(InstanceBoundaryOperator {
1294            instance,
1295            face,
1296            operator: parse_pauli_map(stmt, map_token)?,
1297        });
1298        if !more {
1299            break;
1300        }
1301    }
1302    Ok(operators)
1303}
1304
1305fn parse_edge(stmt: &Stmt<'_>) -> Result<BloqEdge> {
1306    let edge = match stmt.next_token("edge kind")? {
1307        "order" => BloqEdge::Order,
1308        kind @ ("value" | "compose") => {
1309            let slot = parse_int(stmt, stmt.next_token("input slot")?)?;
1310            let output = if kind == "value" && matches!(stmt.peek(), Some("corrected" | "flip")) {
1311                parse_output(stmt, stmt.next_token("output port")?)?
1312            } else {
1313                ObservableOutput::Corrected
1314            };
1315            let role = match stmt.peek() {
1316                Some("feedback") => {
1317                    stmt.advance();
1318                    ValueRole::FeedbackFold {
1319                        action: parse_int(stmt, stmt.next_token("feedback action")?)?,
1320                    }
1321                }
1322                Some("readout") => {
1323                    stmt.advance();
1324                    ValueRole::ReadoutFold
1325                }
1326                _ => ValueRole::Data,
1327            };
1328            if kind == "compose" {
1329                BloqEdge::Compose { slot, role }
1330            } else {
1331                BloqEdge::Value { slot, role, output }
1332            }
1333        }
1334        "quantum" => {
1335            let mut pipes = Vec::new();
1336            while let Some(token) = stmt.peek() {
1337                if token == "when" {
1338                    break;
1339                }
1340                stmt.advance();
1341                let pipe = parse_pipe(stmt, token)?;
1342                // Each pipe's padding, when recorded, immediately follows it.
1343                let padding = if stmt.peek() == Some("padding") {
1344                    stmt.advance();
1345                    stmt.expect("offset")?;
1346                    let offset = parse_coord2(stmt, stmt.next_token("padding offset")?)?;
1347                    stmt.expect("one")?;
1348                    let one_round = parse_prefixed(stmt, stmt.next_token("template id")?, 't')?;
1349                    stmt.expect("loop")?;
1350                    let looped = parse_prefixed(stmt, stmt.next_token("template id")?, 't')?;
1351                    Some(PipePadding {
1352                        offset,
1353                        one_round: TemplateId(one_round),
1354                        looped: TemplateId(looped),
1355                    })
1356                } else {
1357                    None
1358                };
1359                pipes.push(PipeSeam { pipe, padding });
1360            }
1361            let guard = if stmt.peek() == Some("when") {
1362                stmt.advance();
1363                Some(parse_value_ref(
1364                    stmt,
1365                    stmt.next_token("seam guard producer")?,
1366                )?)
1367            } else {
1368                None
1369            };
1370            BloqEdge::Quantum(Box::new(QuantumEdge { pipes, guard }))
1371        }
1372        other => return Err(stmt.err(format!("unknown edge kind `{other}`"))),
1373    };
1374    stmt.finish()?;
1375    Ok(edge)
1376}
1377
1378fn parse_output(stmt: &Stmt<'_>, token: &str) -> Result<ObservableOutput> {
1379    match token {
1380        "corrected" => Ok(ObservableOutput::Corrected),
1381        "flip" => Ok(ObservableOutput::Flip),
1382        _ => Err(stmt.err(format!("unknown output port `{token}`"))),
1383    }
1384}
1385
1386fn parse_value_ref(stmt: &Stmt<'_>, token: &str) -> Result<ValueRef> {
1387    let (node, output) = match token.split_once(':') {
1388        Some((node, output)) => (node, parse_output(stmt, output)?),
1389        None => (token, ObservableOutput::Corrected),
1390    };
1391    Ok(ValueRef {
1392        node: BloqNodeId(parse_prefixed(stmt, node, 'n')?),
1393        output,
1394    })
1395}
1396
1397// ==============================================================================
1398// Provenance
1399// ==============================================================================
1400
1401fn parse_provenance(stmt: &Stmt<'_>) -> Result<NodeProvenance> {
1402    let provenance = match stmt.next_token("provenance kind")? {
1403        "blocks" => {
1404            let mut members = Vec::new();
1405            while let Some(token) = stmt.peek() {
1406                stmt.advance();
1407                members.push(SourceBlockRef {
1408                    pos: parse_coord3(stmt, token)?,
1409                });
1410            }
1411            NodeProvenance::BlockComponent { members }
1412        }
1413        "pipe" => NodeProvenance::TemporalPipe {
1414            pipe: parse_pipe(stmt, stmt.next_token("pipe")?)?,
1415        },
1416        "spatial-port" => {
1417            let role = parse_port_role(stmt)?;
1418            let source = parse_coord3(stmt, stmt.next_token("source spatial Port")?)?;
1419            NodeProvenance::SpatialPortSubstitution { source, role }
1420        }
1421        "padding" => {
1422            let pipe = parse_pipe(stmt, stmt.next_token("pipe")?)?;
1423            stmt.expect("rounds")?;
1424            let rounds = parse_int(stmt, stmt.next_token("round count")?)?;
1425            NodeProvenance::MemoryPadding { pipe, rounds }
1426        }
1427        "generator" => NodeProvenance::Generator {
1428            ordinal: parse_int(stmt, stmt.next_token("generator ordinal")?)?,
1429        },
1430        "action" => NodeProvenance::Action {
1431            ordinal: parse_int(stmt, stmt.next_token("action ordinal")?)?,
1432        },
1433        "selector" => NodeProvenance::BranchSelector {
1434            name: decode_metadata_token(stmt.next_token("branch name")?)
1435                .map_err(|message| stmt.err(message))?,
1436        },
1437        "frame" => {
1438            let basis = match stmt.next_token("frame basis")? {
1439                "x" => Basis::X,
1440                "z" => Basis::Z,
1441                other => return Err(stmt.err(format!("unknown frame basis `{other}`"))),
1442            };
1443            let port = parse_coord3(stmt, stmt.next_token("frame output port")?)?;
1444            NodeProvenance::OutputFrame { port, basis }
1445        }
1446        other => return Err(stmt.err(format!("unknown provenance kind `{other}`"))),
1447    };
1448    Ok(provenance)
1449}
1450
1451// ==============================================================================
1452// Token-level parsers
1453// ==============================================================================
1454
1455/// `prefix(args)` → `Some(args)`, e.g. `call_args("body(b1)", "body")` →
1456/// `Some("b1")`.
1457fn call_args<'a>(token: &'a str, name: &str) -> Option<&'a str> {
1458    token
1459        .strip_prefix(name)?
1460        .strip_prefix('(')?
1461        .strip_suffix(')')
1462}
1463
1464/// Split comma-separated call arguments into exactly `N` pieces.
1465fn parse_call_args<'a, const N: usize>(stmt: &Stmt<'_>, args: &'a str) -> Result<[&'a str; N]> {
1466    let pieces: Vec<&str> = args.split(',').collect();
1467    <[&str; N]>::try_from(pieces).map_err(|pieces: Vec<&str>| {
1468        stmt.err(format!("expected {N} argument(s), found {}", pieces.len()))
1469    })
1470}
1471
1472fn parse_int<T: FromStr>(stmt: &Stmt<'_>, token: &str) -> Result<T> {
1473    token
1474        .parse()
1475        .map_err(|_| stmt.err(format!("invalid number `{token}`")))
1476}
1477
1478/// `x123` → `123` for the id sigils (`n`, `t`, `i`, `m`, `b`, `s`). Digits
1479/// only — `u32::from_str` would also accept a leading `+`, which the writer
1480/// never emits.
1481fn parse_prefixed(stmt: &Stmt<'_>, token: &str, prefix: char) -> Result<u32> {
1482    token
1483        .strip_prefix(prefix)
1484        .filter(|digits| !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()))
1485        .and_then(|digits| digits.parse().ok())
1486        .ok_or_else(|| stmt.err(format!("expected `{prefix}<N>`, found `{token}`")))
1487}
1488
1489/// The next token as a spatial-Port role, shared by the node-level and
1490/// instance-level `spatial-port` provenance forms so both report it alike.
1491fn parse_port_role(stmt: &Stmt<'_>) -> Result<crate::PortRole> {
1492    stmt.next_token("spatial Port role")?
1493        .parse()
1494        .map_err(|_| stmt.err("invalid spatial Port role"))
1495}
1496
1497fn parse_measurement_output(stmt: &Stmt<'_>, token: &str) -> Result<u32> {
1498    let measurement = parse_prefixed(stmt, token, 'm')?;
1499    if measurement == u32::MAX {
1500        return Err(stmt.err(format!(
1501            "measurement id m{measurement} exhausts the measurement id space"
1502        )));
1503    }
1504    Ok(measurement)
1505}
1506
1507fn parse_coord2(stmt: &Stmt<'_>, token: &str) -> Result<IVec2> {
1508    let [x, y] = parse_int_tuple(stmt, token)?;
1509    Ok(IVec2::new(x, y))
1510}
1511
1512fn parse_coord3(stmt: &Stmt<'_>, token: &str) -> Result<IVec3> {
1513    let [x, y, z] = parse_int_tuple(stmt, token)?;
1514    Ok(IVec3::new(x, y, z))
1515}
1516
1517fn parse_int_tuple<const N: usize>(stmt: &Stmt<'_>, token: &str) -> Result<[i32; N]> {
1518    let inner = token
1519        .strip_prefix('(')
1520        .and_then(|rest| rest.strip_suffix(')'))
1521        .ok_or_else(|| stmt.err(format!("expected a coordinate, found `{token}`")))?;
1522    let mut out = [0i32; N];
1523    let mut pieces = inner.split(',');
1524    for slot in &mut out {
1525        let piece = pieces
1526            .next()
1527            .ok_or_else(|| stmt.err(format!("coordinate `{token}` has too few components")))?;
1528        *slot = parse_int(stmt, piece)?;
1529    }
1530    if pieces.next().is_some() {
1531        return Err(stmt.err(format!("coordinate `{token}` has too many components")));
1532    }
1533    Ok(out)
1534}
1535
1536/// `(x,y,z)>(x,y,z)` with an optional trailing `h` for a hadamard pipe.
1537fn parse_pipe(stmt: &Stmt<'_>, token: &str) -> Result<TemporalPipeRef> {
1538    let (token, hadamard) = token
1539        .strip_suffix('h')
1540        .map_or((token, false), |rest| (rest, true));
1541    let (src, dst) = token
1542        .split_once('>')
1543        .ok_or_else(|| stmt.err(format!("expected a pipe `(..)>(..)`, found `{token}`")))?;
1544    Ok(TemporalPipeRef {
1545        src: parse_coord3(stmt, src)?,
1546        dst: parse_coord3(stmt, dst)?,
1547        hadamard,
1548    })
1549}
1550
1551fn parse_pauli(stmt: &Stmt<'_>, token: &str) -> Result<Pauli> {
1552    Pauli::from_str(token).map_err(|_| stmt.err(format!("invalid Pauli `{token}`")))
1553}
1554
1555fn parse_pauli_basis(stmt: &Stmt<'_>, token: &str) -> Result<PauliBasis> {
1556    PauliBasis::from_str(token).map_err(|_| stmt.err(format!("invalid Pauli basis `{token}`")))
1557}
1558
1559/// `X(0,0)*Z(1,2)` (or `_` for the empty map).
1560fn parse_pauli_map(stmt: &Stmt<'_>, token: &str) -> Result<PauliMap> {
1561    if token == "_" {
1562        return Ok(PauliMap::empty());
1563    }
1564    let mut map = PauliMap::empty();
1565    for entry in token.split('*') {
1566        let (pauli, coord) = entry
1567            .split_at_checked(1)
1568            .ok_or_else(|| stmt.err(format!("malformed Pauli map entry `{entry}`")))?;
1569        map.insert(parse_coord2(stmt, coord)?, parse_pauli(stmt, pauli)?);
1570    }
1571    Ok(map)
1572}
1573
1574fn parse_meas_term(stmt: &Stmt<'_>, token: &str) -> Result<u32> {
1575    parse_prefixed(stmt, token, 'm')
1576}
1577
1578/// `iN:mK`.
1579fn parse_instance_measurement(stmt: &Stmt<'_>, token: &str) -> Result<InstanceMeasurement> {
1580    let (instance, measurement) = token
1581        .split_once(':')
1582        .ok_or_else(|| stmt.err(format!("expected `iN:mK`, found `{token}`")))?;
1583    Ok(InstanceMeasurement {
1584        instance: TemplateInstanceId(parse_prefixed(stmt, instance, 'i')?),
1585        measurement: parse_prefixed(stmt, measurement, 'm')?,
1586    })
1587}
1588
1589/// `oN:mK`, where `oN` is a bundle-local owner slot.
1590fn parse_bundle_measurement(stmt: &Stmt<'_>, token: &str) -> Result<BundleMeasurement> {
1591    let (owner, measurement) = token
1592        .split_once(':')
1593        .ok_or_else(|| stmt.err(format!("expected `oN:mK`, found `{token}`")))?;
1594    Ok(BundleMeasurement {
1595        owner: parse_prefixed(stmt, owner, 'o')?,
1596        measurement: parse_prefixed(stmt, measurement, 'm')?,
1597    })
1598}
1599
1600/// Optional `-` sign followed by `term*term*…`, with terms `sN` (loop state)
1601/// or a measurement (via `measurement`); `0` is the empty parity.
1602fn parse_parity<M: Copy + Ord>(
1603    stmt: &Stmt<'_>,
1604    token: &str,
1605    measurement: impl Fn(&Stmt<'_>, &str) -> Result<M>,
1606) -> Result<DetectorParity<M>> {
1607    let (sign, token) = token
1608        .strip_prefix('-')
1609        .map_or((false, token), |token| (true, token));
1610    if token == "0" {
1611        return Ok(DetectorParity::default().with_sign(sign));
1612    }
1613    let mut terms = Vec::new();
1614    for entry in token.split('*') {
1615        // A loop-state term is `s<N>`; instance measurements contain `:` and
1616        // template measurements start with `m`, so the sigil disambiguates.
1617        if entry.starts_with('s') {
1618            terms.push(DetectorTerm::LoopState(LoopStateId(parse_prefixed(
1619                stmt, entry, 's',
1620            )?)));
1621        } else {
1622            terms.push(DetectorTerm::Measurement(measurement(stmt, entry)?));
1623        }
1624    }
1625    Ok(DetectorParity::from_terms(terms).with_sign(sign))
1626}
1627
1628/// Optional ` @ (c0,c1,…)` detector coordinates.
1629fn parse_opt_detector_coords(stmt: &Stmt<'_>) -> Result<Option<bloq_circuit::DetectorCoords>> {
1630    if stmt.peek() != Some("@") {
1631        return Ok(None);
1632    }
1633    stmt.advance();
1634    let token = stmt.next_token("detector coordinates")?;
1635    let inner = token
1636        .strip_prefix('(')
1637        .and_then(|rest| rest.strip_suffix(')'))
1638        .ok_or_else(|| stmt.err(format!("expected coordinates, found `{token}`")))?;
1639    let mut coords = bloq_circuit::DetectorCoords::new();
1640    if !inner.is_empty() {
1641        for piece in inner.split(',') {
1642            coords.push(
1643                piece
1644                    .parse()
1645                    .map_err(|_| stmt.err(format!("invalid coordinate `{piece}`")))?,
1646            );
1647        }
1648    }
1649    Ok(Some(coords))
1650}
1651
1652// ==============================================================================
1653// Classical expressions
1654// ==============================================================================
1655
1656/// Parse an expression from the statement's remaining tokens, stopping (without
1657/// consuming) at any of the `terminators` or at end of line.
1658fn parse_expr_until(stmt: &Stmt<'_>, terminators: &[&str]) -> Result<ClassicalExpr> {
1659    let mut source = String::new();
1660    while let Some(token) = stmt.peek() {
1661        if terminators.contains(&token) {
1662            break;
1663        }
1664        stmt.advance();
1665        source.push_str(token);
1666        source.push(' ');
1667    }
1668    parse_expr(stmt, &source)
1669}
1670
1671#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1672enum ExprToken {
1673    In(u32),
1674    Const(bool),
1675    Not,
1676    Xor,
1677    And,
1678    Or,
1679    XorList,
1680    AndList,
1681    OrList,
1682    Select,
1683    Parity,
1684    Comma,
1685    Open,
1686    Close,
1687}
1688
1689fn lex_expr(stmt: &Stmt<'_>, source: &str) -> Result<Vec<ExprToken>> {
1690    let mut tokens = Vec::new();
1691    let bytes = source.as_bytes();
1692    let mut index = 0;
1693    while index < bytes.len() {
1694        let rest = &source[index..];
1695        let (token, width) = match bytes[index] {
1696            b' ' => {
1697                index += 1;
1698                continue;
1699            }
1700            b'!' => (ExprToken::Not, 1),
1701            b'^' => (ExprToken::Xor, 1),
1702            b'&' => (ExprToken::And, 1),
1703            b'|' => (ExprToken::Or, 1),
1704            b',' => (ExprToken::Comma, 1),
1705            b'x' if rest.starts_with("xor") => (ExprToken::XorList, 3),
1706            b'a' if rest.starts_with("and") => (ExprToken::AndList, 3),
1707            b'o' if rest.starts_with("or") => (ExprToken::OrList, 2),
1708            b's' if rest.starts_with("select") => (ExprToken::Select, 6),
1709            b'p' if rest.starts_with("parity") => (ExprToken::Parity, 6),
1710            b'(' => (ExprToken::Open, 1),
1711            b')' => (ExprToken::Close, 1),
1712            b'0' => (ExprToken::Const(false), 1),
1713            b'1' => (ExprToken::Const(true), 1),
1714            _ => {
1715                let digits = rest
1716                    .strip_prefix("in")
1717                    .map(|after| after.bytes().take_while(u8::is_ascii_digit).count())
1718                    .filter(|&len| len > 0)
1719                    .ok_or_else(|| stmt.err(format!("invalid expression at `{rest}`")))?;
1720                let slot = rest[2..2 + digits]
1721                    .parse()
1722                    .map_err(|_| stmt.err(format!("invalid input slot in `{rest}`")))?;
1723                (ExprToken::In(slot), 2 + digits)
1724            }
1725        };
1726        tokens.push(token);
1727        index += width;
1728    }
1729    Ok(tokens)
1730}
1731
1732/// Precedence-climbing over the lexed tokens; mirrors the writer's levels
1733/// (`|` < `^` < `&` < `!`).
1734struct ExprParser<'s, 'a> {
1735    stmt: &'s Stmt<'a>,
1736    tokens: Vec<ExprToken>,
1737    next: usize,
1738}
1739
1740fn parse_expr(stmt: &Stmt<'_>, source: &str) -> Result<ClassicalExpr> {
1741    let mut parser = ExprParser {
1742        stmt,
1743        tokens: lex_expr(stmt, source)?,
1744        next: 0,
1745    };
1746    let expr = parser.parse_or()?;
1747    if parser.next != parser.tokens.len() {
1748        return Err(stmt.err("trailing tokens in expression"));
1749    }
1750    Ok(expr)
1751}
1752
1753impl ExprParser<'_, '_> {
1754    fn peek(&self) -> Option<ExprToken> {
1755        self.tokens.get(self.next).copied()
1756    }
1757
1758    fn parse_or(&mut self) -> Result<ClassicalExpr> {
1759        self.parse_binop(ExprToken::Or, ClassicalExpr::Or, Self::parse_xor)
1760    }
1761
1762    fn parse_xor(&mut self) -> Result<ClassicalExpr> {
1763        self.parse_binop(ExprToken::Xor, ClassicalExpr::Xor, Self::parse_and)
1764    }
1765
1766    fn parse_and(&mut self) -> Result<ClassicalExpr> {
1767        self.parse_binop(ExprToken::And, ClassicalExpr::And, Self::parse_not)
1768    }
1769
1770    /// One flat precedence level: `next (token next)*`.
1771    fn parse_binop(
1772        &mut self,
1773        token: ExprToken,
1774        build: fn(Box<[ClassicalExpr]>) -> ClassicalExpr,
1775        next: fn(&mut Self) -> Result<ClassicalExpr>,
1776    ) -> Result<ClassicalExpr> {
1777        let first = next(self)?;
1778        if self.peek() != Some(token) {
1779            return Ok(first);
1780        }
1781        let mut operands = vec![first];
1782        while self.peek() == Some(token) {
1783            self.next += 1;
1784            operands.push(next(self)?);
1785        }
1786        Ok(build(operands.into_boxed_slice()))
1787    }
1788
1789    fn parse_not(&mut self) -> Result<ClassicalExpr> {
1790        if self.peek() == Some(ExprToken::Not) {
1791            self.next += 1;
1792            return Ok(ClassicalExpr::Not(Box::new(self.parse_not()?)));
1793        }
1794        self.parse_atom()
1795    }
1796
1797    fn parse_atom(&mut self) -> Result<ClassicalExpr> {
1798        let token = self
1799            .peek()
1800            .ok_or_else(|| self.stmt.err("expression ended unexpectedly"))?;
1801        self.next += 1;
1802        match token {
1803            ExprToken::In(slot) => Ok(ClassicalExpr::In(slot)),
1804            ExprToken::Const(bit) => Ok(ClassicalExpr::Const(bit)),
1805            ExprToken::XorList
1806            | ExprToken::AndList
1807            | ExprToken::OrList
1808            | ExprToken::Select
1809            | ExprToken::Parity => {
1810                if self.peek() != Some(ExprToken::Open) {
1811                    return Err(self.stmt.err("expected `(` after expression function"));
1812                }
1813                self.next += 1;
1814                let mut operands = Vec::new();
1815                if self.peek() != Some(ExprToken::Close) {
1816                    loop {
1817                        operands.push(self.parse_or()?);
1818                        if self.peek() != Some(ExprToken::Comma) {
1819                            break;
1820                        }
1821                        self.next += 1;
1822                    }
1823                }
1824                if self.peek() != Some(ExprToken::Close) {
1825                    return Err(self.stmt.err("unclosed expression function"));
1826                }
1827                self.next += 1;
1828                Ok(match token {
1829                    ExprToken::XorList => ClassicalExpr::Xor(operands.into_boxed_slice()),
1830                    ExprToken::AndList => ClassicalExpr::And(operands.into_boxed_slice()),
1831                    ExprToken::OrList => ClassicalExpr::Or(operands.into_boxed_slice()),
1832                    ExprToken::Parity => {
1833                        let mut operands = operands.into_iter();
1834                        let Some(ClassicalExpr::Const(constant)) = operands.next() else {
1835                            return Err(self
1836                                .stmt
1837                                .err("parity expects a constant followed by input slots"));
1838                        };
1839                        let inputs = operands
1840                            .map(|operand| match operand {
1841                                ClassicalExpr::In(slot) => Ok(slot),
1842                                _ => Err(self.stmt.err("parity operands must be input slots")),
1843                            })
1844                            .collect::<Result<Box<[_]>>>()?;
1845                        ClassicalExpr::Parity { inputs, constant }
1846                    }
1847                    _ => ClassicalExpr::Select(Box::new(operands.try_into().map_err(|_| {
1848                        self.stmt
1849                            .err("select expects condition, when_false, when_true")
1850                    })?)),
1851                })
1852            }
1853            ExprToken::Open => {
1854                let expr = self.parse_or()?;
1855                if self.peek() != Some(ExprToken::Close) {
1856                    return Err(self.stmt.err("unclosed `(` in expression"));
1857                }
1858                self.next += 1;
1859                Ok(expr)
1860            }
1861            _ => Err(self.stmt.err("expected an expression atom")),
1862        }
1863    }
1864}