diff --git a/brian2/parsing/statements.py b/brian2/parsing/statements.py index 4f2661eb4..fdcaef5c4 100644 --- a/brian2/parsing/statements.py +++ b/brian2/parsing/statements.py @@ -3,23 +3,29 @@ Combine, Optional, ParseException, + ParseSyntaxException, Regex, Suppress, Word, alphas, nums, + restOfLine, ) from brian2.utils.caching import cached VARIABLE = Word(f"{alphas}_", f"{alphas + nums}_").set_results_name("variable") -OP = Regex(r"(\+|\-|\*|/|//|%|\*\*|>>|<<|&|\^|\|)?=").set_results_name("operation") +OP = ( + Regex(r"(\+|\-|\*|/|//|%|\*\*|>>|<<|&|\^|\|)?=") + .set_results_name("operation") + .set_name("assignment operator") +) EXPR = Combine( - CharsNotIn("=", min=1, max=1) + Optional(CharsNotIn("#")) + CharsNotIn("=#", min=1, max=1) + Optional(CharsNotIn("#")) ).set_results_name("expression") -COMMENT = Optional(CharsNotIn("#")).set_results_name("comment") -STATEMENT = VARIABLE + OP + EXPR + Optional(Suppress("#") + COMMENT) +COMMENT = restOfLine.set_results_name("comment") +STATEMENT = VARIABLE - OP - EXPR + Optional(Suppress("#") + COMMENT) @cached @@ -49,20 +55,26 @@ def parse_statement(code): """ try: parsed = STATEMENT.parse_string(code, parseAll=True) - except ParseException as p_exc: + except (ParseException, ParseSyntaxException) as p_exc: raise ValueError( - "Parsing the statement failed: \n" + f"Parsing the statement failed: {p_exc.msg}\n" + str(p_exc.line) + "\n" + " " * (p_exc.column - 1) + "^\n" - + str(p_exc) + + f"(line {p_exc.lineno}, col {p_exc.column})" ) from p_exc - parsed_statement = ( - parsed["variable"].strip(), - parsed["operation"], - parsed["expression"].strip(), - parsed.get("comment", "").strip(), - ) - return parsed_statement + var = parsed["variable"].strip() + op = parsed["operation"] + expr = parsed["expression"].strip() + comment = parsed.get("comment", "").strip() + + if expr.count("(") != expr.count(")"): + raise ValueError( + f"Unbalanced parentheses in expression: '{expr}'\n" + f"{code}\n" + f"{' ' * code.find(expr)}{'^' * len(expr)}" + ) + + return var, op, expr, comment diff --git a/brian2/tests/test_codegen.py b/brian2/tests/test_codegen.py index 325a129cc..8fad4040a 100644 --- a/brian2/tests/test_codegen.py +++ b/brian2/tests/test_codegen.py @@ -502,6 +502,8 @@ def test_automatic_augmented_assignments(): "x[0] = 3", "dx/dt = -v / tau", "v == 3*mV", + "v = (5 + 3", + "v = 5 + 3)", ], ) def test_incorrect_statements(s): @@ -509,6 +511,16 @@ def test_incorrect_statements(s): parse_statement(s) +@pytest.mark.codegen_independent +def test_parse_statement_comments(): + s = "v = 1.0 # some # comment" + var, op, expr, comment = parse_statement(s) + assert var == "v" + assert op == "=" + assert expr == "1.0" + assert comment == "some # comment" + + def test_clear_cache(): target = prefs.codegen.target if target == "numpy": diff --git a/brian2/tests/test_synapses.py b/brian2/tests/test_synapses.py index cabfaf94d..39b2ac0c5 100644 --- a/brian2/tests/test_synapses.py +++ b/brian2/tests/test_synapses.py @@ -3665,7 +3665,7 @@ def test_missing_lastupdate_error_run_regularly(): G = NeuronGroup(1, "v : 1") S = Synapses(G, G) S.connect() - S.run_regularly("v += exp(-lastupdate/dt") + S.run_regularly("v += exp(-lastupdate/dt)") with pytest.raises(BrianObjectException) as exc: run(0 * ms) assert exc_isinstance(exc, KeyError)