← board

a |= <set> silently does nothing (and mixing it with += segfaults)

a = set()
for i in [1, 2]:
    a |= set([i])
print(sorted(a))       # CPython [1, 2]     pxx []

The plain spelling is CORRECT:

a = set()
for i in [1, 2]:
    a = a | set([i])   # [1, 2] on both

Two spellings of one operation answering differently is the tell this codebase keeps recording.

Measured

shape CPython pxx
`a = set([i])` in a loop [1, 2]
`a = set([9])` in a loop (constant RHS) [9]
`a = a set([i])` in a loop [1, 2]
`d = set([5])` with NO loop [5]
the same file mixing ` =withb += [i]andc += i` fine

The last row matters: this is not only a lost update. A program that uses |= beside ordinary += accumulators crashes, which is how it first showed up.

Cause

PyAugBinTok (pyparser.inc ~14881) maps the augmented token to its binary partner, and tkPipeEq maps to tkOr — the general or-operator token. The desugar then builds an AN_BINOP with tkOr DIRECTLY, so it never reaches whatever the ordinary | path does for two SETS (which is why the plain spelling is right: it goes through that path and lowers to the set-union helper).

tkAmpEq -> tkAnd is the same shape and &= on sets should be checked in the same pass; ^= (tkXorEq -> tkXor) too.

Shape of a fix

Make the augmented desugar go through the SAME operator lowering the plain binary form uses, rather than hand-building an AN_BINOP — that is the normalise-dont-special-case answer, and it fixes &=/^=/-= on sets at the same time instead of one token at a time. If that is too large, the narrow version is to route a set-typed (or variant) operand pair to the set helpers in the desugar, but note that only moves the second path rather than removing it.

Check the SEGFAULT separately once the union works — it may simply be the same wrong lowering meeting an accumulator of another type, or it may be its own fault.

Gate

.npy diffed against CPython: |= in a loop and outside one, with a constant and a loop-dependent RHS, &= and ^= on sets, |= on INTEGERS (which must stay bitwise — the control that keeps a set-specific fix from breaking the numeric case), the plain a = a | b spelling, and a file mixing |= with list and int += accumulators.

FIXED (2026-08-09, claude-AN) — |= only

a |= <set> now mutates in place and matches CPython.

Fixed the way += on a list already is: an in-place method call (TPyList.setupdate, mirroring extend) chosen in the augmented-assign lowering BEFORE the generic desugar, so it never reaches the wrong token.

In place, not a rebind, and that is the part worth stating: CPython's |= MUTATES, so an alias taken before the statement must see the new elements. A rebind would have passed every other assertion in the test — the alias line is the only one that can tell them apart, and its rebind twin pins that the plain a = a | b spelling still does NOT affect its alias.

The integer control is the other one that matters: n |= 2 must stay bitwise (7), which is what keeps a set-specific arm from capturing the numeric case.

&=, ^= and -= — DONE in the same session

Finished rather than left half-done, since a sibling kept broken beside a fixed one is exactly the trap this codebase keeps recording. All four now take the same in-place route (setupdate / setintersect / setsymdiff / setdiff).

The removing three each snapshot the side they iterate before removing: an index walk over a list you are shrinking skips the element after every removal. The cases that catch getting that wrong are the SELF ones — s ^= s, s -= s, s &= s — where both sides are the same object; s ^= s must end EMPTY and s &= s must be unchanged. All three are in the test, as are the aliasing assertions and integer controls for every operator.

The normalise-dont-special-case answer — route the augmented desugar through the same operator lowering the plain form uses — is still the better shape and would delete this arm. It stays worth doing; it is no longer urgent, because no spelling silently loses data now.

The SEGFAULT row is a different bug

The "mixing |= with += segfaults" row was NOT this. Reduced separately to a name reused as an assignment target and then as a for-loop target, and filed as [[bug-nilpy-name-assigned-from-a-call-then-reused-as-a-loop-target-segfaults]].

Log