From 4777f833214f0e608eaf28dc1e095d3448fb6171 Mon Sep 17 00:00:00 2001 From: Erik Davis Date: Mon, 22 Jul 2024 15:41:43 -0700 Subject: [PATCH 1/4] add permissive flag to parse --- source/openpulse/openpulse/parser.py | 36 +++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/source/openpulse/openpulse/parser.py b/source/openpulse/openpulse/parser.py index 79c704f..2d1f272 100644 --- a/source/openpulse/openpulse/parser.py +++ b/source/openpulse/openpulse/parser.py @@ -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," @@ -49,24 +51,39 @@ from ._antlr.openpulseParser import openpulseParser from ._antlr.openpulseParserVisitor import openpulseParserVisitor +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) -> ast.Program: +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 @@ -318,14 +335,17 @@ def visitOpenpulseStatement(self, ctx: openpulseParser.OpenpulseStatementContext class CalParser(QASMVisitor[None]): """Visit OpenQASM3 AST and pase calibration""" + 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 From ec956f3f3321f54bfcbf1a3d303c77792c3fe90d Mon Sep 17 00:00:00 2001 From: Erik Davis Date: Mon, 22 Jul 2024 16:02:25 -0700 Subject: [PATCH 2/4] add test --- source/openpulse/openpulse/parser.py | 6 ++++- .../openpulse/tests/test_openpulse_parser.py | 25 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/source/openpulse/openpulse/parser.py b/source/openpulse/openpulse/parser.py index 2d1f272..018f421 100644 --- a/source/openpulse/openpulse/parser.py +++ b/source/openpulse/openpulse/parser.py @@ -51,10 +51,12 @@ from ._antlr.openpulseParser import openpulseParser from ._antlr.openpulseParserVisitor import openpulseParserVisitor + 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. @@ -71,7 +73,9 @@ def parse(input_: str, permissive: bool = False) -> ast.Program: return qasm3_ast -def parse_openpulse(input_: str, in_defcal: bool, permissive: bool = True) -> 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) diff --git a/source/openpulse/tests/test_openpulse_parser.py b/source/openpulse/tests/test_openpulse_parser.py index a80a03a..555a509 100644 --- a/source/openpulse/tests/test_openpulse_parser.py +++ b/source/openpulse/tests/test_openpulse_parser.py @@ -38,7 +38,7 @@ UnaryOperator, WaveformType, ) -from openpulse.parser import parse +from openpulse.parser import parse, OpenPulseParsingError from openqasm3.visitor import QASMVisitor @@ -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", [ From eba432e9cc36891b9425302e79ade08d7d0f6edc Mon Sep 17 00:00:00 2001 From: Erik Davis Date: Thu, 1 Aug 2024 13:51:21 -0700 Subject: [PATCH 3/4] update docstring --- source/openpulse/openpulse/parser.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/openpulse/openpulse/parser.py b/source/openpulse/openpulse/parser.py index 018f421..cc347af 100644 --- a/source/openpulse/openpulse/parser.py +++ b/source/openpulse/openpulse/parser.py @@ -337,7 +337,12 @@ def visitOpenpulseStatement(self, ctx: openpulseParser.OpenpulseStatementContext class CalParser(QASMVisitor[None]): - """Visit OpenQASM3 AST and pase calibration""" + """Visit OpenQASM3 AST and pase 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 From dcad6918a328d699d223b7041c467416f691dd76 Mon Sep 17 00:00:00 2001 From: Erik Davis Date: Thu, 1 Aug 2024 13:51:51 -0700 Subject: [PATCH 4/4] fix typo --- source/openpulse/openpulse/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/openpulse/openpulse/parser.py b/source/openpulse/openpulse/parser.py index cc347af..ab8bd0e 100644 --- a/source/openpulse/openpulse/parser.py +++ b/source/openpulse/openpulse/parser.py @@ -337,7 +337,7 @@ 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