Skip to content

Stim parser

stim_parser

T = TypeVar('T') module-attribute

NEWLINE = re.compile('\\n') module-attribute

NOT_NEWLINE = re.compile('[^\\n]*') module-attribute

INDENT = re.compile('[ \\t]*') module-attribute

SPACE = re.compile('[ \\t]') module-attribute

ARG = re.compile('\\d+(\\.\\d+)?') module-attribute

TARGET = re.compile('\\d+') module-attribute

NAME = re.compile('[a-zA-Z][a-zA-Z0-9_]+') module-attribute

A regular expression for a name: a letter followed by a number of letters or numbers or underscores.

StimParseError

Bases: Exception

Source code in xdsl/dialects/stim/stim_parser.py
18
19
20
21
22
23
24
25
class StimParseError(Exception):
    position: Position
    message: str

    def __init__(self, position: Position, message: str) -> None:
        self.position = position
        self.message = message
        super().__init__(f"StimParseError at {self.position}: {self.message}")

position: Position = position instance-attribute

message: str = message instance-attribute

__init__(position: Position, message: str) -> None

Source code in xdsl/dialects/stim/stim_parser.py
22
23
24
25
def __init__(self, position: Position, message: str) -> None:
    self.position = position
    self.message = message
    super().__init__(f"StimParseError at {self.position}: {self.message}")

Instruction

Bases: StrEnum

Enum for the parse-able instructions in Stim.

Source code in xdsl/dialects/stim/stim_parser.py
28
29
30
31
32
33
class Instruction(StrEnum):
    """
    Enum for the parse-able instructions in Stim.
    """

    COORD = "QUBIT_COORDS"

COORD = 'QUBIT_COORDS' class-attribute instance-attribute

StimParser

