← board

What the owner said

"we have a new project ... i told it about pxx and to keep that in mind, so i think it did — not pulling in some typical python libraries etc.. but, it would be a good test target for us and nilpy. so, two things to note please — this time we are allow to 'cheat' on the code base. if there's something that's principally incompatible with nilpy, we could fix the source. so.. it just serves as a real-world testing project, and for now is not to be included in our test suite (like uforth)."

Both halves are load-bearing and both invert a normal rule, so read them before working this.

CHEATING IS ALLOWED HERE. Everywhere else a corpus is sacred: we compile UNVENDORED upstream source precisely so the corpus cannot be bent to fit the compiler, and bending it would destroy what the measurement is for. Not here. lekkerzeilen is the owner's own project and he has explicitly licensed changing it where a construct is principally incompatible with nilpy.

That licence is not a licence to take the easy road, and the judgment it hands you is the actual work. Each blocker below is a fork: fix nilpy, or fix lekkerzeilen. The test is whether the construct is idiomatic Python that nilpy ought to support — then it is a compiler bug and the source stays — or whether it is incidental, in which case changing the source is cheaper than growing the frontend and nothing is lost. The first blocker below is squarely the former; the ctypes one is squarely the latter. Say which fork you took and why, in the commit.

IT IS NOT A GATE. Do not add it to make test, test-nilpy, or any tier. uforth is the precedent: a real program we compile and benchmark, not a row that can go red and block a pin. If it earns a permanent row later, that is the owner's call and a separate ticket.

The target

/home/neo/lekkerzeilen — a leisure sailing simulator, free-roam, 3D, real geography, inland waterways first. Its own README states the constraint that makes it interesting to us:

"Plain Python, no C extensions — SDL2 and OpenGL are bound directly, so the same source can eventually compile under PXX. No asset files either ... because a sample library would mean a decoder and a decoder would mean a C extension."

So the pxx constraint is already designed in, by the owner, before we touched it. That is why the numbers below are as good as they are.

It is under active development — files changed during this very census — so every count here is dated, not current. Re-measure before quoting.

What it is made of (measured 2026-09-08, ast, not grep)

Python 14,297 LOC, 45 files, all parse clean under CPython 3.14
runtime package lekkerzeilen/ 26 files, 58 classes, 429 functions
third-party imports in the runtime package zero
numpy / PIL tests/ (3) and tools/ (1) only — dev-side, not the simulator
stdlib used math, ctypes, struct, array, sys, os, time, sqlite3, io, json, urllib, zlib, collections, argparse, warnings, shutil
f-strings 0 — formats with % at 90 sites
async / await / yield / match / walrus / nonlocal 0 of each
type annotations 0 of 1074 arguments
decorators property (41), staticmethod (15), classmethod (2), 2 setters — nothing else
uses classes, comprehensions (21 genexp, 20 listcomp), try (15), with (4), starred (32), global (3)

A first-error census is a LOWER BOUND, not a work estimate. The compiler stops at the first error in a module, so each row below names the cause that was hit first, not every cause present. Expect more behind each one. The one thing it does establish is which cause blocks the most modules today.

The blockers, in the order attempting the target found them

Measured with compiler/pascal26 sha256 a7b03135f504, built 2026-09-08 13:05. 3 of 16 compile clean today: geometry, scenery, shaders.

1. A class-body assignment cannot reference a method defined in that same class body — blocks SEVEN modules through one line. math3d.py:54 is __rmul__ = __mul__, and every module that imports math3d inherits the failure (math3d, wind, rig, vessel, sim, traffic, app). The diagnostic undefined variable (__mul__) reads as "operator overloading is unsupported", and that reading is wrong — nilpy knows __mul__, __add__, __sub__, __truediv__, __neg__, __eq__ and more. What it does not do is expose an already-defined method as an ordinary NAME in the class namespace. Minimal repro, with the positive control that separates the two readings:

class V:
    def __init__(self, x): self.x = x
    def __mul__(self, s):  return V(self.x * s)
    __rmul__ = __mul__          # <-- pascal26: undefined variable (__mul__)
v = V(3) * 4
print(v.x)                      # CPython: 12

Delete the alias line and the identical class compiles and prints 12, matching CPython. Fork: fix nilpy. name = earlier_name in a class body is ordinary Python and the __rmul__ = __mul__ idiom is how every vector class in the language is written; the source should not move for this.

2. array is not available — 2 modules (world, audio). import: no unit named array and no shim mimic_array. The shim naming suggests the mechanism exists and this module has not been written.

3. ctypes is not available — and the fork it looked like DOES NOT EXIST. This was filed as a Track U decision (emulate ctypes, or bind natively and change the source). Measurement dissolved it, and the ticket is deleted.

The project already carries the seam: lekkerzeilen/platform/__init__.py selects a backend and platform/_pxx.py is a stub its own author wrote for this, planning import SDL2/SDL.h — "the same mechanism behind its wrapper-free import sqlite3". That claim about our compiler is TRUE and tested (test_nilpy_import_sqlite.npy links libsqlite3.so.0 and calls into it; test_nilpy_import_c_header_still_works.npy guards the route). So arm B is not "change the source" at all — filling in a stub the author left for us is completing the project, not bending it, and the cheat licence is not spent.

And native binding already substantially WORKS, measured against compiler/pascal26 a7b03135f504:

import "/usr/include/GL/gl.h" compiles and RUNS
import "/usr/include/SDL2/SDL.h" stops on ONE line — SDL_endian.h:166, asm constraint "=Q"
glGetError() compiles and links; dies at exec on libgl.so (real name libGL.so.1)

So what looked like "build an FFI subsystem" is three small, precise blockers, now filed and wired above: the =Q inline-asm constraint (Track C, one letter, one site), the header-stem-to-soname mapping, and the inability to name a header in a subdirectory by anything but an absolute path.

Decision, and it is settled by measurement rather than taste: bind natively; do not build mimic_ctypes for this target. Emulating CPython's FFI on top of a compiler whose designed feature is wrapper-free C interop would be a wrapper around the mechanism that exists to avoid wrappers. A general ctypes shim remains worth having for arbitrary third-party Python that binds C libraries — that is a separate feature and this umbrella does not rank it.

4. collections.deque — 1 module (chart).

5. math.atan2 — 1 module (hud), and it is a DELIBERATE refusal, not a gap. atan2 is measured 1 ulp off, and the standing policy is to keep a 1-ulp-off RTL routine out rather than trade a loud undefined variable for a silently wrong last digit — see feature-nilpy-math-module-twelve-absent-names-measured, which owns it and is blocked on the correctly-rounded-libm work. Do not "fix" this with a table row; that is the fix the project has refused twice, and this seat proposed it before reading far enough. This is the clearest place the cheat licence applies: the refusal governs what math.atan2 may silently mean for every program, not whether this one may call ArcTan2 knowingly. A HUD heading does not care about the last digit.

6. str.join overload — 1 module (text): no overload of join matches these arguments.

Why this target is worth its rank

Every existing nilpy corpus proves one layer. This is 14k lines of ordinary, idiomatic, third-party-free Python written by someone who was not trying to exercise a compiler — which is exactly the population our fixtures cannot draw from. It is also interactive and graphical, so it reaches the FFI, the float paths and the real-time loop rather than a batch pipeline.

And it is a fair test of NilPy's stated direction: NilPy is upward compatible with CPython, one direction, so a program written in plain Python with no cleverness is precisely what must work.

Provenance and one caveat on the path

Filed after attempting the target, which is how CLAUDE.md says to grow an umbrella — every blocker above is a real failure of a real compile, not a triage of the backlog.

The owner named the project ~/marine. There is no ~/marine on plexus or on seven, and this ticket is about /home/neo/lekkerzeilen, which matches his description (marine, new, Python, told about pxx) and whose README names pxx explicitly. If he meant a different tree, this ticket is about the wrong one.

RE-MEASURED 2026-09-09 (frankB) — blocker 1 is fixed and blocker 1b took its place

