← board

The System. qualifier is stripped rather than resolved, so a shadowing member wins

Repro — 18 lines

program sh;
{$mode delphi}
type
  TCompare = class
    class function Integer(constref ALeft, ARight: Integer): Integer;
    class function UInt8(constref ALeft, ARight: Byte): Integer;
  end;
class function TCompare.Integer(constref ALeft, ARight: Integer): Integer;
begin Result := ALeft - ARight; end;
class function TCompare.UInt8(constref ALeft, ARight: Byte): Integer;
begin
  Result := System.Integer(ALeft) - System.Integer(ARight);
end;
begin
  WriteLn('u8=', TCompare.UInt8(200, 44));
end.
result
fpc 3.2.2 u8=156
HEAD pascal26:15: error: expected ',' before ')'
pin v407 095ef4811a5b identical

Byte-identical to the corpus failure: near: := System . Integer ( ALeft >>> ) - System.

The error message is the diagnosis

"expected ," is what a parser says when it is reading an argument list and wants the next argument. TCompare.Integer takes two. So after the System. strip the bare name bound to the class's own method and the typecast was parsed as a call.

Why the strip is the wrong operation

EatSystemQualifier (landed 2026-08-20, and it cleared this very wall with the different error undefined variable (System)) removes System. and lets the bare name resolve normally. That is correct exactly when nothing shadows the name — and a program only writes System.Integer when something does. The qualifier is discarded at the moment its only job begins.

The fix is to make the qualifier SELECT a scope rather than vanish: resolve the tail in the System/builtin scope and never consult the enclosing class. Same shape as any other qualified lookup.

The control that says it is the strip

The identical construct with no shadowing member compiles and runs on both HEAD and the pin:

var a, b: Byte; r: Integer;
r := System.Integer(a) - System.Integer(b);   { r=156, matches fpc }

So System.-qualified typecasts are not broken in general. Only the case the qualifier exists to serve is.

Do NOT "fix" this by mapping System.Integer to the mode's Integer

Recorded on feature-pascal-corpus-generics.md and worth repeating because it inverts the obvious fix: FPC's System.Integer is SmallInt. The 4-byte Integer comes from the MODE, which shadows the system unit's. SizeOf(System.Integer) is 2 under FPC and 4 under pxx — see compat-p-system-integer-is-smallint-in-fpc (prio 15). A fix that resolves the qualified name to the mode's Integer would be right about this program and wrong about the width, so assert identities that agree under both (System.LongWord) rather than the Integer spelling.

Where it bites

One defect, two files, and it is the wall for both.

Fixed 2026-09-07 (frankS) — and the diagnosis above is right about the symptom and wrong about the layer

It is not the strip. EatSystemQualifier already does the right thing: ConsumeUnitQualifier returns −2 for System., deliberately, and its own comment says why — "the builtin soft-aliases must WIN over a same-named method of the enclosing class". The marker was there and it was correct. One reader of it was not.

pasparser_expr.inc, the bare-call-to-a-sibling-static-method arm:

if (procIdx < 0) and (idx < 0) and (qUnit < 0) and        { <- admits -2 }
   (CurProc >= 0) and (CurMethClass >= REC_UCLASS_BASE) then

qUnit < 0 reads as "not unit-qualified" and means "unqualified OR System-qualified" — and System-qualified is the one case that must not reach a member of the enclosing class. Its two sibling guards, ~60 lines below and in the statement-side twin, both spell it = -1 and both carry the reason. The fix is qUnit = -1.

The name is not the thing: a guard whose condition is 80% right about what it is named after.

Two things measured that the ticket predicted differently

The wall is the STATIC-method arm, not the instance one. The same shadow declared as an INSTANCE method (function TBox.Integer) resolves correctly on this tree and on pin v407. A guard was drafted for the instance dispatch on the symmetry argument, measured, found to fix nothing, and reverted — the site now carries a comment saying so, because the next reader will have the same symmetry intuition. TCompare is a class of class functions, which is what put the corpus program in the static arm.

The refusal is the mild face. With MATCHING arity there is nothing to refuse:

shadow arity pin v407 HEAD
2 args, cast passes 1 expected ',' before ')' 156
1 arg, cast passes 1 0 — silently the method's value 156

A silent wrong answer is what let this reach a released compiler, and the expected ',' message the ticket read as its own diagnosis is only produced when the shadow happens not to fit.

Fixture

test/test_a_system_qualified_name_beats_a_shadowing_class_member.pas (test_sysqualshadow26), five rows against fpc 3.2.2. Both faces above, the two controls that were measured on the pin before being written down (instance shadow; the construct outside any method), and a width row.

The width row is SizeOf(System.LongWord) and the obvious assertion was rejected rather than overlooked. FPC's System.Integer is SmallInt — the 4-byte Integer comes from the MODE — so a row asserting SizeOf(System.Integer) = 4 would pass, would agree with the corpus program that motivated the ticket, and would encode a rule FPC does not have. System.LongWord is 4 under both, so its answer cannot be reached by accident. The fixture header says this, so nobody "simplifies" it back.

What is NOT claimed

The corpus wall at generics.defaults:1178 was not re-driven. The stage directory this rung uses (/tmp/generics-stage) is gone and /tmp is reaped at 6h; rebuilding it is rung 6a's own work. What is measured is the ticket's 18-line repro, which the ticket states is byte-identical to the corpus failure. Someone holding rung 6a should re-drive it and move the wall number.

CORRECTED 2026-09-07, same day, and the retraction is the useful part. This section originally said "a direct uses Generics.Defaults walls EARLIER than 1178 on HEAD and on the pin alike, at FPC_FULLVERSION has no integer value here — a different gap", and offered it as a caution. It is true about the invocation I ran and false as written. frank-optimize re-drove the rung and got :2729; I re-measured against the same tree and the discriminator is a flag of mine:

-Fu<rtl-generics/src>                              -> :2729
-Fu<rtl-generics/src> -Fu.../3.2.2/rtl/objpas      -> :3491 FPC_FULLVERSION

The extra objpas path pulls a DIFFERENT unit in, and :3491 is that unit's line. An instrument that lies by being correct about something else — nothing errored, nothing was out of range, and the wall it reported was real. A wall number is meaningless without the search path it was measured on, and I quoted one without it.

THE FIX CLEARED THE CORPUS WALL: generics.defaults.pas 1178 -> 2729, at binary c58d2b8c0b4b, re-driven by frank-optimize and reproduced here with -Fu<rtl-generics/src> alone — no include path and no staging needed, so the /tmp/generics-stage caution above applies to the old stage and not to the fpcsrc tree. The new wall is no overload of Create matches these arguments (record, Cardinal, record), unowned.

And one word in the framing around this ticket was wrong in a way worth keeping. The wall moving :2279 -> :1178 was reported as "not a regression, since pin v407 walls there too". "The pin has it too" bounds the defect's AGE; it does not establish that the behaviour was always there. The qUnit < 0 guard was always wrong and something in that range made it REACHABLE — so the honest statement is the defect is old, the exposure is new, which is neither "regression" nor "long-standing". frank-optimize raised and corrected this themselves; it is recorded here because the reasoning outlives the ticket.

Log