Skip to content

Add permissive flag for parse routine #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading