← board

Overload resolution picks the signed arm for an unsigned argument

With both arms visible, a Cardinal argument goes to Int64, where FPC sends it to QWord:

function Sig(v: Int64): AnsiString; overload;  begin Sig := 'i64';   end;
function Sig(v: QWord): AnsiString; overload;  begin Sig := 'qword'; end;
var c: Cardinal;
begin c := 5; WriteLn(Sig(c)); end.
argument pxx fpc
QWord qword qword
Int64 i64 i64
Cardinal i64 qword
Integer i64 i64
q shl 1 (QWord expr) qword qword

So only the narrow unsigned case differs: pxx ranks "widen to the signed 64-bit arm" and FPC ranks "keep the signedness". Both conversions are lossless for a Cardinal, so no value is wrong today — this is a preference, not a defect, and it is filed as compat rather than as a bug for that reason.

Why it is still worth recording

It becomes observable the moment the two overloads behave differently rather than merely accepting different types — which is exactly the situation in bug-b-inttostr-of-a-qword-above-2-63-renders-negative. Once IntToStr(QWord) lands beside IntToStr(Int64), a Cardinal argument will still route to the Int64 arm here and to the QWord arm under FPC. For IntToStr the rendered digits are identical either way, so nothing breaks; for a user's own pair of overloads it could pick the other function.

Scope

The rule to match is FPC's: among candidates reachable by widening, prefer one that preserves signedness over one that does not. Narrow-signed→Int64 and narrow-unsigned→QWord both then fall out.

Belongs behind --strict-overload if it turns out to move any existing resolution, and unconditionally if it does not — preferring the same-signedness arm is the better default independently of FPC.

Found by

An integer-arithmetic differential, while confirming that bug-b-inttostr-of-a-qword-above-2-63-renders-negative was a missing overload rather than a resolution failure. It is a resolution failure — just not that one.

Log

CORRECTION (frankS, 2026-09-09) — the title names a preference the compiler does not have

Everything above this line is the ticket as filed and it stays for the record. Two of its claims are false and they are the two it was ranked on.

It is not a preference, it is the absence of one. OverloadArgRank ties every machine-integer pair at rank 1 (if TypeIsMachineInt(pTk) and TypeIsMachineInt(aTk) then Result := 1), and MatchProcCall's Phase 1c2 returned the FIRST candidate in chain order. So the answer was the first-declared arm, and Int64 happened to be declared first in the repro.

The table above was measured in one declaration order. Its Integer row records pxx and fpc agreeing on i64. They agree only because of the order. Swap the two declarations and pxx answers qword for an Integer argument while fpc still answers i64. A confounder held fixed reads as a fact about the subject and nothing in the table says it was there — which is how the ticket concluded that "only the narrow unsigned case differs".

"No value is wrong today" is false, and that is what priced it at 12.

function Big(v: QWord): Boolean; overload;  begin Big := v > 1000000; end;
function Big(v: Int64): Boolean; overload;  begin Big := v > 1000000; end;
var si: Integer;
begin si := -5; WriteLn(Big(si)); end.

pxx TRUE, fpc FALSE. The sign-losing arm was chosen over an exact-signedness one sitting in the same overload set. An Int64/QWord pair is what a numeric library writes and passing it an Integer is ordinary code, so this is not the "programmer made a presumed error" class — the source means the signed arm and says so by the argument's type.

Measured — three doors, both orders

fpc 3.2.2, at 1180aa627, binary 226c79e87aaa. fpc is stable across the swap on every row; pxx followed the swap on every row.

door selector
free call MatchProcCall Phase 1c2 (compiler/symtab.inc)
method FindUMethOverloadAheadOverloadArgRank (compiler/pasparser_call.inc)
constructor FindUCtorOverloadArgsOverloadArgRank

Against the PINNED pre-fix compiler the fixture's 16 rows show the two declaration orders disagreeing on 13, three of them printing qword BIG.

The fix

The ticket's own Scope section had the rule right and predicted the disposition right: "unconditionally if it does not [move any existing resolution] — preferring the same-signedness arm is the better default independently of FPC." It does not move any, gate GREEN, so unconditional.

What the fixture can and cannot see

The expr/ecard/unry rows cannot fail this bug — they read the same before and after, because cd + 0 is a signed expression however unsigned cd is. They are there so the rule cannot be passed by matching the SPELLED type. The call and cast rows are there for a third reason: the method probe parses arguments SPECULATIVELY and the committed loop re-parses them, so a rule that ranks on the probe's reading needs checking on shapes where the two readings could differ (frank-coordinator raised this against frankH's ad7c03b03, where exactly that divergence made a call MATCH on one reading and build another). All four such shapes agree with fpc in both orders.

What this is really an instance of

Two other tickets in this group say the compiler prefers something — [[bug-p-two-array-parameters-at-one-bracket-slot-are-decided-by-declaration-order]] and [[bug-p-a-shadowed-soft-intrinsic-is-closed-without-consulting-the-arguments]]. In all three the compiler makes no choice at all: it runs out of discriminating power and the tie-break becomes visible. Reading agency into the outcome points the fix at changing a preference instead of adding discrimination, and it points the ranking at a disposal — this one sat at prio 12 for it.