compiler/pascal26 sha256 57ba8b0c2c8d, every lekkerzeilen/*.py compiled one by one. undefined variable (__mul__) is gone from every module.

The seven modules it blocked are still blocked, and by one line again: math3d.py:287, f = (target - eye).normalized() inside Mat4.look_at. Filed as [[bug-n-a-user-method-on-a-parenthesised-receiver-of-unknown-type-is-not-parsed]] and wired above in place of the closed row.

That residual is NOT new and it was not introduced by the fix. The PINNED binary, which predates it entirely, gives the identical error on the identical line once the alias line is deleted by hand; with the alias line present it gives the old 54: undefined variable (__mul__). Two binaries, one of which has never seen the change — the control fails differently, which is the point of running it.

Per-module outcome today (23 files under lekkerzeilen/, a wider denominator than the 16 "runtime modules" the summary counts — not the same number, so do not read it as a delta against 3):

compile clean  5   geometry, scenery, shaders, rd, __init__
parenthesised receiver (blocker 1b)   8   math3d wind rig vessel sim traffic wake __main__
array                                 3   app audio world
ctypes                                2   capture gfx
collections.deque                     1   chart
math.atan2                            1   hud
str.join overload                     1   text
queue                                 1   gauges

wake and __main__ are two modules the earlier census did not list under blocker 1; they fail on 1b through the same import of math3d.

RE-MEASURED again 2026-09-09 (frankB) — blocker 1b closed; 5 -> 7 modules

compiler/pascal26 sha256 16efc5348050. math3d and wake now compile. The seven modules that imported math3d get PAST it and fail on their own next causes, nearly all already blockers here: math.atan2 (rig, sim, traffic, vessel — the deliberate refusal), array (app, audio, world), ctypes (capture, gfx), queue (gauges), str.join (text).

Two causes are NEW and not yet filed, both first-error readings that need reducing before they are worth a ticket:

Every number here is a LOWER BOUND: this is a first-error census, so a module that clears one cause may surface another, which is exactly what math3d did twice in a row today.


Third census, 2026-09-09 — 7 of 23, and one cause is now the biggest after atan2

Same instrument as the second census: every lekkerzeilen/*.py compiled on its own with compiler/pascal26, FIRST error only, so every count here is a LOWER BOUND — a module with two causes shows one.

7 of 23 compile clean (geometry, __init__, math3d, rd, scenery, shaders, wake). Unchanged in COUNT from the second census, and that is the honest reading: this pass removed a blocker from chart without unblocking it, and chart now fails on the cause below instead.

cause modules ticket
math.atan2 absent hud, rig, sim, traffic, vessel (5) feature-nilpy-math-module-twelve-absent-names-measured
closed-world method dispatch chart, environment, wind, __main__ (4) feature-n-open-world-method-dispatch-on-a-dynamically-typed-receiver — direction settled 2026-09-09, cost is a runtime name lookup
import array app, audio, world (3) feature-n-the-array-module
import ctypes capture, gfx (2) (ctypes)
import queue gauges (1) (queue)
str.join argument shape text (1) bug-n-str-join-rejects-an-argument-shape-cpython-accepts

The two causes the second census named, both now reduced and both filed

The second census recorded them as unreduced first-error readings and deliberately did not file them. Reducing changed what one of them WAS.

chart:191 "cannot infer the type of field self.z0" reduced to four lines and turned out to have nothing to do with chart or with tuple unpacking: a class field assigned from a bare LOCAL of the same method had no inference arm at all, where a literal, a parameter, a module global, a global holding an instance, a global holding a def, and None each had one. Fixed (bug-n-a-field-assigned-from-a-bare-local-has-no-inferable-type). It unmasked a THIRD gap behind it at chart:184self.width = self.height = max(...), a chained assignment to two attributes, which does not parse; the module-level a = b = 3 does. Filed as bug-n-a-chained-assignment-to-two-attributes-does-not-parse, and it is pre-existing at the pin, not a consequence of the fix.

environment/wind/__main__ at :141 "no class declares a method or callable field .contains()" is one defect at one line — wind.py:141, which the other two reach through their imports — and reducing it made it BIGGER, not smaller: chart:102 is the same cause (tile.read_grid("bed")), so it blocks four modules, not three. It is the first cause on this umbrella that is a genuine FORK rather than a gap: NilPy resolves a method call on a dynamically typed receiver by scanning the classes declared in the compilation unit, and the closed world is deliberate — it is what catches a typo on a variant receiver. canopy's class lives in world.py, which no module in the package imports, because that is what duck typing IS. The recommendation and the measured state of the machinery are on the ticket.

What did NOT need filing

math.atan2 stays the single largest cause at five modules and already has its ticket. Nothing in this pass changes it.

FOURTH CENSUS, 2026-09-09 (frankB) — the stdlib surface, and what a first-error census cannot tell you

Group taken as one question: what of the stdlib surface does a real program actually reach. Three tickets — feature-n-the-array-module, bug-n-collections-deque-is-missing, bug-n-str-join-rejects-an-argument-shape-cpython-accepts — all three fixed and verified against CPython. Census total unchanged at 8 of 23.

THE FINDING THAT MATTERS MORE THAN THE THREE FIXES

Every "unblocks N modules" figure on this umbrella counts modules where a gap is the FIRST wall, not modules the gap is sufficient to clear. A compiler stops at the first error, so a blocker census built by reading the first error of each module can only ever say that. Measured this pass, at compiler 418064fca1d3:

module ticket said wall before wall after
world array unblocks it no unit named array no unit named struct
audio array unblocks it no unit named array dispatch (.queued)
chart deque unblocks it collections.deque dispatch (.read_grid)
text join unblocks it join overloads compiles

Three of four had a second wall behind the first. One did not. Nothing was wrong with the measurements that produced those numbers — they were honest first-error readings — but the NUMBER means "first wall here", and it has been read as "modules this would finish". Read every count on this umbrella that way, including the ones below, and expect the total to move by less than the sum of the parts.

The cheap correction is to re-measure a module AFTER landing, which is what produced this table, and to say in the resolution which wall moved rather than which module was freed.

The three, landed

What writing them turned up in the COMPILER

Two silent defects, both fixed here, neither reported by anyone:

One filed rather than fixed, because it needs new machinery in a hot path: bug-n-an-overloaded-constructor-is-picked-by-name-ignoring-argument-type — two same-arity constructors are not told apart, the first one runs, silently. The FUNCTION spelling of the same thing is correct, which is the control.

Blocked-by, re-measured — what actually stands between here and 23

Counts below are written in the form this census can actually support. frankH's point, and it is the right one: a caveat sitting NEXT to a number gets read as care about the number rather than as a scope on it, so the population goes INSIDE the phrase or it does not travel. Every figure here is a first-wall count, not measured as sufficient.

cause first-wall count; not measured as sufficient
math.atan2 (refused, correctly rounded libm) 6 — hud, rig, sim, traffic, vessel, and one more
open-world dispatch on a dynamic receiver 4 — audio, chart, environment, main, wind
ctypes (settled: bind natively, do not shim) 2 — capture, gfx
queue 2 — gauges, app
struct 1 — world

The null row is what makes this readable as a measurement rather than a number (frankH again): four first walls were cleared this pass and the total moved by zero. A census with no null row prints a figure where the honest answer is "unresolved" — so the zero is recorded here deliberately, not apologised for.

feature-n-the-struct-module and feature-n-the-queue-module are filed with their measured surfaces. Neither needs a compiler change; both are shims, and queue's real question is the BLOCKING semantics, not the container — pylib's new TPyDeque is already the right storage for it.

FIFTH CENSUS, 2026-09-10 (frankuser) — 9 of 28, and the DENOMINATOR moved

Re-measured at HEAD, compiler 61f8a78f8aae, tree 31dad27bd, same instrument as every census before it: every lekkerzeilen/*.py compiled alone, first error only, so every count is a LOWER BOUND.

9 of 28 compile clean — figure, geometry, __init__, lines, math3d, rd, shaders, text, wake.

The denominator is the headline, not the numerator. Previous censuses said of 23; the tree has 28 runtime modules now. lekkerzeilen is the owner's own actively developed project — five modules were ADDED since the last census (bindings, figure, lines, session, ui) and scenery.py was edited today at 10:50. So the ratio's denominator moves under the measurement, and a numerator compared across censuses is comparing two different populations. Quote this as "9 of 28 at tree X on date Y", never as a trend against 8 of 23.

Of the original 23, text newly passes (its str.join ticket closed) and scenery newly FAILS — and that is not a regression on our side: the owner's 7da065e added _sin = math.sin, filed as [[bug-n-a-stdlib-function-referenced-without-calling-it-is-not-a-value]]. Attributed before reporting, because a number moving in the unfavourable direction invites exactly the self-blaming reading that terminates a search.

cause modules ticket
math surface hud, rig, sim, traffic, vessel (atan2), scenery (sin as a value) — 6 feature-nilpy-math-module-twelve-absent-names-measured + the new value ticket
open-world dispatch on a dynamic receiver audio .queued, chart .read_grid, environment / __main__ / wind .contains5 feature-n-open-world-method-dispatch-on-a-dynamically-typed-receiver
queue app, gauges — 2 feature-n-the-queue-module
ctypes capture, gfx — 2 (bind natively, settled)
struct world — 1 feature-n-the-struct-module
os data attributes session — 1 bug-n-os-environ-and-os-sep-are-not-values
platform.KEY_ESCAPE bindings — 1 NEW, unfiled — qualifier resolves, member does not
generator expression as sole argument ui — 1 NEW, unfiled — " ".join(x for x in ...) at ui.py:576, expected ')' before 'for'

Two new causes this census, both in modules that did not exist before, which is the clearest statement of what attempting a moving target costs: the backlog does not converge on a fixed 23, it tracks whatever the owner writes next. The ui one is a PARSE error and therefore the cheapest-looking of the lot — a bare generator expression passed as a function's only argument.

Delta to the fifth census, 2026-09-10 (frankB) — struct and queue landed, and the total moved by zero AGAIN

Not a sixth census: frankuser's numbers above were re-measured independently here after mimic_struct and mimic_queue landed, same instrument, same tree family, and the clean set is identical — figure, geometry, __init__, lines, math3d, rd, shaders, text, wake. Still 9 of 28. Recorded as corroboration rather than as a new figure, because two agreeing counts from the same method are one count, not two.

What moved is WHICH wall each blocked module stops at:

cause first-wall count; not measured as sufficient change
math.<name> — atan2, and sin in scenery 7 — hud, rig, scenery, sim, traffic, vessel, world +1
open-world dispatch on a dynamic receiver 5 — audio, chart, environment, __main__, wind
ctypes (settled: bind natively, do not shim) 2 — capture, gfx
threading 2 — app, gauges was queue
platform.KEY_ESCAPE through a unit qualifier 1 — bindings
undefined variable (os) 1 — session
*-unpacking into a method with defaults 1 — ui

struct and queue are gone from the table; world moved into the math group and app/gauges moved to threading.

THE NULL ROW, FOR THE SECOND CONSECUTIVE PASS. Six first walls have now been cleared across two passes — array, deque, str.join, sorted-over-bytes, struct, queue — and the count of modules that compile has moved by zero both times. That is not a disappointing result to be apologised for; it is the measurement this census exists to produce, and two null rows in a row say something the first one could not: clearing a module's first wall essentially never clears its last. Every module behind a shim wall had at least one more behind it.

The practical consequence for ranking: an import-level wall is worth much less per ticket than its first-wall count suggests, because imports sit at the top of a file and are therefore over-represented as first errors. math at 7 and open-world dispatch at 5 are the two that would actually move the numerator, and neither is a shim.

feature-n-the-threading-module (prio 60) is filed with its measured surface — four names — and with the note that mimic_queue must gain a lock and a real wait in the same change. It is the LAST import-level wall in both app and gauges; every other import in both files resolves, measured per-import.

SIXTH CENSUS, 2026-09-10 — prio 90, and the seam is a STUB

Owner, 2026-09-10: "well, this demo app has prio", and "notice that we don't care compiling tooling right now". Hence prio: 90, the top of the board — above umbrella-pxx-compiles-fpc-itself at 85.

His tooling caveat does not narrow the population, which is itself the finding. The import closure from __main__ reaches every module in the package but __init__. The tooling (tools/import_nl.py and friends) is outside it and was never in any census here. So there is nothing to exclude.

And the previous censuses were measuring too SMALL a population, not too large: lekkerzeilen/platform/ is a SUBPACKAGE, and every census including mine this morning globbed *.py and missed all five of its modules. The app is 32 modules, not 23 or 28.

8 of 32 at compiler b7745aaf0a59, tree 3bebb551e

Clean: figure, geometry, lines, math3d, rd, shaders, text, wake. All five platform/ modules fail.

cause modules note
math.atan2 hud, rig, sim, traffic, vessel, world — 6 world arrived here from struct
open-world dispatch audio, chart, environment, __main__, wind — 5 direction settled
ctypes above/below the seam capture, gfx, platform/_ctypes_backend, _gl, _sdl2 — 5 three are the CPython backend and NOT needed under pxx
staticmethod as a value platform/__init__, platform/_pxx — 2 (+bindings by cascade) [[bug-n-staticmethod-is-not-a-value]]
threading app, gauges — 2 arrived from queue
math.sin as a value scenery — 1
os data attributes session — 1
* unpack at a method call ui — 1 arrived from the generator-expression wall

The null result, for the third census running

queue and struct both landed (1eb448030) between my morning census and this one. The total moved by zero. app/gauges went queue -> threading, world went struct -> atan2, ui went generator-expression -> *unpack. Four walls cleared, four walls behind them. This is the fourth independent confirmation that a first-wall census cannot predict sufficiency, and it is now the expected behaviour of this umbrella rather than a surprise.

THE ACTUAL DISTANCE, WHICH NO RATIO SHOWS

platform/_pxx.py is a 39-line stub whose every entry point raises NotImplementedError. The app's portability seam has two backends — _ctypes_backend (327 lines, CPython, works) and _pxx (not written). So even with all 32 modules compiling, the demo would not run. Filed as [[task-b-write-the-lekkerzeilen-pxx-platform-backend]], prio 85, and it is the headline item. Its real blocker is [[bug-c-inline-asm-constraint-q-is-unsupported-and-it-blocks-every-sdl-header]], which has sat in backlog-cfront while both of its sibling header bugs were fixed and closed, and is now on the critical path of the top-priority target.

Edges rewired this pass

Five of the nine blocked-by entries were in done/ — array, deque, str.join, the GL lowercasing bug, the subdirectory bug — so the umbrella's own ranking had gone stale in the direction that understates it. Replaced with the live set.

ctypes IS THE GATE — from the consumer, 2026-09-10

neo-dd, asked which of the four broken stdlib imports matters most:

"of your real four: ctypes is the one that decides whether lekkerzeilen ever runs on PXX at all, and it is not a library problem. Our entire graphics and window layer is SDL2 and OpenGL hand-bound through ctypes — that is a deliberate constraint, not an accident, and it is why we have no pygame and no moderngl to port. sqlite3 is second: the world tiles are a SQLite database. zlib and threading we could live without in a pinch."

So the module-count census is not the ranking. Of the 14 stdlib modules this runtime imports, 10 work; of the four that do not, ctypes alone decides whether the program can run, and it is the one that is not shim work.

And the obligation has been lifted from their side (their owner, 2026-09-10): "it's up to pxx to get on par with cpython ... for now, we focus on zeilen functionality and should not limit ourselves too much." They are not building shims and not shaping the program around our gaps. Read that as removing an obligation, not withdrawing interest: they stay a truthful corpus and answer measurements. So a gap this umbrella finds is OUR ticket, and "lekkerzeilen could work around it" is no longer an argument for deprioritising one.

An offered fixture nobody has taken. lekkerzeilen/capture.py is a self-contained PNG encoder, ~90 lines of plain Python over zlib.compress and zlib.crc32, no third-party imports, deterministic output. Pointed at a fixed RGBA buffer on both runtimes it exercises crc32 arity, compress arity, bytes/bytearray slicing and struct-free big-endian packing in one go, and fails loudly. neo-dd will produce the fixed input buffer on request. That is a conformance fixture with its own oracle for the cost of asking.

THE DEMO WOULD RUN WRONG EVEN WITH EVERY MODULE COMPILING — RETRACTED IN FULL, 2026-09-11

RETRACTED BY ITS OWN AUTHOR (frankuser), SAME DAY, AND THE RANKING GOES WITH THE WORDING. There is no third gate. lekkerzeilen's four sites use the RELATIVE spelling (from .platform import ...) and the relative spelling does not leak. frankZ settled it by building a binary that disables only their own hunk (83b883221d70 = HEAD minus the SoftUnitMissed fix, nothing else) and running the positive control in the same run: the absolute shape leaks to None on that binary, proving the bug is present, and the relative shape gives 27 anyway. So this section's conclusion is false, its four sites were never affected, and the 29-names-across-4-sites count is a true statement about the source and a false one about exposure. Two gates on this umbrella, not three.

What survives, stated as narrowly as it should be: a real silent wrong-value bug on the ABSOLUTE from-import spelling of a module containing a guarded import, present in pin 095ef4811a5b (v407), fixed at 0f0c04b8b. Zero known live sites in lekkerzeilen. Worth having fixed — silent, in the pin, ordinary construct — and it does not gate this umbrella. Do not rank it as if it does.

All three readings of those four sites were wrong in turn — mine (silent None), frankZ's (hard error instead), and the truth (no effect). Mine failed on an untested spelling; theirs failed on a MASKED CONTROL, because the pin cannot compile from .subpackage import NAME at all, so both arms failed for a reason unrelated to the subject and the probe read as a clean discriminating result. A control only controls if both arms can actually exercise the mechanism; two failures that agree are not a comparison. The thing that separated all three was building a binary that isolates ONE hunk.

The body below is kept as history because the mechanism it describes is real — only its applicability to this umbrella was wrong. Do not act on it.

Measured 2026-09-11 (frankuser), from frankZ's SoftUnitMissed finding, with a control. This is a silent wrong VALUE, not a compile error, and it is on this umbrella's critical path rather than beside it.

The mechanism, reproduced independently before relaying it:

# pkg/__init__.py            # m.npy
VALUE = 27                   # from pkg import VALUE
try:                         # print("v", VALUE)
    from no_such_module import Image
    have = True
except ImportError:
    have = False
subject (package guards an import) control (same package, no guard)
CPython v 27 v 27
pin 095ef4811a5b (v407) v None v 27
HEAD 35dce79343cd v None v 27

The control is what makes it a finding rather than a broken fixture: remove the guarded import and the identical package gives 27 under both compilers. So the guard is the trigger. No diagnostic at any point.

WHY IT LANDS HERE: lekkerzeilen/platform/__init__.py IS THE ONLY PACKAGE IN THE CORPUS THAT GUARDS AN IMPORT (try: import ctypes / except ImportError), and four modules from-import names straight out of it, the entry point among them:

  lekkerzeilen/__main__.py:43  from .platform import KEY_DOWN, KEY_ESCAPE, QUIT, RESIZE, gl
  lekkerzeilen/app.py:26       from .platform import (ARROW_DOWN ... QUIT, RESIZE, gl)   23 names
  lekkerzeilen/gfx.py:10       from .platform import gl
  lekkerzeilen/capture.py:14   from .platform import gl

Every one of those names is a plain module-level constant on the other side — KEY_ESCAPE = 27, QUIT = "quit", KEY_DOWN = "key_down", RESIZE = "resize". Under this defect they all bind to None, silently, in the demo's own entry point.

So the module count was never going to be the measure, for a second reason nobody had named. The umbrella already records that ctypes and the unwritten SDL/GL backend gate whether it RUNS. This is a third gate and it is the nastiest of the three, because it produces no error at any stage: clear every wall, write the backend, and KEY_ESCAPE still compares against None.

NOT EXECUTED, and the distinction matters: those four modules do not compile yet (the ctypes wall), so I have not OBSERVED None in lekkerzeilen. What is measured is the mechanism, the pin's behaviour, and the presence of the exact shape at four sites with plain constants on the other side. Treat it as certain in mechanism and unobserved in situ — and re-check it the moment platform/ compiles, because that is the first opportunity to see it directly.

frankZ has the fix at the same choke point (clear the flag before resolving, and again afterwards when CompiledUnitCount went up). Until a pin carries it, this is a case where the pin is ACTIVELY WRONG about a construct portable Python packages write as a matter of course — a different and stronger argument than "a fix is inert until pinned". Cross-referenced on bug-t-armed-autopin-has-refused-62-consecutive-times-....

RE-MEASURED ON LEKKERZEILEN'S LITERAL GUARD, AND AT origin/master — 2026-09-11, binary 2b72db6e96a1

The table above used from <absent> import X as the guard; lekkerzeilen writes import ctypes, a plain import. Different statement form, different code path, so that was a generalisation step. Removed by measuring all three forms plus the control, against the pin and against a binary built from origin/master tip 8b188a3be:

guard in pkg/__init__.py CPython pin 095ef4811a5b HEAD 2b72db6e96a1
import ctypeslekkerzeilen's literal line 27 None None
import <absent module> 27 None None
from <absent> import Image 27 None None
no guard at all (control) 27 27 27

So it is not a family resemblance: the exact construct in lekkerzeilen/platform/__init__.py returns None, and the guard is provably the variable because the control differs in nothing else.

HEAD IS STILL BROKEN AT origin/master, AND A PEER'S "HEAD IS FIXED" WAS ABOUT A PRIVATE TREE. frankZ reported HEAD printing 27 for both forms and asked me to relax the claim. Their fix is real but unpushedgit log origin/master -S'SoftUnitMissed' -- compiler/ returns nothing, and the newest compiler commit on origin is 53c3c3f45 (the dead-arm fix). They measured their own working tree and called it HEAD, which is the ordinary meaning of the word from inside a session and the wrong one for anybody else. Had I taken the correction, the owner-facing version would have said HEAD was clean.

The rule this is an instance of: a claim about "HEAD" from a peer is a claim about THEIR checkout until the sha is on origin. The discriminator is one command and it is the same one this repo already prescribes for quoting a sha — git merge-base --is-ancestor <sha> origin/master, or for an unlanded change, git log origin/master -S'<identifier>'. Cheap, and it is the difference between "fixed" and "fixed somewhere you cannot build from".

What does NOT change: the pin column, which is the load-bearing half for the pinning argument, and the four from-import sites. What tightens: the construct is now lekkerzeilen's own, not a cousin of it.

And the not executed label still stands exactly as written — the mechanism, the pin's behaviour on the precise construct, and the four sites with plain constants opposite are measured; None arriving in lekkerzeilen is not, because platform/ does not compile yet. frankZ asked for that label to be kept and they are right to.

FIXED AT HEAD 0f0c04b8b, STILL WRONG IN THE PIN — and the population claim re-done with a SECOND FILTER

frankZ pushed the fix (it had been uncommitted, not merely unpushed). Re-measured on lekkerzeilen's literal import ctypes guard at origin tip, binary f1817610c98e:

pin 095ef4811a5b HEAD f1817610c98e
import ctypes guard None 27
no guard (control) 27 27

That is the cleanest form this argument can take: the fix exists, it is landed, and the pin does not have it. Nothing about the demo's correctness is unknown any more — only which compiler you build with.

THE POPULATION CLAIM WAS RIGHT AND MY FILTER WAS WRONG, which frankZ asked for a second opinion on precisely because it was load-bearing for ranking and rested on one grep by one author. tools/lekkerzeilen_guarded_import_census.py is the second filter, built to fail differently — STRUCTURAL (any ast.Try whose body contains an Import/ImportFrom, whatever the handlers say) where mine was TEXTUAL (grep -rln 'except ImportError'):

  lekkerzeilen/platform/__init__.py:91   import ctypes                   handlers: ImportError
  lekkerzeilen/__main__.py:216           from . import app, session, world
                                                   handlers: platform.PlatformError | OSError
  lekkerzeilen/app.py:408                import shutil                   handlers: Exception

Three guarded imports, not one. My grep missed two, because neither handler contains the string ImportError at all. Had either been a package __init__.py I would have under-reported the population — the conclusion survived by luck, not by method, and that is the honest way to record it.

Why the conclusion still holds: only platform/__init__.py is a package __init__.py, and the other two files are never from-imported (grep for from .app import / from .__main__ import: zero sites). No importer exists to poison. So the four sites stand.

AND THE CRITERION IS BROADER THAN I STATED, which matters for future code. A function-local guard leaks just as well as a module-level one:

guard placement in pkg/__init__.py pin HEAD
module level None 27
inside a function None 27
none (control) 27 27

So the rule is any guarded import anywhere in a package's __init__.py poisons every from-import of that package — not "a module-level guard". Today that is one file; it is one try: in any __init__.py away from being more, which is why the census is a committed tool rather than a number in this ticket.

THE CRITERION TOOK THREE TRIES — and package-ness was never a dimension

frankZ widened it a third time and was right a third time. Measured, both compilers, one no-guard control per row:

module kind guard placement CPython pin 095ef4811a5b HEAD f1817610c98e
package __init__.py module level 27 None 27
package __init__.py inside a function 27 None 27
plain module module level 27 None 27
plain module inside a function 27 None 27
either no guard (control) 27 27 27

So the rule is: any from-imported module containing a guarded import ANYWHERE poisons every name of that from-import. Not a package, not module-level. The package-ness was an artefact of platform/ being where both of us happened to look.

The three criteria, because the tool is only as good as the criterion and a widened filter is exactly when nobody questions it again:

  1. grep -rln 'except ImportError' — TEXTUAL. Missed two of three guards here, whose handlers are Exception and platform.PlatformError | OSError.
  2. ast.Try + "is it a package __init__.py" — encoded PACKAGE-NESS, which is not a dimension at all.
  3. ast.Try at any depth in any module, cross-referenced against whether that module is actually from-imported. The from-import side is what makes a site live.

tools/lekkerzeilen_guarded_import_census.py implements (3) with controls drawn from the two things criterion (2) got wrong: its positive control is a PLAIN module with a FUNCTION-LOCAL guard that is from-imported, so a tool that still filters on package-ness or placement fails it. Negative control: a guarded module nobody from-imports must NOT be reported live.

Result — the conclusion holds, now with an exact count:

  SUSPECTS (contain a guarded import)                      3
  LIVE (from-imported, so actually poisoned)               1  -> module `platform`
      __main__.py:43    5 names   KEY_DOWN, KEY_ESCAPE, QUIT, RESIZE, gl
      app.py:26        22 names   ARROW_DOWN ... QUIT, RESIZE, gl
      capture.py:14     1 name    gl
      gfx.py:10         1 name    gl
                       ---------
                       29 names across 4 sites
  DORMANT (guarded, not from-imported)                     2  -> __main__, app

29 names, not "four sites". And the two dormant suspects are the forward-looking risk: app.py:408 is import shutil inside a try in a plain module, one from .app import ... away from being live. That is why this is a committed tool with controls rather than a number in a ticket.

(Earlier revisions of this section said 23 names at app.py:26; the AST counts 22. Eyeballed from source the first time.)

RETRACTED — LEKKERZEILEN'S FOUR SITES WERE NEVER AFFECTED. THE "THIRD GATE" DOES NOT EXIST

Settled 2026-09-11 by building the binary that discriminates. Everything above about 29 names reading as None in this demo is WRONG, and the error is mine: every leaking cell either of us measured used an ABSOLUTE from-import (from pkg import X). Lekkerzeilen's four sites are RELATIVE (from .platform import gl). Different spelling, never tested, and I wrote the conclusion as though it had been — the exact generalisation step I had made frankZ remove from their own table an hour earlier.

frankZ caught it and their first probe could not settle it either: on the pin, lekkerzeilen's shape REFUSES with undefined variable (KEY_ESCAPE)and so does the same shape with no guard at all. The pin cannot compile from .subpackage import NAME for a reason unrelated to guarded imports, so on the pin the guard is not the variable and neither reading is supported.

The binary that discriminates is HEAD with frankZ's fix reverted in place — every later fix present, this one absent. Built it (d53760a81f16, reverse-applied the hunk, rebuilt, measured, restored, rebuilt, verified the fix back at 27):

CPython d53760a81f16 = HEAD minus the fix
absolute from pkg import X, guarded 27 None ← the defect, present
absolute, no guard (control) 27 27
relative from .platform import X, guarded — lekkerzeilen's exact shape 27 27
relative, no guard (control) 27 27

The absolute row is the positive control: it proves this binary HAS the defect, so the relative row's 27 is a genuine negative and not a masked one. That is the whole reason the control binary was worth building — without it, "relative gives 27" is equally explained by a binary that never had the bug.

So: the defect is specific to the ABSOLUTE from-import spelling, and this demo does not use it. No silent None, at any of the four sites, on any binary.

What survives, precisely:

Why it survived four rounds of widening. The chain was except ImportError → package-ness → placement → absolute-vs-relative, and each widening corrected a real error, which is what made the next assumption invisible. Absolute-vs-relative was never examined because from-import-ness felt like the subject rather than a dimension — the same way package-ness had for me. The count was right, the mechanism was right, the population was right, and the SPELLING was the variable nobody varied.

ctypes MUST BE ALL-OR-NOTHING, AND A PARTIAL SHIM IS STRICTLY WORSE THAN NONE

Measured 2026-09-11 (frankuser), compiler c53cb51926a2, and it is a NEGATIVE result that closes a tempting path. I wrote a minimal lib/rtl/mimic_ctypes.pycreate_string_buffer plus the scalar width aliases, differential-clean against CPython on every row — on the reasoning that the only LIVE ctypes user is capture.py, 49 lines of screenshot code using exactly one name.

Recorded expectation before the re-run: 27 → 30. Measured: 27 → 25.

regressed (compiled before, fails now) bindings.py, platform/__init__.py
improved none

THE MECHANISM, AND IT IS THE WHOLE FINDING: import ctypes SUCCEEDING IS A SIGNAL THE APPLICATION USES TO CHOOSE ITS BACKEND. platform/__init__.py is

try:
    import ctypes
    from . import _ctypes_backend as _backend    # needs CDLL, POINTER, Structure, ...
except ImportError:
    from . import _pxx as _backend               # the native arm

so the moment ctypes resolves at all, the seam stops taking the native arm and commits to the arm that needs the full FFI. A partial shim therefore does not buy partial progress — it moves the app onto the path it cannot walk, and it takes the seam and bindings.py down with it. The shim was removed rather than landed.

The general shape, which is not about ctypes: where a corpus uses try: import X as a CAPABILITY PROBE, a partial mimic_X is not an increment, it is a false answer to the probe. Shimming is the right default here (owner, 2026-09-10) and this is the exception with a test attached: before shimming a module, grep the corpus for try: import <that module> — if the import is a probe, the shim has to satisfy everything behind the arm it selects.

What the ctypes surface actually is, which survives the negative result

22 distinct ctypes.<name> uses, 180 sites, five files:

file sites reachable under pxx?
platform/_sdl2.py 76 no — backend arm not taken
gfx.py 60 nonothing in the corpus imports it
platform/_gl.py 27 no — backend arm not taken
platform/_ctypes_backend.py 16 no — backend arm not taken
capture.py 1 yes, lazily from app.py:4129

14 of the 22 names are scalar width aliases. The machinery is eight: CDLL, POINTER, Structure, byref, cast, sizeof, CFUNCTYPE, and the ctypes.util submodule — all eight used only by the four unreachable files.

The fork, stated in goal terms because it is the owner's

The pieces for a real CDLL exist — lib/rtl/dynlibs.pas has LoadLibrary and GetProcedureAddress, and the ELF writer emits DT_NEEDED. What is missing is what CPython uses libffi for: calling a pointer with a signature chosen at run time, where a Pascal call through a pointer needs a procedure type known at compile time. So the question is not "implement ctypes" but:

Do we want pxx to be able to call any C library a Python program names at run time, or only to run this demo natively?

The first is a bounded-signature call gateway (N integer/pointer/float arguments and a few return kinds would cover all of SDL and GL) and serves every future ctypes-using corpus. The second is the ~327-line native platform/_pxx.py this umbrella already names, and touches nothing else. Both leave the four unreachable files unreachable, so NEITHER moves the census past what the native arm needs — which is the part a reader of the wall histogram would get wrong.

2026-09-11, frankZ — 28 of 35 HELD ACROSS THE SEAM REWRITE, which is the row that was open

Independent re-measure at compiler 465845b20d1e, tree 3951695c8, corpus 63adc17 — a different compiler and a different corpus revision from the f9fb672ee109 / 2a3d60e run in the summary, so this is a second reading and not a repeat of that one.

28 of 35 modules compile
walls, by modules hitting each FIRST:
   6  import: no unit named ctypes and no shim mimic_ctypes
   1  no member create_string_buffer came of the qualifier ctypes

Expectation recorded BEFORE the run, per this file's own instruction: 28 of 35, unchanged, ctypes dominant. Matched.

What was actually at risk, and why the null row is the finding

Corpus 63adc17 is "platform: restore the try/except/else spelling now that pxx parses it" — the seam has been put back on the faithful three-clause form it wanted all along, so real application code now depends on bug-n-try-except-else-does-not-parse-when-the-try-body-is-an-import (fixed cc311ec6e). That fix has two layers, and the second one exists because the first alone binds the DEAD arm's module through a first-wins alias table, silently, exit 0. The seam binds _backend in both arms, which is exactly that shape.

So the question this run answered was not "did the count move" but "does the faithful spelling select the right backend in real code, or only in my fixture?" A regression here would have been mine, not the corpus's, and it would have shown up as a count BELOW 28. It did not: all seven remaining walls are ctypes, none is an arm-selection failure, and nothing that compiled before stopped compiling.

A fixture asserts a construct in the shapes its author chose. This is the same construct in the shape the application writes, against a backend selection that matters. Recorded because a null row is only information to someone who said what they expected — and because "my fixture passes" and "the seam works" are different claims, which this corpus has already taught once (a compile is not a run, 2026-09-11).

Goal 4's remaining question is unchanged and is still the owner's, stated in the summary: native-library access for NilPy programs, or this app reaching the native layer through pxx's own binding mechanism with its source changed to suit. Seven modules, one cause. No compiler ticket is blocking it.

THE TWO SECTIONS ABOVE AND BELOW AGREE, AND THEY MEASURE DIFFERENT THINGS

Resolved by keeping both (frankuser, 2026-09-11) — they landed as a conflict only because both were appends. frankZ's run above is an as-subject census and reports 28 of 35 with six modules on no unit named ctypes and one on no member create_string_buffer. Mine below is the entry-point closure and reports that single create_string_buffer row as the only error in the whole closure. That is the same wall — capture.py:44 — seen through two instruments, and the six-module row is the one that does not survive the change of question, because those six sit in a backend arm the closure never enters.

So frankZ's null row and my one-error closure are the same measurement from two sides, which is worth more than either alone: the census says nothing regressed across the seam rewrite, and the closure says what is actually left.

One correction to the section above, and it is to its LAST paragraph rather than to any of its numbers: "Goal 4's remaining question is unchanged and is still the owner's" — it is not. The closure below shows the demo needs no FFI at all, and the owner's own _pxx.py docstring specifies the native backend as having "no create_string_buffer". The question is still real for other programs; it is no longer goal 4's gate. Seven modules, one cause is correct as a census row and is not the demo's distance.

THE ENTRY-POINT CLOSURE IS ONE WALL FROM COMPILING, AND THE FORK IS NARROWER THAN THIS TICKET HAS BEEN STATING — 2026-09-11 (frankuser)

Every census on this umbrella, mine included, has compiled each module as a subject and reported a ratio. The demo does not run modules; it runs python3 -m lekkerzeilen, which is lekkerzeilen/__main__.py. Nobody had compiled that and read the closure.

Measured at 7958322f8, binary 465845b20d1e, corpus clean:

$ cd /home/neo/lekkerzeilen
$ pascal26 --threadsafe lekkerzeilen/__main__.py /tmp/lzmain

27 lines of output. Seven shim notes, a handful of run-time-dispatch warnings, and exactly ONE error:

pascal26:44: error: no member create_string_buffer came of the qualifier ctypes
  in: lekkerzeilen/capture.py

That is the whole remaining compile distance for the demo. Not seven modules — one member access, in one function, in one file.

Why the 28-of-35 number was not telling us this

A module census counts subjects, and this corpus has dead code in it: nothing imports gfx.py, and four of the five ctypes files are in a backend arm pxx never takes. Those rows are real as-subject failures and irrelevant to whether the demo runs. The ratio and the closure answer different questions, and only the closure is goal 4's question. Keep reporting the ratio if you like — but the closure is the number to act on.

The seam's capability probe is CORRECT, which I had left open

platform/__init__.py selects its backend with try: import ctypes / except ImportError: ... / else: .... Probed directly:

try:
    import ctypes  # noqa: F401
except ImportError:
    print("except-arm: no ctypes")
else:
    print("else-arm: ctypes present")

pxx prints except-arm: no ctypes. So the seam picks _pxx and the native arm is genuinely the one under test. The all-or-nothing finding below is about what happens if a shim makes that probe answer differently; the probe itself is not broken.

Note the asymmetry, because it is the reason capture.py fails the way it does: the same import ctypes inside a try raises, while at capture.py's top level, reached as a dependency, it binds nothing and does not raise — so the error arrives at the member access, not the import. frankB measured the as-subject half of that on 2026-09-11.

What is BEHIND that wall — measured, in a scratch copy, not inferred

A first-failure census cannot see past a wall, so the line was stubbed to bytearray(...) in a throwaway copy of the package (the package only: cp -a of the whole tree copies the owner's terrain and filled /tmp, which cost a tier row — see bug-a-the-compiler-prints-ok-with-exact-byte-counts-for-an-output-it-failed-to-write).

The next wall is one line further down, capture.py:45:

error: .ReadPixels() is dispatched at run time (no class here declares it),
and that path takes at most 4 arguments

Filed as feature-n-a-runtime-dispatched-method-call-is-capped-at-four-arguments.

BOTH WALLS ARE THE SAME CAUSE, AND IT IS THE STUB

_pxx.py defines class gl: with five staticmethods — clear_color, clear, viewport, enable, get_string. It declares neither PixelStorei nor ReadPixels, so those calls fall to run-time dispatch, and the 7-argument one meets the cap. Declare the real surface and both walls go at once: the call resolves statically and MAX_DYN_ARGS never applies.

So the compile distance is not "clear ctypes, then clear an arity cap". It is write the backend, which was already this umbrella's second gate (task-b-write-the-lekkerzeilen-pxx-platform-backend). The two walls are that task reporting itself through the compiler.

THE DEMO NEEDS NO FFI, AND THE OWNER'S OWN FILE SAYS SO

The fork on this ticket has been stated as "do we want pxx to be able to call any C library a Python program names at run time, or only to run this demo natively?" The measurement narrows it to a yes/no, because the native answer needs no ctypes at all. _pxx.py's docstring, written by the owner:

this backend should be a translation of _ctypes_backend in which the ctypes machinery simply disappears: no CDLL loading, no restype/argtypes declarations, no create_string_buffer.

capture.py names ctypes for exactly one thing — a writable byte buffer to hand glReadPixels, read back through .raw. That is not FFI; it is a bytearray. Under the native backend the framebuffer read belongs behind the seam, returning bytes, and capture.py should not name ctypes at all.

So the demo is not waiting on a ctypes decision. It is waiting on the backend, and the backend's own spec excludes ctypes. The FFI question is a real question for other programs; it is not this umbrella's.

What NOT to do with this, and it is the same trap as every census above

create_string_buffer is one name and a bytearray is sitting right there, so the cheap move is to shim it. Don't. The section below measured what a partial mimic_ctypes does: import ctypes starts succeeding, the probe flips, the seam abandons the native arm for the FFI arm, and the count went DOWN 27 -> 25. Supplying one member makes the probe's answer wrong, not better.

The honest change is on the consumer side — move the framebuffer read behind the gl seam so capture.py asks the backend for bytes. That is a two-sided edit (_ctypes_backend keeps the ctypes dance where ctypes belongs, _pxx gets the native one) and it is pointless until _pxx.py exists, since the whole point is delegating to a backend that currently raises NotImplementedError. Left for whoever takes that task; not done here, deliberately.

blocked-by WAS SEVEN-TWELFTHS STALE, FOR THE SECOND TIME ON THIS TICKET

Pruned 2026-09-11: seven of the twelve entries were in done/ — the high-byte register bug, the dynsym derivation, the dead-guarded-import resolution, the threading module, open-world dispatch, the stdlib-reference-as-a-value bug, *-unpacking at a method call, and the chained attribute assignment. Five remain live.

The section at "Five of the nine blocked-by entries were in done/" above records the same thing happening earlier on this same ticket, which is why this is written down rather than quietly fixed. The edge is the ranker's input and the body is the history; a closed blocker left in the list makes an umbrella read as blocked on work that is finished, and next cannot tell the difference. CLAUDE.md already names this class — the math.atan2 ticket found closed by events 26 days earlier — so nothing new is being claimed, only that an umbrella accumulates it faster than a leaf ticket because it is the only place edges are written by hand.

One entry was worth keeping apart from the prune: feature-n-open-world-method-dispatch-on-a-dynamically-typed-receiver is done, and it is the feature that BUILT pydyn_meth0..4. The four-argument cap filed today is the ceiling that feature left behind, not a regression in it.

2026-09-12 summary, superseded

Kept verbatim because the with ... as entry is a diagnosis lesson, not a changelog row, and because this umbrella's summary is read far more often than its body.

2026-09-12, LATEST — ALL OF app.py NOW COMPILES (4344 lines of warnings, no error). CURRENT WALL is print(__doc__.strip()) in __main__.py's --help path — undefined variable (__doc__) — and CITE THE CONSTRUCT, NOT THE LINE: that file is modified on disk by an in-flight backend port, so it is line 312 in the owner's working tree and 299 at HEAD. The gap is real in both (each opens with the same leading docstring); only the citation moves — the module docstring dunder, read in the --help path as print(__doc__.strip()). UNASSESSED: nobody has yet sized whether NilPy should bind a module's leading string literal to __doc__, so do not read its position here as a decision that it should. READ THE LINE NUMBER CAREFULLY — it prints as a bare pascal26:312: with NO file name, the documented trap, and grep settled it: lekkerzeilen/__main__.py:312 is the only __doc__ inside the package (the nine others are in tools/*.py, which the closure does not reach). TEN WALLS CLEARED 2026-09-12, closure 1240 -> __main__.py:312. THE CHARACTER OF WHAT IS LEFT HAS CHANGED AND IT IS NOW MEASURED RATHER THAN PREDICTED: the first seven walls were parser gaps, and three of the last four were LIBRARY gaps — tempfile.mkdtemp (115 -> 138), os.rmdir beside it, and zlib.ZLIB_VERSION (141 -> 312). A seat picking this up should expect lib/rtl and builtin work, not parser work. TWO OF THOSE THREE ARE INERT FOR $(PXX_STABLE) UNTIL THE OWNER PINS, because os.rmdir is a BUILTIN addition (compiler/builtin/pylib.pas + pypal.pas); mkdtemp and ZLIB_VERSION are lib/rtl and live immediately. make pin is owner-only and nobody is waiting on one. ZLIB_VERSION WAS A VALUE DECISION, NOT A GAP, and it is the one thing here worth overruling cheaply: there is no libz under lib/rtl/zlib.pas (RFC 1950/1951 from scratch), so it reports pxx-rtl rather than a libz release number — the caller prints it to say WHICH zlib produced its bytes, directly above its own note that a differing file column is not a fault, so a spoofed version would defeat the line's only purpose. The cost is real and stated in the unit: a program doing version arithmetic on it raises where CPython returns numbers. The earlier narrative of this summary — including the with ... as diagnosis lesson, which is the one most worth reading — moved to the body section "2026-09-12 summary, superseded" rather than being dropped.

2026-09-12, LATEST — ALL OF app.py NOW COMPILES. CURRENT WALL __main__.py:115, no member mkdtemp came of the qualifier tempfile — blocker feature-n-tempfile-has-no-mkdtemp (track B, p70), and it is the FIRST wall in this closure that is a LIBRARY gap rather than a parser gap. All seven cleared before it were parser gaps, so the prediction this summary has been repeating ("what remains is parser gaps on constructs ordinary Python writes") has just expired — say so rather than carrying it forward. READ THE LINE NUMBER CAREFULLY: it prints as a bare pascal26:115: with NO file name, which is the documented trap — an error inside an imported module names no file and the reader supplies the one they invoked. Here grep settled it (the tree has exactly one mkdtemp) and it IS main.py, but do not read the next such line as app.py without checking. SEVEN WALLS CLEARED 2026-09-12, closure 1240 -> main.py:115, app.py fully past: extended-slice assignment (e3dcd6a35), a bare module attribute as a whole argument to a method (003154db3), chained assignment to nested attribute targets (203919198, corrected in b1215bf2e), a chain store through a VARIANT-typed receiver (da460243c), a float literal in a lambda body together with {/} (df8c96140), and with C(...) as NAME where NAME names a class. THAT LAST ONE IS THE ONE TO LEARN FROM AND THE LESSON IS ABOUT DIAGNOSIS, NOT ABOUT with. The cause was not in the with parser at all: is and as share one arm in the SHARED PASCAL expression parser, is carried a not PyExprMode guard and as did not, and FindUClass is case-INSENSITIVE — so as window resolved to class Window and that arm ate it as a CAST. Two failure modes from one cause, and the loud one was not the one that mattered: a NEW target name gave undefined variable, an ALREADY-BOUND one COMPILED AND KEPT THE OLD VALUE with no diagnostic. The ticket records TWO of my own diagnostic errors because both would mislead the next reader: its "REFUTED" list named the correct cause and dismissed it from a grep scoped to pyparser.inc alone (the NilPy parser delegates its arithmetic atom to the shared ParseExpr, which is what builds the node — so the grep was honest about the file and was read as an answer about the compiler), and the headline "enter returns self" discriminator came from a four-row table that moved TWO VARIABLES, since every passing row also happened to use a non-colliding name pair. A one-variable ladder built from the WORKING program found it in three steps. ALSO FIXED, and it is a TEST defect rather than a compiler one: test_nilpy_the_threading_module asserted the order of two lines that were genuinely racing — gate.set() released the waiting thread BEFORE main printed — and it flipped once in a suite run sharing the box with a second compile. Thirty standalone runs at load 2.3 would not reproduce it, which is why it survived: it only flips when somebody else is measuring. set() now comes after the print, so the order is deterministic by construction and the row tests MORE than before. SUPERSEDED LEAD, 2026-09-12 — WALL WAS app.py:4237, undefined variable (window) on with platform.open_window(...) as window:. Blocker is bug-n-a-with-as-target-is-not-bound-when-enter-returns-self (p85), and it is PRE-EXISTING — identical on the PINNED compiler, so it is not a regression from tonight; it was simply hidden behind the walls in front of it. READ THAT TICKET BEFORE TOUCHING IT: the as target goes unbound only when enter returns SELF, which is the idiom nearly every context manager uses, and the arm that matters is not the refusal. With the name PRE-DECLARED the program COMPILES AND KEEPS THE OLD VALUE (prints 1 where CPython prints 3), so a fixture asserting only the undefined variable row would pass the moment the symbol gets allocated whether or not the assignment happens. Narrowed, not solved: PXXDBG=a.ast shows enter emitted as a DISCARDED statement, i.e. PyParseWithTail took its no-as arm, so the cursor was not on as when the check ran; PyCallMeth1 is the only candidate between the expression parse and the check and that is a HYPOTHESIS, not a measurement. Two theories are recorded as REFUTED on the ticket (the Pascal as cast — the kind=57 node in the dump is an IR coercion, not an AN_AS_CAST from the NilPy parser; and a syntactic recognition of return self — a class with no enter at all fails the same way). SIX WALLS CLEARED 2026-09-12 and the closure moved 1240 -> 4237 (+2997 lines): extended-slice assignment (e3dcd6a35), a bare module attribute as a whole argument to a method (003154db3), chained assignment to nested attribute targets (203919198, whose own message misquoted the closure as 3305 — corrected in b1215bf2e), a chain store through a VARIANT-typed receiver (da460243c), and a FLOAT LITERAL in a lambda body together with {/} in one. Every one was a PARSER gap on a construct ordinary Python writes, not a missing library, which is the useful prediction for what remains. TWO LESSONS WORTH MORE THAN THE LINE COUNT. (1) The variant receiver needed NO new machinery — the statement path had solved it long ago (an AN_CLASS_CAST over AN_CALL(pyvarobj), plus a run-time class dispatcher for fields at differing offsets) and the chain path simply never asked; grep before proposing a mechanism, and read the prose when the grep returns prose. (2) The float wall looked like three other things: app.py:3305 is lambda x, z: canopy.at(x, z, outside=ceiling + 1.0) <= ceiling, so the keyword argument, the comparison and the method call were all better suspects, and lambda x: 2.0 alone turned out to be enough. Every INT spelling compiled all along, which is the expected-value collision that makes an ordinary suite CERTIFY the gap rather than catch it. RETIRED HAZARD BLOCK, and it is worth knowing why it was right then and wrong now: this summary said in capitals that reaching 3305 by disabling the variant guard was NOT progress, because the same shape then stored NOTHING (CPython 11 11 against our 11 True). That was measured and correct. The 3305 that followed was a DIFFERENT 3305 — stores verified row by row against CPython, the ambiguous-layout case included. The block also prescribed "FIX THE RECEIVER'S TYPE, NOT THE STORE", and that was wrong: the receiver does get a class identity, but via the cast the STORE builder places. A hazard block decays silently in the direction of doing nothing — re-measure one before obeying it. SUPERSEDED LEAD, 2026-09-12 — WALL WAS app.py:3305, a float literal in a lambda body, now fixed. FOUR WALLS CLEARED TODAY and the closure moved 1240 -> 3305 (+2065 lines): extended-slice assignment (e3dcd6a35), a bare module attribute as a whole argument to a method (003154db3), chained assignment to nested attribute targets (203919198, whose own message misquoted the closure as 3305 — corrected in b1215bf2e), and a chain store through a VARIANT-typed receiver. All four were PARSER gaps on constructs ordinary Python writes, not missing libraries, which is the useful prediction for what remains — and the fourth says something sharper: THE FIX WAS NOT NEW MACHINERY, IT WAS ASKING A DOOR THAT ALREADY EXISTED. The statement path had solved the variant receiver long ago (an AN_CLASS_CAST over AN_CALL(pyvarobj), plus a run-time class dispatcher for fields at differing offsets); the chain path simply never asked. Before proposing a mechanism for a NilPy wall, grep for it — and when the grep returns prose, read the prose. RETIRED HAZARD BLOCK, and it is worth knowing why it was right then and wrong now: this summary said in capitals that reaching 3305 by disabling the guard was NOT progress, because the same shape then stored NOTHING (CPython 11 11 against our 11 True). That was measured and correct. The current 3305 is a DIFFERENT 3305 — the stores are correct, verified row by row against CPython including the ambiguous-layout case app.py actually needs. The block also prescribed "FIX THE RECEIVER'S TYPE, NOT THE STORE", and that prescription was wrong: the receiver does get a class identity, but via the cast the STORE builder places, so the work was in the store path after all. A hazard block decays silently in the direction of doing nothing — re-measure one before obeying it. SUPERSEDED LEAD, 2026-09-12 — CURRENT WALL app.py:3204, AND IT IS OUR OWN GUARD, DELIBERATELY: in a chained assignment, the receiver of visible has no static class here. Blocker is bug-n-a-chained-assignment-through-a-variant-typed-intermediate (raised 40 -> 85). DISABLING THAT GUARD REACHES 3305 AND IS NOT PROGRESS — measured, the same shape then compiles and stores NOTHING into the variant-receiver target (CPython 11 11 against our 11 True), so those 101 lines are bought with a silent wrong store, which is this umbrella's own recorded trap (the closure compiling is not the demo running). DO NOT "unblock" the closure by relaxing it. MECHANISM NARROWED 2026-09-12 and TWO EARLIER HYPOTHESES REFUTED BY MEASUREMENT: the dynamic setter is NOT dropped by the chain's sequence and the receiver is NOT copied by PyForceVariant — a chain storing to an UNDECLARED attribute through the SAME variant receiver gives CPython's answer exactly (22 22). The fault is a DECLARED field reached through a variant receiver: the chain's store goes dynamic while the read of a declared field does not, so they disagree and the read returns the initial value. The SINGLE-statement spelling of the same store is correct, so the difference is the RECEIVER NODE'S TYPE, not the store builder — PyMakeAttrLoad types self.menu from UFldTk, which is not a class, where the expression parser's own receiver for the same source is. FIX THE RECEIVER'S TYPE, NOT THE STORE. And narrowing the guard to allow the fresh-attribute case would NOT unblock app.py, whose field (visible) is declared — do not spend the attempt. THREE WALLS CLEARED 2026-09-12 and the closure moved 1240 -> 3204 (+1964 lines): extended-slice assignment (e3dcd6a35), a bare module attribute as a whole argument to a method (003154db3), chained assignment to nested attribute targets (203919198, whose own message misquoted the closure as 3305 — corrected in b1215bf2e). Every one was a PARSER gap on a construct ordinary Python writes, not a missing library, which is the useful prediction for what remains. SUPERSEDED LEAD, 2026-09-12: THREE WALLS CLEARED TONIGHT AND THE CLOSURE HAS MOVED 1719 LINES (app.py:1240 -> 1678 -> 2360 -> 2959). CURRENT WALL: app.py:2959, undefined variable (ui) on leading=ui.ROW -- filed as bug-n-a-bare-module-attribute-is-undefined-as-a-whole-argument-to-a-method (p75) with an 11-line repro. IT IS NOT THE KEYWORD-ARGUMENT BUG IT LOOKS LIKE: the POSITIONAL form self.m(1, ui.ROW) fails identically and b=1 + ui.ROW compiles, so the discriminator is a bare mod.ATTR occupying a WHOLE argument at a method/constructor site, which the plain-function path handles correctly -- the same argument-position speculative-parse family as ad7c03b03. THE EXTENDED-SLICE WALL IS CLEARED and the lesson is about the FIXTURE, not the feature: PyRewriteSliceAssign's refusal was only reachable for a STATICALLY typed target, because the slice-assign-target predicate named pylist_slice_step and pybytes_slice_step and NOT pyvar_slice_step, so a target with no static type fell out of the store path entirely and died on expected newline after statement -- and that is any name first bound inside an if or try body. All eight top-level spellings compiled; only the fixture's try/except rows, which rebind their target inside the block, exposed it. A fixture written the ordinary way would have certified two thirds of the feature as working. The diagnostic also misreported the LINE, naming the first top-level occurrence of a similar statement 46 lines earlier. SUPERSEDED LEAD, 2026-09-12: TWO MORE WALLS CLEARED AND THE CLOSURE HAS MOVED 1120 LINES TONIGHT (app.py:1240 -> 1678 -> 2360). CURRENT WALL: app.py:2360, assigning to an extended slice (with a step) is not implemented -- an ALREADY-FILED ticket, bug-nilpy-an-extended-slice-cannot-be-assigned, found by a pydiff sweep on 2026-08-16 and sitting at prio 30; it is now wired here and inherits effective_prio, which is the ranker working as designed. The diagnostic is honest and precise, so it is a missing FEATURE, not a misresolution. The pop wall before it (TPyDeque.pop() takes exactly 0 argument(s), got 2 on a dict field) was NOT the scan-order bug its ticket predicted: arFits and the arity promotion beside it asked the same question with different tests, so an overload-only match suppressed the run-time deferral and promoted nothing. SEVEN reductions of it failed and no regression fixture exists -- the closest passes pre-fix too, so committing it would be a guard that cannot fail; verification is this closure. One live bug found while reducing, filed separately: collections.deque() compiles and SEGFAULTS (p70). SUPERSEDED LEAD, earlier 2026-09-12: THE STAR-FOLLOWER WALL IS CLEARED AND THE CLOSURE MOVED 438 LINES FURTHER INTO app.py. A KEYWORD argument after a * unpack now works at all three METHOD sites; the fix was not the index-split the p75 ticket predicted but a CAP -- the compile-time expander was greedy (wanted := ParamCount - firstSlot, every remaining slot), and a keyword names its own slot, so stopping the star at the lowest slot a trailing name= claims is the whole change. CURRENT WALL: tile.grids.pop(name, None) refused with pop() takes exactly 0 argument(s), got 2 -- a dict field resolved against a list's or deque's overload set, filed as bug-n-a-dict-field-resolves-pop-against-a-list-or-deque-overload-set (p80). FOUR REDUCTIONS OF IT FAILED; the ticket lists them so nobody repeats them, and says the next step is an INSTRUMENT (the arity error does not name the receiver class) rather than a fifth reduction. Still refused and separately ticketed: a trailing POSITIONAL after a star (needs the run-time length), and the CONSTRUCTOR arm of the keyword fix (its loop resolves a keyword to a FIELD index, not a parameter slot). SUPERSEDED LEAD, 2026-09-11: WALL NOW FULLY DIAGNOSED AND FILED as feature-n-a-method-call-cannot-take-an-argument-after-a-star-unpack (p75): the SAME star-plus-trailing-argument shapes work on a free function and are refused on a method, because plain calls route to the run-time forwarder and the three method sites still use the compile-time expansion that claims every remaining slot. LATE 2026-09-11: the closure now fails on an argument after *unpacking is not supported yet in app.py -- a KEYWORD argument after a * unpack, f(*self.start[:2], forced=...), a shape feature-n-a-call-cannot-unpack-a-sequence-into-its-arguments did not cover and which is now the only thing between this umbrella and a compiling entry point, as far as a first-failure instrument can see. FOUR WALLS CLEARED IN ONE EVENING: ctypes in capture.py (corpus seam rewrite, uncommitted in /home/neo/lekkerzeilen); world.label(...) (55981bc63, f004e408e -- a unit-qualified member named like a Pascal reserved word); import threading (no code, just the --threadsafe flag its own diagnostic prescribes); and the type-widening refusal in App.open_region, which was NOT the conditional it appeared to be but world.open(...) being typed by fcntl.h's C int open(...) instead of world.py's own -- found only after the diagnostic was taught to print tyInt32 instead of 11. Still REQUIRES --threadsafe -dSDL_DISABLE_IMMINTRIN_H -dGL_GLEXT_PROTOTYPES; without the two defines you get a bogus avx512fintrin.h/MAX_PROC_PARAMS error that is not the real wall. DO NOT QUOTE AN app.py LINE NUMBER -- the owner edits it live; it moved 1195 -> 1217 -> 1233 -> 1240 in one evening. PREVIOUS LEAD, 2026-09-11 evening, SUPERSEDED: pascal26 --threadsafe -dSDL_DISABLE_IMMINTRIN_H -dGL_GLEXT_PROTOTYPES lekkerzeilen/__main__.py now walls inside app.py's App.open_region: annotate the type / too dynamic [a=11 b=22] (tyInt32 vs tyVariant) on self.world = scene if isinstance(scene, world.World) else None, filed as bug-n-a-field-assigned-a-class-or-none-in-two-methods-wont-widen and NOT YET REDUCED -- the identical construct on the same field compiles ~600 lines earlier in the ctor, and four reductions all pass. THREE WALLS CLEARED THAT EVENING: the ctypes wall in capture.py (corpus seam rewrite, deliberately uncommitted in /home/neo/lekkerzeilen); world.label(...), which was a COMPILER bug and not a corpus one (55981bc63 -- a unit-qualified member named like a Pascal reserved word was rewritten to a trailing-underscore spelling no Python module declares, so it missed by construction; f004e408e did the class sibling); and import threading, which needed no code at all, only the --threadsafe flag its own diagnostic prescribes. The two -d defines are required for the native backend's C headers and are not optional. DO NOT QUOTE AN app.py LINE NUMBER -- the owner edits that file live and the wall moved 1195 -> 1217 within an hour. SUPERSEDED LEAD, 2026-09-11 morning: THE ENTRY-POINT CLOSURE IS ONE WALL FROM COMPILING, measured 2026-09-11 at 7958322f8 / binary 465845b20d1e: pascal26 --threadsafe lekkerzeilen/__main__.py emits 27 lines and EXACTLY ONE error, capture.py:44 no member create_string_buffer came of the qualifier ctypes. The 28-of-35 ratio below counts modules AS SUBJECTS and includes dead code (nothing imports gfx.py; four of the five ctypes files sit in a backend arm pxx never takes) -- the closure is goal 4's question and the ratio is not. Behind that line, measured in a scratch copy, the next wall is one line further (gl.ReadPixels, 7 args against the 4-arg run-time-dispatch cap, filed as feature-n-a-runtime-dispatched-method-call-is-capped-at-four-arguments) and BOTH WALLS ARE THE SAME CAUSE: _pxx.py's class gl: declares five names and neither of these, so writing the backend clears both at once. AND THE FORK IS NARROWER THAN STATED BELOW -- THE DEMO NEEDS NO FFI. capture.py names ctypes for one thing, a writable byte buffer for glReadPixels read back via .raw, and the owner's own _pxx.py docstring specifies the native backend as having 'no CDLL loading, no restype/argtypes declarations, no create_string_buffer'. So goal 4 waits on task-b-write-the-lekkerzeilen-pxx-platform-backend, not on a ctypes decision; the FFI question is real for other programs and is not this umbrella's. The seam's probe is CORRECT (verified: try: import ctypes / except ImportError takes the except arm under pxx), so do NOT shim one member to clear the line -- that flips the probe, which is the 27 -> 25 measurement below. PREVIOUS LEAD, still accurate as a module ratio: RE-MEASURED 2026-09-11 at compiler f9fb672ee109 / corpus 2a3d60e: 28 of 35 compile and CTYPES IS THE ONLY WALL LEFT -- all seven remaining failures are it. Every count and cause-list later in this summary is from 2026-09-10 and is SUPERSEDED; they are kept because the per-module reasoning is still the best record of how each wall was characterised. What cleared since: the _pxx module-as-a-value wall (frankZ's getattr-over-a-unit-alias fold plus frankuser's corpus seam rewrite), heapq (frankuser), and the import-order arity refusal on nearest (frankB, 8de1fff93, which took hud.py and traffic.py). threading and sqlite3 no longer appear as walls under --threadsafe. SO GOAL 4 NOW REDUCES TO ONE QUESTION AND IT IS NOT A COMPILER QUESTION: do we want NilPy programs to be able to call a native library the way CPython programs do, or do we want this app to reach the native layer through pxx's own binding mechanism and change its source to suit? The umbrella already answers it one way at 'do not build mimic_ctypes for this target' below, and frankuser MEASURED the cost of the other way on 2026-09-11: a correct partial mimic_ctypes moved the census 27 -> 25, because import ctypes succeeding IS A CAPABILITY PROBE -- the app's seam commits to the arm that needs the full FFI the moment the import resolves, so a partial shim is not partial progress. Owner-set target (2026-09-08): the lekkerzeilen sailing simulator -- /home/neo/lekkerzeilen -- as a REAL-WORLD nilpy target. It was written knowing about pxx and it shows: the runtime package imports ZERO third-party libraries (numpy and PIL appear only under tests/ and tools/), there is not one f-string in it, and no async, yield, match, walrus or annotation. RE-MEASURED 2026-09-10 at compiler a812b9549413, tree 813c99cc7: 22 of 35 modules compile clean. THE CORPUS MOVES UNDER YOU -- the owner renamed flight.py to drone.py at 15:47 and added atlas.py at 16:30 the same day, so it was 34 modules that morning and a delta between two censuses is not attributable to compiler work unless you check. The 13 remaining walls are FIVE causes, and THREE OF THE THIRTEEN ARE BY DESIGN AND OWE THE COMPILER NOTHING: platform/{_ctypes_backend,_gl,_sdl2} are the CPython arm of the app's own two-backend seam, which under NilPy is never imported at all -- try: import ctypes / except ImportError: from . import _pxx takes the _pxx branch, measurably so since 708555fdb, and they only appear as walls because the census compiles every file DIRECTLY. Of the ten that remain: capture.py and gfx.py need ctypes, and CALLING THAT A SEAM EDIT WAS WRONG -- corrected 2026-09-11 (frankB measured, confirmed here): gfx.py uses ctypes. SIXTY times across NINE names (byref 21, c_uint 19, c_void_p 5, sizeof 4, c_float 4, c_int 3, create_string_buffer 2, c_char 2, c_char_p 1) plus the (TYPE * N)(...) array-type constructor at three sites, and 22 of its 148 gl.* call sites marshal through it -- it IS the OpenGL marshalling layer. Routing the import through a try/except makes the module COMPILE and leaves it unable to do the one thing it exists for: the census moves by two and the demo moves by zero. So these two rows are ONE MISSING CAPABILITY (a bounded mimic_ctypes: nine names and an array-type constructor, not all of CPython's ctypes), not two module fixes, and anyone who does the seam edit anyway MUST say in the resolution that ctypes is not solved or two cleared rows will read as the capability landing; threading blocks 3 modules on 2 real sites (main reports gauges.py line 36 through the import chain); sqlite3 blocks 2 subjects on ONE site at world.py:188 (atlas.py:188 is if box is None: and has no sqlite3 near it); a module as a VALUE blocks platform/init.py with bindings.py cascading behind it; and a field from a qualified module constant blocks traffic.py:402. So the COMPILER owes 8 modules on 4 causes, not 13 on 5. READ THE PER-MODULE LIST, NOT THE COUNT: clearing the *unpack-with-defaults wall moved five modules and only three of them went green -- the other two advanced into a SIGSEGV that was invisible behind it. TWO STANDING RULES FROM THE OWNER, both unusual and both deliberate: (1) WE MAY CHEAT ON THE SOURCE -- where something is principally incompatible with nilpy, changing lekkerzeilen is allowed, which is the opposite of the usual corpus rule; (2) it is NOT to be wired into the test suite, like uforth. It is a target to attempt, not a gate.

2026-09-12 — THE CLOSURE COMPILES AND RUNS, and the last wall was a RUNTIME one

The headline is in the summary. What belongs here is the part a changelog row loses.

The three walls were found by RUNNING the target, and only one of them was on this umbrella's list. io.open was new code in a file nobody had looked at; __doc__ was the banked ticket and the ticket named the wrong file; the ** fault was not on any board anywhere, because nothing that compiles a census can see it.

This is the aperture change worth recording. Every census in this ticket above — six of them — asks does it compile. The moment the answer was yes, that instrument had nothing left to say, and the next defect was a ValueError raised at import time by a module-level table. A first-failure compile census cannot observe a runtime fault at all; it is not a weak instrument for the question, it is the wrong instrument. The replacement is free: run the binary.

Two predictions of mine were falsified, and stating them first is what made them useful:

  1. I predicted open(p, encoding="utf-8") would silently bind encoding as the MODE. It is LOUD — undefined variable (encoding). So the wall was a missing function, not a silent wrong value.
  2. I predicted that a ** in the MAIN module would make the imported module's ** correct, since the hook is one runtime global. It did not: a unit's initialisation runs before the main body that assigns it. That falsification is the second half of bug-n-an-imported-module-s-star-star-never-installs-pypowhook, and without the prediction it would have read as one defect.

Eliminated suspects, each measured rather than reasoned about, because the ** fault presented as "lekkerzeilen segfaults somewhere": audio.py:40's GAIN expression (identical to CPython); 0.0 ** 4.56 in a MAIN module (correct — which is what hid it); every variant/static operand combination for ** (correct); the whole nested-closure structure of lines.py's station(t) reproduced in a main module (correct); and PyExprMode being false inside an imported module (disproved — Python and/== precedence is in effect there).

The instrument note, and it is the owner's: I was about to temporarily tag pylib.pas's six math domain error messages to find which function raised. "you realize you can build with debug params?"-g plus gdb gave the location in one shot. Worth knowing what -g does NOT give here: no symtab, and no .pas files in the line table, only the app's .py, so a backtrace past the exception-handler frame is garbage (the handler has no CFI and gdb scans to a stale return address). It reported a plausible wrong function with a plausible wrong line; stepping was the reliable evidence.

What is NOT established. Only --help has been run. The simulator itself still waits on task-b-write-the-lekkerzeilen-pxx-platform-backend, and ctypes still decides whether it runs at all — unchanged. The next measurement someone takes here should be a non-graphics subcommand, and it should be confirmed rather than assumed to work.

2026-09-12, later — --conform PASSES, and it is the best instrument this ticket has had

--conform is the owner's own conformance mode: no window, no GPU, six PNGs encoded by capture.write_png and decoded back by png.decode, checking their own answer. A fixture that carries its own oracle, which is the shape conformance work almost never gets — nothing shipped beside it to keep in step.

All six round-trip. The pixels digest — which the function's own docstring says must match on any correct runtime, because it is the image and not the encoding — is identical to CPython on all six rows, the awkward 37-wide case included (its comment says a width that is not a multiple of four is where a pack alignment bug shears the image, so it is there on purpose).

The bytes and file columns differ and that is not a fault, by the app's own statement and by ours. lib/rtl/zlib.pas is RFC 1950/1951 from scratch, reports pxx-rtl rather than a libz release, and emits STORED deflate blocks — its header says so in capitals and names this program's capture.py as the wall that prompted the members. Measured: zlib.compress of 2000 identical bytes returns 2011, against CPython's 23, and the stream round-trips. So our PNGs are 3-12x larger and every byte of them is correct. That is a documented position, not a finding, and the place to change it is the existing feature-n-mimic-zlib-...-inflater territory, not here.

The wall it found, and why no ordinary spelling of it was broken

bytearray(x) over a dynamically-typed x raised TypeError: expected a number, got object. The cause is written out in full in pylib.pas's own comment on bytes(const v: Variant): adding an Integer overload changes which arm a Variant binds, so bytes had to grow a Variant arm. bytearray has carried bytearray(n: Integer) all along and never grew one, so this has been broken for as long as that overload has existed — one concept, two spellings, and the one nobody extended is the one that stayed broken.

It survived because the spelling that fails is the one nobody writes by hand:

s = raw[1:1 + n]
bytearray(s)                      # fine — s is a local and is inferred bytes
bytearray(raw[1:1 + n])           # aborts — raw is a parameter, so Variant

png._unfilter(raw, height, stride, step) writes the second form, because raw is a parameter. An intermediate local rescues it.

The test that should have caught it is test_nilpy_builtin_over_variant_receiver, and it could not: its sweep has two payload kinds, str and list, and lekkerzeilen's is BYTES. A payload kind nobody listed is a payload kind nobody dispatches on. That file now sweeps three, and says so in its header — the aperture is the lesson, not bytearray.

2026-09-12 — --probe passes, --starts is a SILENT wall

Two more non-graphics modes run. Taken together with --help and --conform, four of the five no-window paths have now been exercised.

--probe passes. It loads the SDL2 and libGL bindings without opening a window and prints bindings load cleanly. Its only difference from CPython is the one line that is supposed to differ:

-backend : ctypes        (CPython)
+backend : pxx           (pxx)

That is the app's own two-backend seam choosing correctly, and it is the first direct evidence in this ticket that the _pxx arm loads real libraries, not merely that it compiles.

--starts does not, and nothing says so. Under CPython it lists the rijn region and two places to start; under pxx it prints open water: nowhere in particular, which is the point and exits 0. The world directory is not found.

Found by stepping the resolution chain with prints, after a standalone probe was misleading: from lekkerzeilen import world (an absolute package import, which the app does not use) binds NO members at all under pxx — world._home is "no member ... came of the qualifier". That is possibly its own gap and is not the cause here; the app uses from . import world, which binds fine. Recording it so the next reader does not mistake the probe artefact for the finding. The real chain was found by instrumenting a scratch copy of the package.

The cause is __file__, the package case, and it is a FORK rather than a bug to take — see the summary and decide-n-what-does-dunder-file-mean-for-a-module-inside-a-package.

2026-09-19 — MEASURED: 11 OF THESE 18 blocked-by EDGES ARE ALREADY CLOSED

Counted by FOLDER, not by a glob across folders: 7 open, 11 in done/.

Still open (7):

Closed but still listed as blockers (11): the two C-pointer binding bugs, the callee-declared-below ABI bug, the non-constant default, the with-header star unpack, the staticmethod/classmethod-through-a-class-value bug, the getattr segfault, the heap-lock-on-allocation bug, feature-b-pil-..., feature-n-...-capped-at-four-arguments, and feature-n-the-module-docstring-....

WHY THIS IS WORTH A BLOCK RATHER THAN A QUIET PRUNE. The edges are not wrong as history — this umbrella genuinely needed all eighteen — and the RANKER is not fooled: tools/progress.sh ready --track N correctly omits every closed one. The damage is done to readers, and it has now been measured three times, all three from this same list of eleven: a seat was pointed at the module-docstring ticket (closed since 09-12), at the four-argument cap (closed by the seat itself), and at PIL — closed 2026-09-14 in 6b45b991b, owned by frankb-56, with 44KB of working lib/rtl/pil.pas behind it. The third one cost more than a wasted message: claim PULLED A DONE TICKET OUT OF done/ into working/ and rewrote its owner, and it was caught only because the seat opened the file before starting.

The instrument that was right the whole time is tools/progress.sh ready|next, and the mistake was reading this file's raw blocked-by: list instead — CLAUDE.md already says the tooling beats reading a board, and an umbrella's edge list is a board. Do not dispatch from this list; dispatch from ready.

2026-09-20 — ONE EDGE REMOVED, AND IT IS THE CASE THE 09-19 AUDIT EXONERATED THE RANKER FOR (frankb-8e)

Removed: bug-n-a-class-level-method-through-a-class-value-is-refused-when-the-name-has-two-carriers. 18 -> 17 edges. Kept as history in this note, which is where the 09-19 block asked such things to go.

THE 09-19 AUDIT ASKED WHETHER EACH EDGE'S TICKET IS CLOSED. IT NEVER ASKED WHETHER AN OPEN EDGE IS STILL TRUE. Those are different questions and they have different victims. That block's own conclusion — "the edges are not wrong as history ... and the RANKER is not fooled: ready --track N correctly omits every closed one. The damage is done to READERS" — is correct for a CLOSED edge and does not reach this one, because this ticket is OPEN. The ranker therefore honoured the edge exactly as designed and handed a seat p40 work at effective prio 90, top of Track N, with unblocks 1 ticket(s) beside it. The instrument the 09-19 block names as the one that was right the whole time — tools/progress.sh next — is the instrument that misdispatched here, and it did so by reading this very list.

THE MECHANISM IS PARTIAL CLOSURE, WHICH NO STATUS TEST CAN SEE. This ticket is two defects under one slug: shape (a), alias.m() where one class carries the name at class level and others carry it as an instance method — that was lekkerzeilen's live wall (gl.clear()), and it was fixed on 2026-09-13 in 5445b96d8 — and shape (b), two distinct classes both carrying one name at class level, which the ticket has said since that day "nothing measured asks for it". The edge was discharged by the fix and the ticket stayed open for the remainder, so status: backlog remained honest while the edge silently stopped being true. A ticket that closes in halves passes every open/closed audit and keeps its edges.

THE SUMMARY WAS NOT THE PROBLEM, WHICH IS WHY THE EXISTING REMEDY MISSES IT. CLAUDE.md's rule for part-closed tickets is to edit the SUMMARY in the same commit, and that was done here exemplarily — the summary leads with SHAPE (a) IS FIXED, 2026-09-13 and says the remainder has no demand. It is the first thing a reader sees and it told me not to be here. It is not what the ranker reads. The ranker reads the EDGE, in a different file, owned by a different ticket, and no amount of honesty in the summary reaches it. So the rule needs its other half: when you close the part of a ticket that earned it an umbrella edge, the edge is what you go and remove — and the person who fixed shape (a) is the one holding the knowledge that the edge is spent.

Verified before removing, not inferred from the ticket's prose — see the ticket's own THE p40 SURVIVES AND THE PARAGRAPH ABOVE DOES NOT block. ast census, 192 files at lekkerzeilen 587681a, 192 parsed: the corpus does hold a shape-(b) pair (identity on Mat4 and Quat) that the 09-13 census reported absent, and it still does not bite, because all eight call sites spell the class literally and neither class is ever aliased. Re-add this edge the day something binds a two-carrier class to a name.

2026-09-20 — ATTEMPTED THE TARGET: THE WHOLE DEMO COMPILES CLEAN, AND THE OWNER'S BLOCKER 01 IS FIXED BUT INERT UNTIL PINNED (frankb-8e)

Attempted rather than triaged, which is what this umbrella asks for. The edge audit that started it found two edges false; the build settles more than the audit could.

THE BUILD. compiler/pascal26 at f94c2a7e2396d2be (pxx HEAD a852664bb), CWD at the pxx repo root, exactly runbin.sh's invocation:

./compiler/pascal26 --threadsafe -dSDL_DISABLE_IMMINTRIN_H -dGL_GLEXT_PROTOTYPES \
    /home/neo/lekkerzeilen/lekkerzeilen/__main__.py <out>

rc=0, ZERO errors, 83 warnings, binary emittedcode=12021473B data=633660B bss=226823164B procs=11589. The import closure from __main__.py compiles end to end. 2m05s.

WHAT MOVED, ATTRIBUTED BEFORE BEING QUOTED. bin/build.log from 2026-09-19 20:09 ends on one error — pascal26:745: cannot infer the type of field self.chart_view, the owner's blocker 01 (devdocs/pxx-blockers/01-field-from-qualified-float-const/, filed 01d0fec at 20:11). The demo source did not change: self.chart_view = chart.VIEW is still unannotated at app.py:745. Controlled directly — the pre-fix demo tree a98958a, exported with git archive so nothing in the live checkout moved, compiled with the SAME current compiler: rc=0, clean. Same source, different compiler, so the repair is pxx-side and none of it is the demo's 25 commits since.

THE OWNER'S OWN REPRO AND ITS CONTROLS, RUN AS HE WROTE THEM:

dir pinned a6a2a1cc2278e1a9 HEAD f94c2a7e2396
repro/ (VIEW = 1700.0) error, his exact message builds, prints 1700
control_int/ (VIEW = 1700) builds builds, 1700
control_local/ (local, not a field) builds builds, 1700

The pinned binary's sha is the one his README records having measured with, so the negative control is his instrument, not a lookalike — and the green is attributable rather than a row that cannot fail.

WHICH COMMIT FIXED IT IS NOT ESTABLISHED, AND IS NOT GUESSED HERE. 45b8571d7 is the nearest by subject and its own message rules it out: it adds TUPLE and None arms to the module-global literal reader and says scalars were already known — VIEW is a scalar float. A case-fold theory (class View exists at atlas.py:169, and two case-folding fixes landed today) was tested and discarded: the repro is self-contained with its own chart.py and no such class, yet still reproduces on the pin. Range is 934f40376..a852664bb. Left open rather than filled in with the plausible answer.

THE CONSEQUENCE THAT IS NOT ABOUT THIS TICKET: THE FIX IS INERT UNTIL PINNED. runbin.sh already documents PXX=pinned as "CURRENTLY BROKEN for this demo" and defaults to the latest compiler for that reason; blocker 01 is now a second, independent row with the same shape, measured above. The demo therefore builds only against a moving compiler in a working checkout — which that script's own header calls out as a hazard (a mid-rebuild window gives a spray of errors and no window). Stated here because it is exactly the class CLAUDE.md names: a compiler fix a consumer depends on is inert until a pin carries it. Whether to pin is the owner's call and is not being acted on from this seat.

STILL NOT ESTABLISHED, and the distinction this umbrella has been careful about: COMPILES is not RUNS. A clean build says nothing about frames. There are untracked devdocs/pxx-blockers/pxx-frame-*.png in the demo checkout dated today and a seat is live in that tree (HEAD moved 587681a -> 1d9fcff -> 4379db0 during this measurement), so the run half is being carried elsewhere and is not claimed here.

Second false edge removed in the same pass: the math ticket

feature-nilpy-math-module-twelve-absent-names-measured17 -> 16 edges. Same mechanism as the class-level one above: a real open ticket whose edge stopped being true, so next ranked a p30 feature at effective 90 and would have handed it to the next Track N seat as the top of the queue.

Measured against the 39-module demo population (lekkerzeilen/**, which is the denominator the "36 of 39" figure uses; tools/ at 18 and tests/ at 51 are outside it): none of the fifteen still-absent names — acosh asinh atanh cbrt dist erf erfc exp2 gamma lgamma log1p nextafter remainder sumprod ulp — appears in the demo. There is no from math import form in lekkerzeilen/** either, so the qualified-math. scan is complete rather than a sample.

math.dist IS used, nine times, and every one is in tools/ (corridor.py 6, import_nl.py 3) — an offline toolchain that nothing under lekkerzeilen/ imports. That is the whole reason this edge looked live: a scan over *.py at the repo root finds math.dist and stops, and the population question decides the answer. The demo's actual math surface is sin/cos/radians/hypot/degrees/atan2/sqrt/floor/ceil/asin/tan/log/log10/isnan/exp/copysign/acos/isinf/atan — all present.

And the build above is the stronger statement: the demo compiles with zero errors, so no ticket blocks the COMPILES half of this umbrella today. Every remaining edge is either about RUNS or is discharged and has not been re-checked. Re-add either of these two the day the measurement changes.

PRE-REGISTERED, 2026-09-20, before the pin that was starting as this was written

A pin is being cut by another seat under firsthand authorisation. Recorded BEFORE the re-run, because a null row is only information to someone who said what they expected.

EXPECTED: after that pin lands, ./stable_linux_amd64/default/pinned devdocs/pxx-blockers/01-field-from-qualified-float-const/repro/__main__.py builds and prints 1700, where against pin a6a2a1cc2278e1a9 it errors with cannot infer the type of field self.chart_view. The fix is in 934f40376..a852664bb and the pin is cut from a tree containing it.

If it holds, two retirements and no new work: blocker 01 stops being inert, and runbin.sh's header note documenting PXX=pinned as "CURRENTLY BROKEN for this demo" becomes retirable — that note is about a different failure (TypeError: forwarded call got 5 arguments, expected 0 to 4) also fixed after the old pin [CORRECTED 2026-09-20 — I NEVER MEASURED THAT AND IT SHOULD NOT HAVE BEEN STATED; see the correction block below], and its own text says "Re-measure before believing either half of this." It is 7a's file; flagged here, not edited.

WHAT WOULD FALSIFY IT: the pin cut from a tree without the fix, or the fix depending on something in lib/** the pin does not carry. A still-failing repro under the new pinned binary is the interesting result and must not be quietly re-run — it would mean the pin and the fix disagree about what carries it.

OUTCOME of the pre-registration above — pin v413, measured 2026-09-20

CONFIRMED. ./stable_linux_amd64/default/pinned .../01-field-from-qualified-float-const/repro/__main__.py builds and prints 1700, where against pin a6a2a1cc2278e1a9 it errored. Pinned binary after the pull: f94c2a7e2396d2be.

AND THE CALIBRATION, BECAUSE A CONFIRMED PREDICTION IS WORTH LESS THAN IT FEELS WHEN HALF OF IT WAS ALREADY SETTLED. The pinned bytes are IDENTICAL to the compiler this umbrella's build row was measured with, so the binary half could hardly have come out otherwise. What was genuinely open is the half the falsifier named — whether the fix depends on something in lib/** the pin does not carry. A pin is a binary AND a lib/ pairing, and byte-identity settles only the binary. The pair is coherent.

THE DEMO NOW COMPILES UNDER THE PIN, which is a real change of state and not a new measurement: ./stable_linux_amd64/default/pinned on lekkerzeilen 4379db0 gives 0 errors, 83 warnings, code=12021473B procs=11589 — byte-for-byte the same figures as the HEAD row above, because it is the same compiler bytes. Before v413 this build died on blocker 01.

WHAT IS NOT RETIRED: runbin.sh's PXX=pinned note. It documents a RUNTIME failure (TypeError: forwarded call got 5 arguments, expected 0 to 4), and a clean compile says nothing about it. Running the demo needs a window and belongs with whoever owns the SDL_VIDEODRIVER question. Flagged, not claimed.

CORRECTION 2026-09-20 (frankb-8e) — I USED THE NOTE'S CLAIM ABOUT ITSELF AS EVIDENCE, IN THE SENTENCE FLAGGING THE NOTE AS UNRELIABLE

The pre-registration above said runbin.sh's PXX=pinned note concerns a failure "also fixed after the old pin". I never measured that. I took it from the note's own header — "because the fix for that landed after the pin was cut" — and restated it as established, in the same sentence that quotes the note saying "Re-measure before believing either half of this."

That is the failure and it is worse than an unmeasured claim. I believed one half while quoting the instruction not to. A document under suspicion cannot be its own corroboration, and a self-description is the FIRST thing to go stale in a stale document — it is written once, at the moment the author is most confident, and nothing later contradicts it because nothing later reads it. So the rule is narrow and worth having: when you flag a document as needing re-measurement, every claim you take FROM it inherits that flag, including the ones you are only passing along. Mark it as the document's claim, or measure it.

WHAT 7a MEASURED (b2eddde), and it refutes the note without a repro: pinned and latest are byte-identical today — both f94c2a7e2396d2be — so "pinned is broken, latest works" cannot be true of anything. A note contrasting two configurations is refuted by showing the configurations are the same, which is cheaper than reproducing either and settles it outright. Keep the shape: check that two configurations CAN differ before investigating HOW they differ.

ONE DISTINCTION I AM KEEPING, because collapsing it would misfile an open bug. The relay reached me as "the TypeError was not fixed". There are two different TypeErrors here:

message mechanism status
the note's TypeError: forwarded call got 5 arguments, expected 0 to 4 arity of a forwarded call unknown to me; still unmeasured, and I am not claiming it either way this time
7a's, measured today TypeError: object is not callable a FIELD shadows a method, so the call lands on the field — blockers 03 and 04 open

Same exception type, different messages, different causes. This tree already says that when two subjects fail with the same diagnostic you suspect the diagnostic before believing a shared cause; an exception CLASS is a coarser match than a line number, so the trap is larger. The live failure is 03/04 and it has nothing to do with the pin — the note blamed the pin for a failure the pin never caused.

WHY THE NOTE SURVIVED TO BE MEASURED PROPERLY: I declined to retire it on a clean compile, because it describes a RUNTIME failure. Had it been retired on that green, the one line pointing at a live TypeError would have gone with it — retired on a green that was about a different question. That is the only part of my handling here that was right, and it was right for the stated reason rather than by luck.