← board

An augmented assign through a variant subscript is computed and dropped

Measured (before, self-hosted at 88b8aeb94)

d = {"a": {"n": 0}}
d["a"]["n"] += 1
print(d)                        # CPython {'a': {'n': 1}}   pxx {'a': {'n': 0}}

grid = [[1, 2], [3, 4]]
grid[0][0] += 10
print(grid)                     # CPython [[11, 2], [3, 4]]  pxx [[1, 2], [3, 4]]

Three things bounded it, and together they name the cause:

d["a"]["n"] = d["a"]["n"] + 1   # the PLAIN spelling works
d["a"]["n"] = 9                 # plain assignment through a variant base works
e = {"n": 0}; e["n"] += 1       # augmented on a STATIC base works

And it is not about nesting — a local bound from a container is the same variant:

inner = d["a"]
inner["n"] += 1                 # also dropped

Cause

compiler/parser.inc, the variant-base subscript arm of ParseLValueAST:

        if CurTok.Kind = tkAssign then
        begin
          Next; PyParseBoolExpr;
          node := PyMakeVariantSetItem(node, indexNode, CurASTNode);
          
        end;
        node := PyMakeVariantGetItem(node, indexNode);

Only tkAssign — a plain = — was handled. An augmented token fell through to the GETTER, and the enclosing augmented-assign machinery built a value with nowhere to go. PXXDBG=a.ir shows it exactly: the flat case ends with the store call, the nested case computes the result and emits no store at all.

A statically dict/list-typed base never reaches here — it goes through the default indexed property, which [[feature-nilpy-augmented-subscript-assign]] taught to desugar the read-modify-write itself. That ticket fixed the path it could see; this is the sibling route it did not.

Fix

Handle the augmented tokens in the same arm, desugaring v[k] op= x to v[k] = v[k] OP x with PyMakeVariantGetItem for the read and PyMakeVariantSetItem for the write — the same shape the property path uses.

Verified

test/test_nilpy_augmented_subscript_variant_base.npy (new, wired into make test-nilpy): nested dict and nested list, a local bound from a container, every augmented operator over a variant base (+= -= *= //= %= <<= /=, plus += on a str and on a list), the static-base controls, plain assignment, and the record-aggregation shape that found it. All lines match CPython. The wider probe corpus is unchanged and tools/gate.sh quick is GREEN.

Known, pre-existing, NOT introduced here: the index is evaluated twice

d["a"][key()] += 1     # CPython calls key() once; pxx calls it twice

The desugar re-evaluates the base and index expressions, so a side-effecting index runs twice. The statically-typed path already did this — measured, e[key()] += 1 calls key() twice too — and [[feature-nilpy-augmented-subscript-assign]] documents it as a deliberate trade ("the same trade the del d[k] rewrite makes"). The new arm matches the existing behaviour rather than diverging from it; the VALUE is correct either way. Filed on its own as [[bug-nilpy-augmented-subscript-evaluates-its-index-twice]].

Log