← board

summary

A bare NilPy import X has three routes, and only one of them is silent. mimic_X.pas in scope -> substitution, which prints note: X -> mimic_X (shim, subset). X in PyRtlUnitServesPython -> lib/rtl/X.pas. Neither -> the HOST's /usr/include/X.h, bound with no note and no refusal. The program then fails with a diagnostic about a C signature it never mentioned — or, when the arities happen to agree, compiles and returns a wrong value.

the three routes, measured

At compiler b092b705aacb, one -Fu dir holding mimic_dflt.pas (frankB's probe, reproduced here):

import mimic_dflt  -> REFUSED: "mimic_dflt is the Pascal unit ... not a Python
                      module ... name it with its extension"
import dflt        -> COMPILED, reached the shim, and printed
                      note: dflt -> mimic_dflt (shim, subset)
import zlib (pre-fix) -> bound /usr/include/zlib.h, silently

This ticket originally said "a bare NilPy import resolves to Python only". That was a quotation from the first diagnostic above, over-generalised: that message is about the unit's OWN name, and the shimmed name reaches the same Pascal unit correctly and announces it. Corrected here because the wrong version hides the actual mechanism, which is that the NAME decides the route and only one route is quiet.

measured, and the arity-agreement case is real

import zlib before zlib was added to the curated list (fixed in 80d71d782, so reproduce at 09976a4e8):

zlib.crc32(b"hello")        -> error: no overload of crc32 matches these arguments
zlib.crc32(b"hello", 0)     -> same
zlib.crc32(0, b"hello", 5)  -> COMPILES, links system libz, prints 1577690842
                               CPython's answer is 907060870

C's is crc32(uLong, const Bytef*, uInt) — three arguments — so the two CPython spellings cannot match and the three-argument spelling does. The bytes object's pointer is passed where a const Bytef* is wanted, and the result is a plausible wrong number with no diagnostic.

the population on this box: 16

CPython stdlib names with a matching /usr/include/<name>.h, NOT in PyRtlUnitServesPython (17 entries) and with no mimic_ shim (18):

curses  errno  fcntl  fnmatch  getopt  glob  grp  locale
lzma    pty    pwd    sched    signal  syslog termios time

time, signal, errno, locale and glob are ordinary imports in real Python. And the population is HOST-DEPENDENT, which is the nastier half: it is a function of what is installed, so the same program fails differently on two boxes and a census compared across machines sees two walls for one cause. Without zlib-dev the same import zlib gives no member crc32 came of the qualifier zlib instead.

RE-PRIORITISED 60 -> 35 on 2026-09-11, and the measurement is why

frankB named the question that sizes this: how many of the 16 have a lib/rtl/<name>.pas today the way zlib did? Those are the rows where the silence hides a WORKING IMPLEMENTATION rather than merely producing a confusing error, and frankB's read was that they would justify prio 60 on their own.

Measured: ZERO of 16.

costs a working unit (the zlib shape) : 0
costs only a confusing error          : 16
  curses errno fcntl fnmatch getopt glob grp locale
  lzma pty pwd sched signal syslog termios time

So zlib was the only instance of the expensive shape and it is fixed. What remains is a diagnostic-quality defect on 16 ordinary module names, which is real and is not prio 60.

The name-equality test is the RIGHT test here, which is not obvious and was checked rather than assumed — testing a name instead of a capability is this project's house error. Route 2 is name-based: all 17 of 17 listed names have a lib/<name>.pas of exactly that name. So a capability living under a different filename is not reachable as import <name> by any route, and therefore cannot be something the silence is hiding.

THE FORWARD-LOOKING REASON TO FIX IT ANYWAY, which the count does not capture

The population is zero TODAY because I closed the only member an hour ago. The trap is not a list of names, it is a RECURRING step: add a Python surface to a lib/rtl/<name>.pas, forget the PyRtlUnitServesPython entry, and you get exactly the zlib failure -- a working unit in the tree, a host header answering instead, and a plausible wrong number if the arities happen to agree. I walked into it while holding both halves and spent five experiments on the wrong file. Anyone adding the next lib/rtl Python surface walks into the same one.

NOT "close the C route" — the C route is deliberate

pxx-crash-course.md and the comment at pasparser_proc.inc:6330 both say a bare NilPy import reaching a C header is designed behaviour, and imports resolving OURS FIRST is for PREDICTABILITY. So the defect is not the route, it is that the route is taken silently when the name is a stdlib module name.

recommendation, narrow on purpose — and it is now "make one arm match its siblings"

frankB's framing, which is better than the one this ticket was filed with: the frontend already owns a good diagnostic for two of the three routes, and the header arm is the only decision of the three that neither refuses nor announces. import mimic_dflt is refused with the fix in the message; import dflt succeeds and prints a note:. So this is not new machinery, it is the third arm saying what its siblings already say.

Say it. When a bare NilPy import binds a C header AND the name is a CPython stdlib module name, emit a note naming the header path — import time bound /usr/include/time.h, not a Python module. That is a one-line fact the reader cannot otherwise get, it leaves the designed route open, and it scopes the noise to the 16 names above rather than to every C-header import.

The alternative — refusing when the name is a stdlib module — is a behaviour change that would break any program deliberately importing a C header whose name collides, so it should not be taken without someone wanting it.

how it wasted a session, which is the argument for the warning

I added a declaration to lib/rtl/zlib.pas and then ran five experiments on it — changed the arity, the parameter types, the return type, removed the default — and every one changed nothing, because none of them were ever consulted. I had a positive control (base64.b64encode, the identical (const data: Variant) shape) and it PASSED, which confirmed the shape was fine and sent me to the wrong unit. The thing that broke it open was a probe whose correct answer only the header could produce: a three-argument call.

When every edit to a declaration changes nothing, stop editing the declaration and ask what else answers to that name.

CAVEAT ON THIS TICKET'S OWN MEASUREMENT: A HEADER'S REACH IS ITS INCLUDE CHAIN

The 16-name population above was computed against /usr/include/<name>.h FILENAMES. That is the right test for which header a bare import binds, and it is the wrong test for which symbols that header provides.

Measured 2026-09-11, on this ticket's own subject: grep -c '\bfloor\b' /usr/include/math.h answers 0. Glibc declares floor through __MATHCALL in bits/mathcalls.h, which math.h includes, so the word is not in the top file at all. Against the preprocessed header:

  printf '#include <math.h>\n' | cpp -P | grep -cE '\bfloor *\('     -> 1
                                          grep -cE '\bgcd *\('       -> 0
                                          grep -cE '\bfactorial *\(' -> 0

I ran the filename-level grep to CHECK someone else's claim that floor is ambiguous, got 0, and would have told them they were wrong. They were right.

So the 16 rows stand — those are the names whose import binds a host header — but any future sizing of WHICH MEMBERS collide must preprocess. A header's surface is everything it pulls in, and /usr/include/<name>.h is frequently a few lines of #include. This matters for the arity-agreement hazard specifically: the question "could a C function of this name accept the Python call" ranges over the whole include closure, not over the named file.

AND THE POPULATION FILTER ITSELF IS A SELECTION: sys.stdlib_module_names

The 16 names were selected by intersecting host-header filenames with sys.stdlib_module_names. Anything outside that set was removed before any assertion could see it, and frankZ demonstrated on 2026-09-11 that this exact filter hides real members — on the neighbouring arm, not this one, which is why it is worth writing down here rather than assuming it cancels out.

Their measurement: a ticket claimed nineteen colliding names, they measured two, and the answer is four — found only by dropping the CPython-name filter and testing all 117 lib/rtl unit names. Two of the four, textfile and typinfo, are not CPython module names at all, so every census that subject ever ran, theirs included, selected them out first.

What this does and does not change here. It does NOT move the 16 rows: for a bare import to reach a host header the name must match a header filename, which is a fact about /usr/include and not about CPython. What it DOES mean is that the 16 are the names a user would be surprised by, not the mechanism's extent — import anything where /usr/include/anything.h exists has the same shape, stdlib or not, and a local foo.h in an include path widens it again. So:

do not quote 16 as the size of the mechanism. It is the size of the intersection someone chose to count, and the choice was mine.

Combined with the include-chain caveat above, this ticket's numbers have two independent selections in them — which filenames, and which names. Both were made for readability and neither was measured as safe.