Source code in xdsl/dialects/stim/stim_parser.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
class StimParser:
    input: Input
    pos: int

    def __init__(self, input: Input | str, pos: int = 0):
        if isinstance(input, str):
            input = Input(input, "<unknown>")
        self.input = input
        self.pos = pos

    @property
    def remaining(self) -> str:
        return self.input.content[self.pos :]

    # region Base parsing functions

    def parse_optional_chars(self, chars: str):
        if self.input.content.startswith(chars, self.pos):
            self.pos += len(chars)
            return chars

    def parse_optional_pattern(self, pattern: re.Pattern[str]):
        if (match := pattern.match(self.input.content, self.pos)) is not None:
            self.pos = match.regs[0][1]
            return self.input.content[match.pos : self.pos]

    # endregion

    # region Helpers
    def expect(self, message: str, parse: Callable[["StimParser"], T | None]) -> T:
        if (parsed := parse(self)) is None:
            raise StimParseError(self.pos, message)
        return parsed

    def parse_one_of(
        self, StimParsers: Sequence[Callable[["StimParser"], T | None]]
    ) -> T | None:
        for StimParser in StimParsers:
            if (parsed := StimParser(self)) is not None:
                return parsed

    # endregion

    def parse_circuit(self) -> StimCircuitOp:
        """
        Parse Stim dialect operations from a string formatted as a Stim file and return a StimCircuitOp
        containing the operations in its single block.

        Circuits have format:
            circuit         ::= (line)*

        Collect by operations instead of by line to skip any lines that are not converted into operations.
        """
        lines: list[Operation] = []
        while (op := self.parse_optional_operation()) is not None:
            lines.append(op)

        circuit_body = Region(Block(ops=lines))
        return StimCircuitOp(circuit_body, None)

    def parse_optional_comment(self) -> None:
        """
        Parse a comment if there, but this is not stored.

        Comments have format:
            comment ::= `#` NOT_NEWLINE
        """
        if self.parse_optional_chars("#") is None:
            return

        self.expect(
            "comment", lambda parser: parser.parse_optional_pattern(NOT_NEWLINE)
        )

    def _check_comment_and_newline(self) -> str | None:
        """
        Consume an optional comment and newline, returning if a newline has occured.
        """
        self.parse_optional_comment()
        return self.parse_optional_pattern(NEWLINE)

    def parse_optional_operation(
        self,
    ) -> Operation | None:
        """
        Stim lines have format:
            line ::= indent? (instruction | block_start | block_end)? (comment ::= `#' NotNewLine)? NEWLINE
        As this parser goes straight to operations, and stim comments, indentations and empty lines have no semantic meaning
        we look for operations, then skip straight through any lines without instructions or block starts or ends.

        Operations are given by instructions, or the a block containing instructions.
        """

        self.parse_optional_pattern(INDENT)
        op = self.parse_optional_instruction()  # TODO: add parsing for blocks

        # Skip comments and empty lines
        while self._check_comment_and_newline() is not None:
            pass
        return op

    # region Instruction parsing

    def parse_optional_instruction(self) -> Operation | None:
        """
        Parse instruction with format:
            instruction ::= name (parens_arguments)? targets
        """
        if (name := self.parse_optional_pattern(NAME)) is None:
            return None
        op = Instruction(name)
        parens = self.parse_optional_parens()
        if parens is None:
            parens = []
        targets = self.parse_targets()
        return self.build_operation(op, parens, targets)

    # region Parens parsing
    def parse_optional_paren(self):
        """
        Parse an argument passed to an instruction in parentheses with format:
            arg ::= double
        """
        self.parse_optional_pattern(INDENT)
        if (str_val := self.parse_optional_pattern(ARG)) is not None:
            arg = float(str_val)
            return arg

    def parse_optional_parens(self) -> list[float] | None:
        """
        Parse an optional parenthesis with optional arguments with format:
            parens ::= `(` (INDENT* arg INDENT* (',' INDENT* arg INDENT*)*)? `)`

        TODO: The Stim documentation uses this format:
            <PARENS_ARGUMENTS> ::= '(' <ARGUMENTS> ')'
            <ARGUMENTS> ::= /[ \t]*/ <ARG> /[ \t]*/ (',' <ARGUMENTS>)?

            but its implementation accepts empty arguments - this is kept here to be consistent.

        """
        # Parse the opening bracket.
        if self.parse_optional_chars("(") is None:
            return None
        if self.parse_optional_chars(")") is not None:
            return []
        # Check if an argument exists.
        args: list[float] = [
            self.expect("arg", lambda parser: parser.parse_optional_paren())
        ]
        # Until the closing bracket is found:
        while self.parse_optional_chars(")") is None:
            self.parse_optional_pattern(INDENT)
            self.expect("comma", lambda parser: parser.parse_optional_chars(","))
            arg = self.expect("arg", lambda parser: parser.parse_optional_paren())
            args.append(arg)

        return args

    # endregion

    # region Targets parsing

    def parse_optional_target(self) -> QubitAttr | None:
        """
        Parse a target with format:
            target ::= <QUBIT_TARGET> | <MEASUREMENT_RECORD_TARGET> | <SWEEP_BIT_TARGET> | <PAULI_TARGET> | <COMBINER_TARGET>
        TODO: Currently only supports target ::= qubit_target as the other relevant operations are not yet supported
        """
        # Check for a space
        space = self.parse_optional_pattern(SPACE)
        self.parse_optional_pattern(INDENT)
        if (str_val := self.parse_optional_pattern(TARGET)) is not None:
            if space is None:
                raise StimParseError(self.pos, "Targets must be separated by spacing.")
            target = int(str_val)
            return QubitAttr(target)

    def parse_targets(self) -> list[QubitAttr]:
        """
        Parse targets with format:
            targets = SPACE INDENTS target targets?
        TODO: the Stim documentation indicates that their parser requires at least one
        target per instruction - but their actual parser does not enforce this.
        Check incongruency.
        """
        # Check that there is a first target
        if (first := self.parse_optional_target()) is None:
            raise StimParseError(self.pos, "Expected at least one target")
        targets = [first]
        # Parse targets until no more are found
        while (target := self.parse_optional_target()) is not None:
            targets.append(target)
        return targets

    # endregion

    # region Build operation

    def build_parens(self, parens: list[float]) -> ArrayAttr[FloatData | IntAttr]:
        """
        Convert a list of parens into an ArrayAttr.
        """
        args = [
            (IntAttr(int(arg))) if (arg.is_integer()) else (FloatData(arg))
            for arg in parens
        ]
        coords = ArrayAttr(args)
        return coords

    def build_operation(
        self, op: Instruction, parens: list[float], targets: Sequence[QubitAttr]
    ):
        """
        Build the operation corresponding to the name, parens, and targets found by the parser.
        """
        match op:
            case Instruction.COORD:
                if targets == []:
                    # In line with the behaviour of the Stim parser, parsing an empty parens argument gives a paren with 0 in.
                    targets = [QubitAttr(0)]
                qubit = targets[0]
                coords = self.build_parens(parens)
                mapping = QubitMappingAttr(coords, qubit)
                return QubitCoordsOp(mapping)

input: Input = input instance-attribute

pos: int = pos instance-attribute

remaining: str property

__init__(input: Input | str, pos: int = 0)

Source code in xdsl/dialects/stim/stim_parser.py
55
56
57
58
59
def __init__(self, input: Input | str, pos: int = 0):
    if isinstance(input, str):
        input = Input(input, "<unknown>")
    self.input = input
    self.pos = pos

parse_optional_chars(chars: str)

Source code in xdsl/dialects/stim/stim_parser.py
67
68
69
70
def parse_optional_chars(self, chars: str):
    if self.input.content.startswith(chars, self.pos):
        self.pos += len(chars)
        return chars

