← board

Repro, with the four controls that locate the boundary

class V:
    def __init__(self, x): self.x = x
    def __sub__(self, o): return V(self.x - o.x)
    def norm(self):       return self.x

def f(p, q):
    return (p - q).norm()      # pascal26: expected newline after statement
print(f(V(3), V(1)))           # CPython: 2
shape verdict
(p - q).norm(), unannotated params error
(p).norm(), unannotated — no operator at all error
t = p - q then t.norm() compiles, 2
p.__sub__(q).norm() — same call, no parentheses compiles, 2
def f(p: V, q: V) — annotated compiles, 2
(a - b).norm() at MODULE level, a = V(3) compiles, 2

The operator is not the variable and neither is the dunder(p).norm() fails on its own. What the failing rows share is a parenthesised receiver whose static type the parser does not know; give it a type by any route (annotation, module-level binding, a temp) and it works.

Where it is

pyparser.inc, the postfix tail after a parenthesised expression. Its comment records the sibling fix bug-nilpy-postfix-after-a-parenthesised-expression, which made this tail stand down in Python mode and hand . and [ to ParseFactor's own Python suffix loops, on the stated grounds that those already cover "str methods, subscript/slice, variant dispatch, class methods". That is true for a receiver of known type and for the int/str intrinsics — ("ab").upper() and (1 + 2).bit_length() both work, and the tail exempts the int ones explicitly via PyIsIntMethodSuffixAhead. The variant-receiver arm is the hole: the grouped value is left with the . dangling, the statement parser sees a token it did not expect, and reports the newline error.

A COMMENT AND THE CODE DISAGREE HERE, WHICH IS ITS OWN FINDING — the comment claims the handoff covers variant dispatch and it does not, so whoever fixes this should decide which is wrong before editing either. Matching the comment to the behaviour would destroy the evidence.

Why it ranks here

It is the residual of bug-n-a-class-body-cannot-alias-a-method-defined-above-it and inherits its blast radius exactly: with that one fixed, math3d.py now reaches line 287 (f = (target - eye).normalized() inside Mat4.look_at) and stops there, so the same seven modules — math3d, wind, rig, vessel, sim, traffic, app — are still blocked, on one line again.

Fork: fix nilpy, not the source. (a - b).method() is ordinary Python and the umbrella's cheat licence is for constructs that are principally incompatible; this is a parser gap with a one-line workaround that would have to be applied wherever it recurs.

Not new, and that is measured, not assumed. The pinned compiler — which predates the alias fix entirely — gives the identical error on the identical line once the alias line is deleted, so nothing here was introduced by that change. It was masked behind the earlier error.

Log