Skip to content

Commit

Permalink
Merge pull request #26 from openqasm/non-permissive-parse
Browse files Browse the repository at this point in the history
Add `permissive` flag for `parse` routine
  • Loading branch information
braised-babbage authored Aug 2, 2024
2 parents 1636847 + dcad691 commit 5471569
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 10 deletions.
47 changes: 38 additions & 9 deletions source/openpulse/openpulse/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
from typing import List, Union

try:
from antlr4 import CommonTokenStream, InputStream, ParserRuleContext
from antlr4 import CommonTokenStream, InputStream, ParserRuleContext, RecognitionException
from antlr4.error.Errors import ParseCancellationException
from antlr4.error.ErrorStrategy import BailErrorStrategy
except ImportError as exc:
raise ImportError(
"Parsing is not available unless the [parser] extra is installed,"
Expand All @@ -50,23 +52,42 @@
from ._antlr.openpulseParserVisitor import openpulseParserVisitor


def parse(input_: str) -> ast.Program:
class OpenPulseParsingError(Exception):
"""An error raised by the AST visitor during the AST-generation phase. This is raised in cases where the
given program could not be correctly parsed."""


def parse(input_: str, permissive: bool = False) -> ast.Program:
"""
Parse a complete OpenPulse program from a string.
:param input_: A string containing a complete OpenQASM 3 program.
:param permissive: A Boolean controlling whether ANTLR should attempt to
recover from incorrect input or not. Defaults to ``False``; if set to
``True``, the reference AST produced may be invalid if ANTLR emits any
warning messages during its parsing phase.
:return: A complete :obj:`~ast.Program` node.
"""
qasm3_ast = parse_qasm3(input_)
CalParser().visit(qasm3_ast)
qasm3_ast = parse_qasm3(input_, permissive=permissive)
CalParser(permissive=permissive).visit(qasm3_ast)
return qasm3_ast


def parse_openpulse(input_: str, in_defcal: bool) -> openpulse_ast.CalibrationBlock:
def parse_openpulse(
input_: str, in_defcal: bool, permissive: bool = True
) -> openpulse_ast.CalibrationBlock:
lexer = openpulseLexer(InputStream(input_))
stream = CommonTokenStream(lexer)
parser = openpulseParser(stream)
tree = parser.calibrationBlock()
if not permissive:
# For some reason, the Python 3 runtime for ANTLR 4 is missing the
# setter method `setErrorHandler`, so we have to set the attribute
# directly.
parser._errHandler = BailErrorStrategy()
try:
tree = parser.calibrationBlock()
except (RecognitionException, ParseCancellationException) as exc:
raise OpenPulseParsingError() from exc
result = (
OpenPulseNodeVisitor(in_defcal).visitCalibrationBlock(tree)
if tree.children
Expand Down Expand Up @@ -316,16 +337,24 @@ def visitOpenpulseStatement(self, ctx: openpulseParser.OpenpulseStatementContext


class CalParser(QASMVisitor[None]):
"""Visit OpenQASM3 AST and pase calibration"""
"""Visit OpenQASM3 AST and parse calibration
Attributes:
permissive: should OpenPulse parsing be permissive? If True, ANTLR
will attempt error recovery (although parsing may still fail elsewhere).
"""

def __init__(self, permissive: bool = False):
self.permissive = permissive

def visit_CalibrationDefinition(
self, node: ast.CalibrationDefinition
) -> openpulse_ast.CalibrationDefinition:
node.__class__ = openpulse_ast.CalibrationDefinition
node.body = parse_openpulse(node.body, in_defcal=True).body
node.body = parse_openpulse(node.body, in_defcal=True, permissive=self.permissive).body

def visit_CalibrationStatement(
self, node: ast.CalibrationStatement
) -> openpulse_ast.CalibrationStatement:
node.__class__ = openpulse_ast.CalibrationStatement
node.body = parse_openpulse(node.body, in_defcal=False).body
node.body = parse_openpulse(node.body, in_defcal=False, permissive=self.permissive).body
25 changes: 24 additions & 1 deletion source/openpulse/tests/test_openpulse_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
UnaryOperator,
WaveformType,
)
from openpulse.parser import parse
from openpulse.parser import parse, OpenPulseParsingError
from openqasm3.visitor import QASMVisitor


Expand Down Expand Up @@ -368,6 +368,29 @@ def test_switch_in_cal_block():
assert _remove_spans(program) == expected


def test_permissive_parsing(capsys):
p = """
cal {
int;
}
"""

with pytest.raises(AttributeError, match=r"'NoneType' object has no attribute 'line'"):
# In this case, we do get an exception, but this is somewhat incidental --
# the antlr parser gives us a `None` value where we expect a `Statement`
parse(p, permissive=True)
# The actual ANTLR failure is reported via stderr
captured = capsys.readouterr()
assert captured.err.strip() == "line 2:9 no viable alternative at input 'int;'"

with pytest.raises(OpenPulseParsingError):
# This is stricter -- we fail as soon as ANTLR sees a problem
parse(p)
captured = capsys.readouterr()
# The actual ANTLR failure is reported via stderr
assert captured.err.strip() == "line 2:9 no viable alternative at input 'int;'"


@pytest.mark.parametrize(
"p",
[
Expand Down

0 comments on commit 5471569

Please sign in to comment.