parse_optional_pattern(pattern: re.Pattern[str])

Source code in xdsl/dialects/stim/stim_parser.py
72
73
74
75
def parse_optional_pattern(self, pattern: re.Pattern[str]):
    if (match := pattern.match(self.input.content, self.pos)) is not None:
        self.pos = match.regs[0][1]
        return self.input.content[match.pos : self.pos]

expect(message: str, parse: Callable[[StimParser], T | None]) -> T

Source code in xdsl/dialects/stim/stim_parser.py
80
81
82
83
def expect(self, message: str, parse: Callable[["StimParser"], T | None]) -> T:
    if (parsed := parse(self)) is None:
        raise StimParseError(self.pos, message)
    return parsed

parse_one_of(StimParsers: Sequence[Callable[[StimParser], T | None]]) -> T | None

Source code in xdsl/dialects/stim/stim_parser.py
85
86
87
88
89
90
def parse_one_of(
    self, StimParsers: Sequence[Callable[["StimParser"], T | None]]
) -> T | None:
    for StimParser in StimParsers:
        if (parsed := StimParser(self)) is not None:
            return parsed

parse_circuit() -> StimCircuitOp

Parse Stim dialect operations from a string formatted as a Stim file and return a StimCircuitOp containing the operations in its single block.

Circuits have format

circuit ::= (line)*

Collect by operations instead of by line to skip any lines that are not converted into operations.

Source code in xdsl/dialects/stim/stim_parser.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def parse_circuit(self) -> StimCircuitOp:
    """
    Parse Stim dialect operations from a string formatted as a Stim file and return a StimCircuitOp
    containing the operations in its single block.

    Circuits have format:
        circuit         ::= (line)*

    Collect by operations instead of by line to skip any lines that are not converted into operations.
    """
    lines: list[Operation] = []
    while (op := self.parse_optional_operation()) is not None:
        lines.append(op)

    circuit_body = Region(Block(ops=lines))
    return StimCircuitOp(circuit_body, None)

parse_optional_comment() -> None

Parse a comment if there, but this is not stored.

Comments have format

comment ::= # NOT_NEWLINE

Source code in xdsl/dialects/stim/stim_parser.py
111
112
113
114
115
116
117
118
119
120
121
122
123
def parse_optional_comment(self) -> None:
    """
    Parse a comment if there, but this is not stored.

    Comments have format:
        comment ::= `#` NOT_NEWLINE
    """
    if self.parse_optional_chars("#") is None:
        return

    self.expect(
        "comment", lambda parser: parser.parse_optional_pattern(NOT_NEWLINE)
    )

parse_optional_operation() -> Operation | None

Stim lines have format

