From cef0b50221ccd30aa7163574775100f33cef400c Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:39:41 +0800 Subject: [PATCH] Treat ^ and v as operators even without surrounding whitespace parse_boolean_expr has a fast-path that returns the whole string as a single variable name when it contains no operator. It only checked for "!", not the other two classical operators "^" (and) and "v" (or), so an unspaced guard like cond="a^b" skipped operator replacement and was looked up as a variable named "a^b" -- silently evaluating wrong (or raising InvalidDefinition on a StateChart). "!a" already worked unspaced, so this was an inconsistency. Gate the fast-path on the module's existing operator `pattern` (which already matches !, ^ and the word-bounded v) instead of a hand-rolled "!" check. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> --- statemachine/spec_parser.py | 6 ++++-- tests/test_spec_parser.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/statemachine/spec_parser.py b/statemachine/spec_parser.py index 306a49c3..007d4d86 100644 --- a/statemachine/spec_parser.py +++ b/statemachine/spec_parser.py @@ -315,8 +315,10 @@ def parse_boolean_expr(expr, variable_hook, operator_mapping): if expr.strip() == "": raise SyntaxError("Empty expression") - # Optimization trying to avoid parsing the expression if not needed - if "!" not in expr and " " not in expr and "In(" not in expr: + # Optimization trying to avoid parsing the expression if not needed. Skip it + # when any classical operator is present, not just "!" -- an unspaced "^"/"v" + # (e.g. "a^b") would otherwise be swallowed into a single variable name. + if not pattern.search(expr) and " " not in expr and "In(" not in expr: return variable_hook(expr) expr = replace_operators(expr) tree = ast.parse(expr, mode="eval") diff --git a/tests/test_spec_parser.py b/tests/test_spec_parser.py index 51115bd7..cce7b87e 100644 --- a/tests/test_spec_parser.py +++ b/tests/test_spec_parser.py @@ -210,6 +210,18 @@ def test_classical_operators_name(): ) # name reflects expression structure +def test_classical_operators_without_spaces(): + # "^" and "v" are operators even without surrounding whitespace, like "!", + # so they must not be swallowed into a single variable name. + for unspaced, spaced in [ + ("frodo_has_ring^sam_is_loyal", "frodo_has_ring ^ sam_is_loyal"), + ("(frodo_has_ring)v(sauron_alive)", "(frodo_has_ring) v (sauron_alive)"), + ]: + got = parse_boolean_expr(unspaced, variable_hook, operator_mapping)() + want = parse_boolean_expr(spaced, variable_hook, operator_mapping)() + assert got is want, unspaced + + def test_empty_expression(): expr = "" with pytest.raises(SyntaxError):