← board

Measured 2026-09-10, compiler b7745aaf0a59, tree 3bebb551e

lekkerzeilen/platform/__init__.py:31   undefined variable (staticmethod)
lekkerzeilen/platform/_pxx.py:31       undefined variable (staticmethod)

The idiom, from _pxx.py — a class used as a namespace whose members are plain functions:

class gl:
    clear_color = staticmethod(_unimplemented)
    clear       = staticmethod(_unimplemented)

CORRECTED 2026-09-10 — what this ticket got wrong, and why

It was filed at prio 80 on the argument that it gated the whole platform/ package. That argument was false and the fix is worth +1 module, not 3.

platform/__init__.py contains no staticmethod. Measured: 0 occurrences there, 5 in _pxx.py. And bindings.py's KEY_ESCAPE row did not clear — it was never a cascade of this ticket. __init__.py's real wall is _ctypes_backend.py's import ctypes, reached through from . import _ctypes_backend on the branch taken when import ctypes SUCCEEDS. That branch is dead under pxx and the compiler resolves it anyway.

The tell was in the census and two readers missed it: the two rows carried the IDENTICAL line number, 31, and both were _pxx.py:31. An error raised inside an imported module prints as pascal26:<n>: with that module's line number and no file name, so the reader supplies the file they invoked. Two unrelated modules therefore reported one cause at one line, which reads exactly like a shared dependency. frankB filed that separately (bug-n-an-error-inside-an-imported-module-is-reported-with-that-modules-line-number-and-no-file-name, prio 55) and it cost a second chase the same hour: post-fix, __init__.py reports pascal26:8: no unit named ctypes, and line 8 of that file is the RST prose ``ctypes`` — ten minutes spent testing whether a docstring was being read as an import. It was not; the line belongs to another file.

The import-position mechanism held exactly. staticmethod sits at line 31 and there were four modules behind it that had never been reached, so a small move was the prediction and a small move is what happened. The p80 was an artefact of a diagnostic, not of the mechanism.

Relationship to its two siblings — one concept, three doors

CORRECTED: that framing is right about two of the three and wrong about this one. os.environ and math.sin do share PyIsStdlibMemberValue. staticmethod does not go near it — a builtin NAME is a separate mechanism, in the eval/setattr chain (frankB, measured while fixing it). So do NOT try to fix all three in one place; the first two are one job and this was another.

Also measured on the way past: bug-n-os-environ-and-os-sep-are-not-values is half staleos.sep and os.linesep have worked since 996bcf5a8, 2026-08-29, the day after it was filed. What remains is environ, and it is a different job: a MAPPING that both in and .get/[] must reach, not another name in a list.

THE FIX AS LANDED (frankB)

compiler/pyparser.inc, in the builtin-name chain beside eval / setattr: staticmethod(X) is the IDENTITY, classmethod(X) is refused by name.

Why identity is a measurement and not a shortcut. clear = f in a class body already worked before this landed, and dispatched correctly through both doors — gl.clear(1) and gl().clear(1) each answer 2. So wrapping the same function value is exactly what CPython's descriptor does when it is READ. The only remaining difference is gl.__dict__['clear'] is not f, and NilPy exposes no __dict__ for that to be visible through.

Why classmethod is refused rather than approximated. CPython binds the class as the first argument, so identity would silently drop cls and shift every remaining argument by one — a wrong answer replacing a loud diagnostic. The @classmethod DECORATOR is supported and the message says so.

Two tests, deliberately of different assertion classes:

file asserts
test_nilpy_staticmethod_as_a_value.npy class door, instance door, module scope, and a user def shadowing the builtin
test_nilpy_classmethod_as_a_value_is_refused.npy rc=1, the message, and no binary

The instance-door rows are what say the lowering is right rather than merely quiet: a staticmethod must NOT take the receiver, and there it does not.

Recorded, not fixed, and both found while building the CONTROL

The remaining walls, from the same 33-module census

math.atan2 / math.sin is 7 modules and the largest single cause; ctypes 5; .contains() / .read_grid() / .queued() 5, which is one error shape and worth reading as a group before assuming it is one bug; threading 2; os 1; *unpack 1.

Log