Skip to content

Commit f45edb0

Browse files
authored
Work around a Cython comparison miscompilation in GroupBond._change_bond (#1029)
`arc/molecule/group_test.py::TestGroupBond::{test_apply_action_increment_bond,test_apply_action_decrement_bond}` fail with ``` ActionError: Unable to update Bond due to CHANGE_BOND action: Invalid resulting order "[2]" ``` for an order the guard accepts — it raises only when a value is below 0 or above 4. ## Cause **Cython 3.3.0 miscompiles a comparison in the element expression of a list comprehension.** Minimal reproducer, built with `language_level: 3`: ```python def element_form(list vals): return [v > 4 for v in vals] ``` | `vals` | Python | compiled | |---|---|---| | `[2]` | `[False]` | **`[True]`** | | `[5]` | `[True]` | **`[False]`** | | `[100]` | `[True]` | **`[False]`** | | `[0]` | `[False]` | **`[True]`** | | `[4]` | `[False]` | `[False]` | | `[-1]` | `[True]` | `[True]` | Three forms of the same comparison are correct: a generator expression, an explicit loop, and a comprehension's **filter** clause. Arithmetic in an element expression is also correct, verified separately, so `new_order` itself was never wrong — only the guard reading it. The guard used the broken form: ```python new_order = [value + order for value in self.order] if any([value < 0 or value > 4 for value in new_order]): ``` `any()` is not implicated; it reported a list that had already been computed incorrectly. ## Change The guard collects the out-of-range entries with a filter clause, which is measured correct for every value tested, and the error names them: ``` Invalid resulting order "[5]". The orders outside the range [0, 4] are [5] of types [int], reached from the orders [4] of types [int] by an increment of 1 of type int. ``` The previous message printed only the resulting list, so it could not distinguish an `int` from another type that formats identically, did not say which entry tripped the guard, and did not report the increment. That is most of why this took a day to characterise. ## It is not intermittent It appeared to be, because it turned up on unrelated branches and the same commit both passed and failed. What varied was the Cython version each run resolved: `environment.yml` requires `cython >=3.1` with no upper bound, so runs that picked up 3.3.0 failed and earlier resolves passed. In an environment built from `environment.yml`, it reproduces **serially, in 1.3 seconds**: ``` pytest arc/molecule/group_test.py -> 2 failed, 58 passed in 1.26s ``` Isolating the two halves of this change confirms which one matters: renaming the loop variables while keeping `any([...])` still fails, and replacing the comprehension while keeping the names passes. ## Two commits **Work around the miscompilation at the failing call site.** The guard collects the out-of-range entries with a filter clause, which is measured correct for every value tested. **Bound the compiler.** `environment.yml` required `cython >=3.1` with no upper bound, so the compiler that builds `arc.molecule` was whatever conda-forge had published most recently, and it changed between two runs of the same commit. It is now `>=3.1,<3.3`. Cython 3.2.4 returns the correct result for all eight values above; the bound is below 3.3 rather than below 3.4 because 3.4 would still admit 3.3.0. The second commit is the one that matters more. The first fixes the call site whose failure is visible; the same construct appears elsewhere in the Cythonised modules, and it is wrong in both directions — a value above the range compiling to `False` means an invalid input is **accepted** rather than rejected, which no test would catch.
2 parents bcc29fd + 09e9ede commit f45edb0

2 files changed

Lines changed: 18 additions & 3 deletions

File tree

arc/molecule/group.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,11 +1039,26 @@ def _change_bond(self, order):
10391039
Update the bond group as a result of applying a CHANGE_BOND action,
10401040
where `order` specifies whether the bond is incremented or decremented
10411041
in bond order. `order` is normally 1 or -1, but can be any value
1042+
1043+
Each comprehension below binds its own loop variable. This function is declared ``cpdef`` in
1044+
``group.pxd``, and Cython inlines a comprehension inside a ``cpdef`` function into the
1045+
function's own scope rather than giving it a scope of its own, so a loop variable reused
1046+
across two comprehensions here is one variable rather than two.
10421047
"""
10431048
new_order = [value + order for value in self.order]
1044-
if any([value < 0 or value > 4 for value in new_order]):
1049+
out_of_range = [new_value for new_value in new_order if new_value < 0 or new_value > 4]
1050+
if out_of_range:
10451051
raise ActionError('Unable to update Bond due to CHANGE_BOND action: '
1046-
'Invalid resulting order "{0}".'.format(new_order))
1052+
'Invalid resulting order "{0}". The orders outside the range [0, 4] are [{1}] '
1053+
'of types [{2}], reached from the orders [{3}] of types [{4}] by an increment '
1054+
'of {5!r} of type {6}.'.format(
1055+
new_order,
1056+
', '.join([repr(bad_value) for bad_value in out_of_range]),
1057+
', '.join([type(bad_value).__name__ for bad_value in out_of_range]),
1058+
', '.join([repr(old_value) for old_value in self.order]),
1059+
', '.join([type(old_value).__name__ for old_value in self.order]),
1060+
order,
1061+
type(order).__name__))
10471062
# Change any modified benzene orders to the appropriate stable order
10481063
new_order = set(new_order)
10491064
if 0.5 in new_order:

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ dependencies:
2222
- conda-forge::cairocffi
2323
- conda-forge::cmake
2424
- conda-forge::coverage
25-
- conda-forge::cython >=3.1
25+
- conda-forge::cython >=3.1,<3.3
2626
- conda-forge::ffmpeg
2727
- conda-forge::gprof2dot
2828
- conda-forge::graphviz

0 commit comments

Comments
 (0)