← board

A filter BETWEEN two for clauses is not supported

print([x + y for x in [0, 1, 2] if x > 0 for y in range(2)])
# CPython [1, 2, 2, 3]
# pxx     error: undefined variable (y)  near: __py_cv6_0  y >>> for __py_cv6_0

The boundary — the POSITION of the filter, on either path

comprehension result
[x + y for x in xs for y in ys] (no filter) fine
[x + y for x in xs for y in ys if y > 1] (after the LAST clause) fine
[x for x in xs if x > 1] (one clause, filtered) fine
[x + y for x in xs if x > 0 for y in ys] (BETWEEN) error
the same with range() in either clause error

CPython allows a filter after every clause, and the natural reading — "filter the outer loop before entering the inner one" — is also the efficient one, which is why the position is used.

Where to look

Both paths (PyParseForIn's comprehension arm and PyParseFor's counted-range arm) treat the filter as a suffix: they parse the element expression, then look for a single trailing if and wrap the append in it. A clause-position filter needs the filter to wrap the REST OF THE HEADER instead — the recursion into the next clause has to happen INSIDE the AN_IF, not after it.

The two-clause recursion landed in [[bug-nilpy-a-second-for-clause-fails-when-the-first-iterable-is-a-range]] (and, for containers, in [[bug-nilpy-nested-for-comprehension-not-supported]]), so the shape to add is: at the point where the header continues, if the next token is if, parse the condition, then recurse for whatever follows and wrap the recursion's body in the filter. Both paths need it — fixing one arm of a double case is what devdocs/dev/normalise-dont-special-case.md is about, and this bug is the second time this pair has diverged.

Gate

A .npy diffed against CPython: the interleaved filter with a container first clause and with a range() first clause, a filter on BOTH clauses, a filter between three clauses, and the already-working suffix-filter rows as controls.

2026-08-13 — FIXED on both paths, same shape on each

A filter between clauses gates the inner LOOP, so it has to WRAP the rest of the header. Both comprehension paths now recognise it at the point where the header continues — PyCompForAfter answers "is there another depth-0 for before the closer?" — parse the condition there, and recurse for the remainder INSIDE the AN_IF. The trailing-filter arms are untouched and still gate the append.

The stale CurTok was the one real surprise. In the container path the filter's if is temporarily rewritten to a fake : so the ITER expression parser stops at it, and restored afterwards — but CurTok is a COPY taken when the token was still the fake colon, so the test had to read Tokens[TokPos - 1] and re-read the token before parsing. Testing CurTok.Kind = tkIf silently never matched, which looked exactly like the arm not being reached.

Both paths were fixed in one pass rather than one-then-the-other: the ticket already recorded that this pair has diverged twice, and the range path's arm is five lines once the container path's is written.

Gate

test/test_nilpy_filter_between_for_clauses.npy + .expected from CPython, wired into make test-nilpy: the interleaved filter with a container first clause and with a range() first clause, filters on BOTH clauses, a filter between three clauses, a filter over a nested-list flatten, and the already-working suffix-filter / no-filter / single-clause forms as controls. make test-nilpy green, gate.sh quick GREEN.

Log