← board

Track guessed as N from the test source. The ranker reads frontmatter, so this line — not the body — decides who works it; correct it if the guess is wrong.

origin/master has advanced 37 commit(s) since this sha. Re-verify at current HEAD before acting — the callback is tagged to the sha that was tested, which may no longer be the state of the tree.

regression: test-nilpy#src:test/test_nilpy_relative_import_in_package.npy red at ee62e6dc0582 (auto-filed by twatch)

Repro

tools/testmgr.py --tier full --job 'test-nilpy#src:test/test_nilpy_relative_import_in_package.npy' at ee62e6dc0582f6a018102c4e1d1d9a083d7e4f32

Range

bad ee62e6dc0582, last good 154d1aa3fba6, 76 commit(s) in range — the watcher narrows this by idle bisect; check tstate/TSTATE.md for the current range.

Log tail

pascal26:10: error: undefined variable (RENAMED)
(tail)
pascal26:10: error: undefined variable (RENAMED)
  near: A   U  RENAMED >>>    

Stub ticket: signal only. Track T agent (face 2) enriches or a dev track takes it from the repro line.


Resolution (2026-08-29)

Track N confirmed, not guessed — the defect is in compiler/pyparser.inc (PyParseImportRun), squarely a frontend file. Verified before claiming, as the auto-filed track-guess banner asks.

Cause

__init__.py reaches its siblings like every real package does:

from .two import A, B          # queues A and B
from . import two
from .two import A as RENAMED  # source A is now "queued"

Inside a pulled module every from-imported name is queued, aliased or not — that queue entry is the re-export binding. The self-capture guard (added so that from M import f as g, g as f does not resolve the second item's source g onto the g the first item just allocated) scanned the whole queue. But PyParseImportRun loops over every consecutive from statement and the queue accumulates across all of them, so statement 3's source A matched the entry statement 1 had queued. aliasRealSym was cleared, RENAMED was never allocated, and line 10 (U = RENAMED + 100) failed with undefined variable (RENAMED).

Fix

The claim the guard makes is about one from-import statement, so the scan is now per-statement. It matches on a statement stamp — the statement's own token index, carried in a new PyImpAliasStmt parallel to the existing PyImpAliasSym/PyImpAliasSrc.

Why a stamp and not the obvious queue index

The cheap version — aliasStmtBase := PyImpAliasCount at statement start, scan from there — passes this ticket's test and silently breaks the guard, which is why it is worth recording. PyParseImportRun is visited twice over the same tokens (measured with a temporary PXXDBG probe: four visits for a two-item import). A statement's first-pass entries therefore sit below the count its second pass starts from, so the second pass scans an empty range and the guard stops firing altogether.

Measured three-way, PRE / index-base / stamp, against compilers built from the same base so the comparison is honest (pinned is many commits behind HEAD and is not a control for this):

shape PRE index-base stamp CPython
from m import f as g, g as f (one stmt) 5018 5005 5018 18005
from m import f as g / g as f (two stmts) 5018 18018 18018 18005
this ticket's package shape ERR PASS PASS PASS

Row 1 is the regression the index-base version would have introduced, invisible from this ticket's own test. The stamp restores PRE there.

Row 2 still differs from PRE (5018 → 18018): one of the two names becomes correct and the other stays wrong. That is not this fix — it is a separate pre-existing defect, measured identical on PRE and filed as bug-n-a-from-import-alias-resolves-its-source-through-flat-scope.

Gate

make compiler/pascal26 — self-host fixedpoint verified, 69e9fbed8b22. tools/gate.sh quick GREEN. The test passes and its output matches .expected exactly. The whole nilpy import-test family was run individually against both PRE and the fix: identical on every one except this ticket's, which goes ERR → PASS; the five with no .expected were compared by runtime output rather than skipped, and are byte-identical.