From 51ba794e752e441f6b8e6a53cd5bbef85e7155c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:49:27 +0000 Subject: [PATCH 1/3] Initial plan From 6dd388fed123d8bc36f99e3e04328957fc2428a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:56:14 +0000 Subject: [PATCH 2/3] Add ThrowingErrorListener to Python ANTLR4 parser matching JavaScript implementation Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com> --- bnf/apyds_bnf/__init__.py | 14 +++++++++ bnf/tests/test_parse_unparse.py | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/bnf/apyds_bnf/__init__.py b/bnf/apyds_bnf/__init__.py index a6ba0778..5c35f594 100644 --- a/bnf/apyds_bnf/__init__.py +++ b/bnf/apyds_bnf/__init__.py @@ -1,6 +1,7 @@ __all__ = ["parse", "unparse"] from antlr4 import InputStream, CommonTokenStream +from antlr4.error.ErrorListener import ErrorListener from .DspLexer import DspLexer from .DspParser import DspParser from .DspVisitor import DspVisitor @@ -9,6 +10,11 @@ from .DsVisitor import DsVisitor +class ThrowingErrorListener(ErrorListener): + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): + raise Exception(f"line {line}:{column} {msg}") + + class ParseVisitor(DspVisitor): def visitRule_pool(self, ctx): return "\n\n".join(self.visit(r) for r in ctx.rule_()) @@ -71,8 +77,12 @@ def visitBinary(self, ctx): def parse(input: str) -> str: chars = InputStream(input) lexer = DspLexer(chars) + lexer.removeErrorListeners() + lexer.addErrorListener(ThrowingErrorListener()) tokens = CommonTokenStream(lexer) parser = DspParser(tokens) + parser.removeErrorListeners() + parser.addErrorListener(ThrowingErrorListener()) tree = parser.rule_pool() visitor = ParseVisitor() return visitor.visit(tree) @@ -81,8 +91,12 @@ def parse(input: str) -> str: def unparse(input: str) -> str: chars = InputStream(input) lexer = DsLexer(chars) + lexer.removeErrorListeners() + lexer.addErrorListener(ThrowingErrorListener()) tokens = CommonTokenStream(lexer) parser = DsParser(tokens) + parser.removeErrorListeners() + parser.addErrorListener(ThrowingErrorListener()) tree = parser.rule_pool() visitor = UnparseVisitor() return visitor.visit(tree) diff --git a/bnf/tests/test_parse_unparse.py b/bnf/tests/test_parse_unparse.py index 0b3e039b..00195a3b 100644 --- a/bnf/tests/test_parse_unparse.py +++ b/bnf/tests/test_parse_unparse.py @@ -143,3 +143,58 @@ def test_roundtrip_unparse_parse() -> None: dsp_intermediate = unparse(ds_original) ds_result = parse(dsp_intermediate) assert ds_result == ds_original + + +def test_parse_error_missing_closing_parenthesis() -> None: + """Test that parse throws error on missing closing parenthesis""" + dsp_input = "(a + b -> c" + try: + parse(dsp_input) + assert False, "Expected exception to be raised" + except Exception as e: + assert "line 1:7" in str(e) + assert "no viable alternative" in str(e) + + +def test_parse_error_bad_syntax() -> None: + """Test that parse throws error on bad syntax""" + dsp_input = "a b c -> -> d" + try: + parse(dsp_input) + assert False, "Expected exception to be raised" + except Exception as e: + assert "line 1:2" in str(e) + assert "mismatched input" in str(e) + + +def test_parse_error_malformed_parentheses() -> None: + """Test that parse throws error on malformed parentheses""" + dsp_input = "()()()" + try: + parse(dsp_input) + assert False, "Expected exception to be raised" + except Exception as e: + assert "line 1:1" in str(e) + assert "no viable alternative" in str(e) + + +def test_unparse_error_incomplete_binary() -> None: + """Test that unparse throws error on incomplete binary expression""" + ds_input = "(binary" + try: + unparse(ds_input) + assert False, "Expected exception to be raised" + except Exception as e: + assert "line 1:7" in str(e) + assert "mismatched input" in str(e) + + +def test_unparse_error_malformed_function() -> None: + """Test that unparse throws error on malformed function""" + ds_input = "(function" + try: + unparse(ds_input) + assert False, "Expected exception to be raised" + except Exception as e: + assert "line 1:9" in str(e) + assert "mismatched input" in str(e) From a84c6af8cd240b2a7121932e86ce21a1b7ffae60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:58:46 +0000 Subject: [PATCH 3/3] Improve error tests to use pytest.raises for better test structure Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com> --- bnf/tests/test_parse_unparse.py | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/bnf/tests/test_parse_unparse.py b/bnf/tests/test_parse_unparse.py index 00195a3b..eef8d491 100644 --- a/bnf/tests/test_parse_unparse.py +++ b/bnf/tests/test_parse_unparse.py @@ -1,3 +1,4 @@ +import pytest from apyds_bnf import parse, unparse @@ -148,53 +149,33 @@ def test_roundtrip_unparse_parse() -> None: def test_parse_error_missing_closing_parenthesis() -> None: """Test that parse throws error on missing closing parenthesis""" dsp_input = "(a + b -> c" - try: + with pytest.raises(Exception, match=r"line 1:7.*no viable alternative"): parse(dsp_input) - assert False, "Expected exception to be raised" - except Exception as e: - assert "line 1:7" in str(e) - assert "no viable alternative" in str(e) def test_parse_error_bad_syntax() -> None: """Test that parse throws error on bad syntax""" dsp_input = "a b c -> -> d" - try: + with pytest.raises(Exception, match=r"line 1:2.*mismatched input"): parse(dsp_input) - assert False, "Expected exception to be raised" - except Exception as e: - assert "line 1:2" in str(e) - assert "mismatched input" in str(e) def test_parse_error_malformed_parentheses() -> None: """Test that parse throws error on malformed parentheses""" dsp_input = "()()()" - try: + with pytest.raises(Exception, match=r"line 1:1.*no viable alternative"): parse(dsp_input) - assert False, "Expected exception to be raised" - except Exception as e: - assert "line 1:1" in str(e) - assert "no viable alternative" in str(e) def test_unparse_error_incomplete_binary() -> None: """Test that unparse throws error on incomplete binary expression""" ds_input = "(binary" - try: + with pytest.raises(Exception, match=r"line 1:7.*mismatched input"): unparse(ds_input) - assert False, "Expected exception to be raised" - except Exception as e: - assert "line 1:7" in str(e) - assert "mismatched input" in str(e) def test_unparse_error_malformed_function() -> None: """Test that unparse throws error on malformed function""" ds_input = "(function" - try: + with pytest.raises(Exception, match=r"line 1:9.*mismatched input"): unparse(ds_input) - assert False, "Expected exception to be raised" - except Exception as e: - assert "line 1:9" in str(e) - assert "mismatched input" in str(e)