← board

summary

A Variant default parameter is not applied when the call arrives from NilPy: the omitted argument comes through as pynone (pyvartag 0, pyvar_to_int 0) instead of the declared default. Boolean, Integer and AnsiString defaults are applied correctly, so the default-value machinery works and only the Variant arm is wrong. The same Variant declaration honours its default when called from Pascal.

RE-TITLED 2026-09-11 — it was filed as "a Pascal default parameter is ignored"

That was too wide and frankB caught it. The wide version tells every shim author their defaults are unreliable when three of four kinds are fine, and it points a fixer at the whole default machinery instead of at the one arm that special-cases Variant. Their probe, independently reproduced here at compiler b092b705aacb (frankB's at 786b88673e62):

b omitted True   | given False      (Boolean = True)      APPLIED
i omitted 7      | given 3          (Integer = 7)         APPLIED
s omitted dflt   | given x          (AnsiString = 'dflt')  APPLIED
v omitted 0      | given 5          (Variant = 1)          NOT APPLIED
vtag omitted 0   | given 1          <- the omitted slot's tag is 0 (none)

Their probe design is the lesson and it is the inverse of my mistake below. Every default was chosen so an unapplied default could not produce it by accident: Boolean = True (absent reads False), Integer = 7, AnsiString = 'dflt'. In their words: had they written Boolean = False they would have reported the same green I did.

Reaching such a unit from NilPy needs the extension form — import 'mimic_dfltchk.pas' as d — because a bare import resolves to Python only. The compiler says so itself, and says it well; see the contrast in [[bug-n-a-bare-nilpy-import-falls-through-to-a-host-c-header-of-the-same-name-and-says-nothing]], where the same frontend takes a wrong route silently.

the measurement

lib/rtl/zlib.pas, two throwaway probes in the unit's Python surface:

function zdbgtag(const value: Variant = 1): Int64; begin Result := pyvartag(value); end;
function zdbgint(const value: Variant = 1): Int64; begin Result := pyvar_to_int(value); end;
            tag   int
arity 0      0     0     <- declared default is 1; pynone arrived instead
arity 1      1     1     <- explicit 1

From Pascal the same function at arity 0 returns the declared default. Compiler b092b705aacb, tree 09976a4e8+.

why it is prio 55 and not higher

It is silent, and it is silent in the worst direction: the value a caller gets is 0, which is a legal value for most optional numeric arguments. Every shim in lib/rtl that declares a Variant default is exposed, and the failure shows up as a plausible wrong number rather than an error.

THE REASON THIS SAT UNSEEN, AND IT IS THE REUSABLE PART

zlib.crc32's CPython default is 0, which is exactly the value an unsupplied argument already reads as. All four crc32 rows matched the CPython oracle -- two of them with the top bit set, and the chunked-continuation row too -- while the mechanism was completely broken. zlib.adler32's default is 1, so its a accumulator started at 0 and every row was wrong by a visible amount.

The expected value collided with the do-nothing value, which is the class CLAUDE.md already names ("if the machinery did nothing at all, would this row still pass?"). A shim whose optional argument defaults to 0, to None, or to anything falsy cannot test this at all. Pick a probe whose correct answer differs from the absent-argument answer, or the guard cannot fail.

repro

import zlib
print(zlib.adler32(b"hello"))        # 103219732 with the bug; CPython 103547413
print(zlib.adler32(b"hello", 1))     # 103547413 -- correct, explicit

With the workaround in place (see below) both print 103547413.

the workaround that is in the tree, and what to delete when this is fixed

lib/rtl/zlib.pas keeps the declared default -- that is the correct Pascal signature -- and adds ChecksumSeed(value, whenAbsent), which tests value = pynone and substitutes CPython's documented start. That is mimic_urllib_request.pas's established idiom (isNone := data = pynone in DataToString), not a new mechanism, and it is harmless once the frontend honours the declaration: the test simply stops firing. Delete it then, or leave it; it costs a comparison.

the idiom that is already correct, measured by frankB

mimic_sqlite3's execute(sql; parameters: Variant = 0) is the affected kind and is nonetheless safe, because its guard is spelled positively: pyvar_is_objtag(parameters). That answers "no parameters" correctly whether the omitted slot holds NONE today or an int-tagged 0 after this is fixed. The negative spelling not pyvar_is_inttag(...) would call NONE a sequence and raise TypeError from inside the shim. So the rule for a shim with a Variant default is: test for what you CAN handle, not for what you cannot — it survives the fix either way.

a second idiom exists and may be the intended one

mimic_urllib_request.pas spells urlretrieve's optional argument as TWO arity-differing overloads rather than a default, and getheader does the same. Arity-differing overloads are not hit by bug-n-an-overloaded-constructor-is-picked-by-name-ignoring-argument-type, which is about overloads differing only in argument TYPE. If defaults are not meant to work across this seam, say so and the shims should stop declaring them -- silently ignoring one is the worst of the three options.