← board

a two-deep method chain on open()'s result does not parse

Repro

print(open("/tmp/x.txt").read().strip())
pascal26: error: unexpected token

The boundary — measured, one variable at a time

shape result
f = open(p) then f.read().strip() ok
open(p).read() ok
open(p).read().strip() error: unexpected token
s.strip().upper() on an ordinary str ok

So it is neither "chaining off open()" (one link works) nor "two-deep chaining" (fine on any ordinary value). It is specifically the second link of a chain rooted at the open() intrinsic.

Lead (unverified — measure before writing this into a fix)

open is a frontend intrinsic in compiler/parser.inc (the isNilPy and (name = 'open') arm). Every path through it ends with

CurASTNode := atRecv;
LastExprTk := tyClass;
Exit;

— it returns from the primary-expression parser directly rather than falling through to whatever normally continues a postfix/selector chain. One trailing .read() still binds (some outer loop picks it up), and the next . is what finds no handler. Confirm this by reading the postfix loop before changing it; the Exit is the obvious suspect and obvious suspects in this file have been wrong before.

If that is the cause, the same shape is worth checking on every NilPy frontend intrinsic that ends in a bare Exitinput, int, str, and the rest — since they were written to the same pattern. A one-site fix here would leave the siblings broken, which is the recurring shape in this frontend (see normalise-dont-special-case).

Diagnostic quality

"unexpected token" names nothing — not the construct, not open, not the member. Whatever the fix, the error should say what it could not continue.

Gate

The repro printing the file's stripped contents; the sibling intrinsics checked for the same shape and covered if they share it; make test-nilpy green + self-host fixedpoint.

Resolution (2026-08-11) — already fixed on master; verified and pinned down by a test

Measured before touching anything: the repro WORKS at HEAD and matches CPython. The fix was not the intrinsic's Exit the lead suspected — it was 3757dddd0 ("a Python suffix chain composes in any order"), which landed earlier the same day and is an ancestor of this session's first commit. That commit made the suffix cluster re-run until a pass consumes nothing, so a suffix is no longer reachable only in the order the loops happen to appear; .read() then .strip() needed them backwards, which is exactly what the ticket's boundary table described.

pinned predates it, which is why the bug still reproduces there — and that makes a good control: the same file fails on pinned at exactly the second link and passes at HEAD.

The ticket's real remaining ask was the sibling check, and the siblings are fine because the fix was made at the cluster rather than per-intrinsic: str(12).zfill(4).upper(), str(3.5).split(".")[1].strip(), open(p).readlines()[0].strip() (a chain through a subscript) and a four-link open(p).read().strip().replace(...).lower() all match CPython.

Closed with test/test_nilpy_intrinsic_result_chain.npy so it cannot come back silently — the ticket was filed against a shape nothing covered.

One genuine gap surfaced while checking siblings: int("42").bit_length() is refused because the METHOD does not exist (the chain parses; the diagnostic names it). Filed as bug-n-int-bit-length-is-not-implemented.

Log