Skip to content

Commit 5471569

Browse files
Merge pull request #26 from openqasm/non-permissive-parse
Add `permissive` flag for `parse` routine
2 parents 1636847 + dcad691 commit 5471569

File tree

2 files changed

+62
-10
lines changed

2 files changed

+62
-10
lines changed

source/openpulse/openpulse/parser.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
from typing import List, Union
2828

2929
try:
30-
from antlr4 import CommonTokenStream, InputStream, ParserRuleContext
30+
from antlr4 import CommonTokenStream, InputStream, ParserRuleContext, RecognitionException
31+
from antlr4.error.Errors import ParseCancellationException
32+
from antlr4.error.ErrorStrategy import BailErrorStrategy
3133
except ImportError as exc:
3234
raise ImportError(
3335
"Parsing is not available unless the [parser] extra is installed,"
@@ -50,23 +52,42 @@
5052
from ._antlr.openpulseParserVisitor import openpulseParserVisitor
5153

5254

53-
def parse(input_: str) -> ast.Program:
55+
class OpenPulseParsingError(Exception):
56+
"""An error raised by the AST visitor during the AST-generation phase. This is raised in cases where the
57+
given program could not be correctly parsed."""
58+
59+
60+
def parse(input_: str, permissive: bool = False) -> ast.Program:
5461
"""
5562
Parse a complete OpenPulse program from a string.
5663
5764
:param input_: A string containing a complete OpenQASM 3 program.
65+
:param permissive: A Boolean controlling whether ANTLR should attempt to
66+
recover from incorrect input or not. Defaults to ``False``; if set to
67+
``True``, the reference AST produced may be invalid if ANTLR emits any
68+
warning messages during its parsing phase.
5869
:return: A complete :obj:`~ast.Program` node.
5970
"""
60-
qasm3_ast = parse_qasm3(input_)
61-
CalParser().visit(qasm3_ast)
71+
qasm3_ast = parse_qasm3(input_, permissive=permissive)
72+
CalParser(permissive=permissive).visit(qasm3_ast)
6273
return qasm3_ast
6374

6475

65-
def parse_openpulse(input_: str, in_defcal: bool) -> openpulse_ast.CalibrationBlock:
76+
def parse_openpulse(
77+
input_: str, in_defcal: bool, permissive: bool = True
78+
) -> openpulse_ast.CalibrationBlock:
6679
lexer = openpulseLexer(InputStream(input_))
6780
stream = CommonTokenStream(lexer)
6881
parser = openpulseParser(stream)
69-
tree = parser.calibrationBlock()
82+
if not permissive:
83+
# For some reason, the Python 3 runtime for ANTLR 4 is missing the
84+
# setter method `setErrorHandler`, so we have to set the attribute
85+
# directly.
86+
parser._errHandler = BailErrorStrategy()
87+
try:
88+
tree = parser.calibrationBlock()
89+
except (RecognitionException, ParseCancellationException) as exc:
90+
raise OpenPulseParsingError() from exc
7091
result = (
7192
OpenPulseNodeVisitor(in_defcal).visitCalibrationBlock(tree)
7293
if tree.children
@@ -316,16 +337,24 @@ def visitOpenpulseStatement(self, ctx: openpulseParser.OpenpulseStatementContext
316337

317338

318339
class CalParser(QASMVisitor[None]):
319-
"""Visit OpenQASM3 AST and pase calibration"""
340+
"""Visit OpenQASM3 AST and parse calibration
341+
342+
Attributes:
343+
permissive: should OpenPulse parsing be permissive? If True, ANTLR
344+
will attempt error recovery (although parsing may still fail elsewhere).
345+
"""
346+
347+
def __init__(self, permissive: bool = False):
348+
self.permissive = permissive
320349

321350
def visit_CalibrationDefinition(
322351
self, node: ast.CalibrationDefinition
323352
) -> openpulse_ast.CalibrationDefinition:
324353
node.__class__ = openpulse_ast.CalibrationDefinition
325-
node.body = parse_openpulse(node.body, in_defcal=True).body
354+
node.body = parse_openpulse(node.body, in_defcal=True, permissive=self.permissive).body
326355

327356
def visit_CalibrationStatement(
328357
self, node: ast.CalibrationStatement
329358
) -> openpulse_ast.CalibrationStatement:
330359
node.__class__ = openpulse_ast.CalibrationStatement
331-
node.body = parse_openpulse(node.body, in_defcal=False).body
360+
node.body = parse_openpulse(node.body, in_defcal=False, permissive=self.permissive).body

source/openpulse/tests/test_openpulse_parser.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
UnaryOperator,
3939
WaveformType,
4040
)
41-
from openpulse.parser import parse
41+
from openpulse.parser import parse, OpenPulseParsingError
4242
from openqasm3.visitor import QASMVisitor
4343

4444

@@ -368,6 +368,29 @@ def test_switch_in_cal_block():
368368
assert _remove_spans(program) == expected
369369

370370

371+
def test_permissive_parsing(capsys):
372+
p = """
373+
cal {
374+
int;
375+
}
376+
"""
377+
378+
with pytest.raises(AttributeError, match=r"'NoneType' object has no attribute 'line'"):
379+
# In this case, we do get an exception, but this is somewhat incidental --
380+
# the antlr parser gives us a `None` value where we expect a `Statement`
381+
parse(p, permissive=True)
382+
# The actual ANTLR failure is reported via stderr
383+
captured = capsys.readouterr()
384+
assert captured.err.strip() == "line 2:9 no viable alternative at input 'int;'"
385+
386+
with pytest.raises(OpenPulseParsingError):
387+
# This is stricter -- we fail as soon as ANTLR sees a problem
388+
parse(p)
389+
captured = capsys.readouterr()
390+
# The actual ANTLR failure is reported via stderr
391+
assert captured.err.strip() == "line 2:9 no viable alternative at input 'int;'"
392+
393+
371394
@pytest.mark.parametrize(
372395
"p",
373396
[

0 commit comments

Comments
 (0)