← board

Measured 2026-09-10, compiler 4d3d006cf973

import os
os.remove("/tmp/zz", dir_fd=None)
pascal26:2: error: undefined variable (dir_fd)

The table's entries map a dotted NAME onto a pylib proc and PyParseStdlibCall then walks ( .. ) calling the ordinary expression parser per argument. exist_ok=True is therefore parsed as the expression exist_ok — a name nothing defines — and the = never gets a chance to mean anything.

Why the diagnostic is the expensive half

undefined variable (exist_ok) is a true statement about what the parser did and a false lead about what is wrong. A reader greps for exist_ok, finds nothing, and concludes the program is broken — where the program is correct CPython and the mechanism is the gap. This is a whole class: every CPython stdlib signature with a keyword-only tail hits it, and each one produces a message naming the keyword.

The cheap half, which is worth doing even if the feature is not: make the argument loop RECOGNISE <ident> = and refuse by name — os.makedirs(): this call takes positional arguments only; exist_ok= is not supported yet. That is a one-site change and it turns a misleading error into an accurate one.

The feature, when someone takes it

Each shim would need to declare which keywords it accepts and in which slot. os.makedirs(name, mode=0o777, exist_ok=False) maps cleanly onto a Pascal default-parameter list, and Queue(maxsize=2) already proves a keyword binds to a Pascal parameter NAME (lib/rtl/mimic_queue.pas records why the parameter had to be called maxsize and not n). So the mechanism exists on the ordinary call path and this table is the one door that does not use it.

Boundary, measured

shape today
os.makedirs(d) works (2026-09-10)
os.makedirs(d, exist_ok=True) undefined variable (exist_ok)
Queue(maxsize=2) — an ordinary CLASS ctor works
q.put(v) — an ordinary method works

The third row is the one that says this is a table problem and not a NilPy keyword problem.

FIXED 2026-09-10 (frankB)

The feature, not the cheap half — and the cheap half came free with it, because the refusal path had to know the dotted name anyway.

PyParseStdlibCall's argument loop now calls PyKwArgIndex(procIdx, 0) per argument and PyBindKwArgs after the list. firstParam is 0 and not 1: these are plain procs with no Self in slot zero.

The bind runs BEFORE the arity re-target, and that ordering is the whole reason it is safe. After the bind, nArgs is the count of parameters actually BOUND, so the overload chosen by arity is chosen against the number the callee will receive. Re-targeting first would resolve the keyword NAMES against one overload and the call against another — the same class as bug-nilpy-keyword-arg-vs-overload-set, which is what PyKwArgIndex's own sibling-overload message exists for.

pyos_makedirs was respelled (name, mode = 511, exist_ok = False). A keyword binds by the Pascal parameter's name, so a shim's parameters have to be spelled as CPython spells them; lib/rtl/mimic_queue.pas records the same constraint from the other side, having been refused for calling its parameter n. mode is now threaded through to PyPalMkdir instead of being hardcoded, and pyos_mkdir_one's leaf parameter was renamed failIfExists because the caller now passes not exist_ok and the old name had become a lie.

The diagnostic half

pascal26:2: error: Nil Python: os.makedirs() has no parameter named
'nosuch'. The shim is pyos_makedirs; a keyword binds by the Pascal
parameter name, so adding the keyword means naming a parameter for it there

The dotted spelling is rebuilt from the tokens before they are consumed, purely so the message can name what the program WROTE. pyos_makedirs is an internal name the source never mentions — this file already pays for that mistake once, in the math.log intercept, which exists because the generic arity error named the RTL's Ln.

urlopen(timeout=) was a DIFFERENT door, and it was not broken

Worth recording because it looked like the same bug and is filed under the same line of the corpus. urllib.request.urlopen goes through the Pascal qualified-call path, not this table, and that path handles keywords perfectly well. The parameter was Integer and CPython's timeout is a float number of seconds, which real code writes — gauges.py's fetch defaults to 20.0. So the failure was

no overload of urlopen matches these arguments

which says nothing whatever about timeouts, instead of the shim's own careful refusal explaining that lib/rtl/http.pas has no request timeout. Now a Double, tested with < 0 rather than against the sentinel, because comparing a Double for equality against a constant works until someone writes the constant as an expression.

Two doors, one corpus line, and the second one was a TYPE problem wearing an overload message. A fix aimed only at the table would have left gauges.py walled one line later with a message that named neither the keyword nor the timeout.

Effect

lekkerzeilen/gauges.py moves :141 -> :173 -> :174, sqlite3.connectfeature-n-the-sqlite3-module-db-api-over-the-c-library, filed, p65.

Positive control: the previous binary refuses the fixture at line 28 with the reported undefined variable (exist_ok).