line ::= indent? (instruction | block_start | block_end)? (comment ::= `#' NotNewLine)? NEWLINE

As this parser goes straight to operations, and stim comments, indentations and empty lines have no semantic meaning we look for operations, then skip straight through any lines without instructions or block starts or ends.

Operations are given by instructions, or the a block containing instructions.

Source code in xdsl/dialects/stim/stim_parser.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def parse_optional_operation(
    self,
) -> Operation | None:
    """
    Stim lines have format:
        line ::= indent? (instruction | block_start | block_end)? (comment ::= `#' NotNewLine)? NEWLINE
    As this parser goes straight to operations, and stim comments, indentations and empty lines have no semantic meaning
    we look for operations, then skip straight through any lines without instructions or block starts or ends.

    Operations are given by instructions, or the a block containing instructions.
    """

    self.parse_optional_pattern(INDENT)
    op = self.parse_optional_instruction()  # TODO: add parsing for blocks

    # Skip comments and empty lines
    while self._check_comment_and_newline() is not None:
        pass
    return op

parse_optional_instruction() -> Operation | None

Parse instruction with format

instruction ::= name (parens_arguments)? targets

Source code in xdsl/dialects/stim/stim_parser.py
154
155
156
157
158
159
160
161
162
163
164
165
166
def parse_optional_instruction(self) -> Operation | None:
    """
    Parse instruction with format:
        instruction ::= name (parens_arguments)? targets
    """
    if (name := self.parse_optional_pattern(NAME)) is None:
        return None
    op = Instruction(name)
    parens = self.parse_optional_parens()
    if parens is None:
        parens = []
    targets = self.parse_targets()
    return self.build_operation(op, parens, targets)

parse_optional_paren()

Parse an argument passed to an instruction in parentheses with format

arg ::= double

Source code in xdsl/dialects/stim/stim_parser.py
169
170
171
172
173
174
175
176
177
def parse_optional_paren(self):
    """
    Parse an argument passed to an instruction in parentheses with format:
        arg ::= double
    """
    self.parse_optional_pattern(INDENT)
    if (str_val := self.parse_optional_pattern(ARG)) is not None:
        arg = float(str_val)
        return arg

parse_optional_parens() -> list[float] | None

Parse an optional parenthesis with optional arguments with format

parens ::= ( (INDENT arg INDENT (',' INDENT arg INDENT)*)? )

The Stim documentation uses this format:

::= '(' ')' ::= /[ ]/ /[ ]/ (',' )?

but its implementation accepts empty arguments - this is kept here to be consistent.

Source code in xdsl/dialects/stim/stim_parser.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def parse_optional_parens(self) -> list[float] | None:
    """
    Parse an optional parenthesis with optional arguments with format:
        parens ::= `(` (INDENT* arg INDENT* (',' INDENT* arg INDENT*)*)? `)`

    TODO: The Stim documentation uses this format:
        <PARENS_ARGUMENTS> ::= '(' <ARGUMENTS> ')'
        <ARGUMENTS> ::= /[ \t]*/ <ARG> /[ \t]*/ (',' <ARGUMENTS>)?

        but its implementation accepts empty arguments - this is kept here to be consistent.

    """
    # Parse the opening bracket.
    if self.parse_optional_chars("(") is None:
        return None
    if self.parse_optional_chars(")") is not None:
        return []
    # Check if an argument exists.
    args: list[float] = [
        self.expect("arg", lambda parser: parser.parse_optional_paren())
    ]
    # Until the closing bracket is found:
    while self.parse_optional_chars(")") is None:
        self.parse_optional_pattern(INDENT)
        self.expect("comma", lambda parser: parser.parse_optional_chars(","))
        arg = self.expect("arg", lambda parser: parser.parse_optional_paren())
        args.append(arg)

    return args

parse_optional_target() -> QubitAttr | None

Parse a target with format

target ::= | | | |

TODO: Currently only supports target ::= qubit_target as the other relevant operations are not yet supported

Source code in xdsl/dialects/stim/stim_parser.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def parse_optional_target(self) -> QubitAttr | None:
    """
    Parse a target with format:
        target ::= <QUBIT_TARGET> | <MEASUREMENT_RECORD_TARGET> | <SWEEP_BIT_TARGET> | <PAULI_TARGET> | <COMBINER_TARGET>
    TODO: Currently only supports target ::= qubit_target as the other relevant operations are not yet supported
    """
    # Check for a space
    space = self.parse_optional_pattern(SPACE)
    self.parse_optional_pattern(INDENT)
    if (str_val := self.parse_optional_pattern(TARGET)) is not None:
        if space is None:
            raise StimParseError(self.pos, "Targets must be separated by spacing.")
        target = int(str_val)
        return QubitAttr(target)

parse_targets() -> list[QubitAttr]

Parse targets with format

targets = SPACE INDENTS target targets?

TODO: the Stim documentation indicates that their parser requires at least one target per instruction - but their actual parser does not enforce this. Check incongruency.

Source code in xdsl/dialects/stim/stim_parser.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def parse_targets(self) -> list[QubitAttr]:
    """
    Parse targets with format:
        targets = SPACE INDENTS target targets?
    TODO: the Stim documentation indicates that their parser requires at least one
    target per instruction - but their actual parser does not enforce this.
    Check incongruency.
    """
    # Check that there is a first target
    if (first := self.parse_optional_target()) is None:
        raise StimParseError(self.pos, "Expected at least one target")
    targets = [first]
    # Parse targets until no more are found
    while (target := self.parse_optional_target()) is not None:
        targets.append(target)
    return targets

build_parens(parens: list[float]) -> ArrayAttr[FloatData | IntAttr]

Convert a list of parens into an ArrayAttr.

Source code in xdsl/dialects/stim/stim_parser.py
249
250
251
252
253
254
255
256
257
258
def build_parens(self, parens: list[float]) -> ArrayAttr[FloatData | IntAttr]:
    """
    Convert a list of parens into an ArrayAttr.
    """
    args = [
        (IntAttr(int(arg))) if (arg.is_integer()) else (FloatData(arg))
        for arg in parens
    ]
    coords = ArrayAttr(args)
    return coords

build_operation(op: Instruction, parens: list[float], targets: Sequence[QubitAttr])

Build the operation corresponding to the name, parens, and targets found by the parser.

Source code in xdsl/dialects/stim/stim_parser.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def build_operation(
    self, op: Instruction, parens: list[float], targets: Sequence[QubitAttr]
):
    """
    Build the operation corresponding to the name, parens, and targets found by the parser.
    """
    match op:
        case Instruction.COORD:
            if targets == []:
                # In line with the behaviour of the Stim parser, parsing an empty parens argument gives a paren with 0 in.
                targets = [QubitAttr(0)]
            qubit = targets[0]
            coords = self.build_parens(parens)
            mapping = QubitMappingAttr(coords, qubit)
            return QubitCoordsOp(mapping)