← board

A dynamic array of CLASS loses its element type when it is a parameter

Repro — 20 lines, self-contained

program p5;
{$mode objfpc}
type
  TEl = class Ext: string; end;
  TRec = record Ext: string; end;
  TArrC = array of TEl;
  TArrR = array of TRec;

procedure PC(A: TArrC);        begin WriteLn('named class : ', A[0].Ext); end;
procedure PR(A: TArrR);        begin WriteLn('named record: ', A[0].Ext); end;
procedure PV(var A: TArrC);    begin WriteLn('var class   : ', A[0].Ext); end;
procedure PK(const A: TArrC);  begin WriteLn('const class : ', A[0].Ext); end;

var ac: TArrC; ar: TArrR; e: TEl;
begin
  SetLength(ac,1); e := TEl.Create; e.Ext := 'cc'; ac[0] := e;
  SetLength(ar,1); ar[0].Ext := 'rr';
  WriteLn('global class: ', ac[0].Ext);
  PC(ac); PR(ar); PV(ac); PK(ac);
end.
row fpc 3.2.2 pxx
global class — same type, GLOBAL cc cc — correct
named record — element is a RECORD rr rr — correct
named int (separate probe) 7 7 — correct
named class — by value cc 4265192, no diagnostic
var class cc dumps ~45KB of heap, out-of-bounds read
open array of TEl cc dumps heap
A[0].ClassName TEl "ClassName": no such member on this record/class

So the boundary is exact: element is a CLASS and the array is a PARAMETER. Global-scope arrays of class are fine, and parameters whose element is a record or a scalar are fine.

Why the three symptoms are one defect

The element's CLASS identity is gone by the time A[i] is subscripted, so the result is treated as an untyped/record blob:

Both fields of the two-field probe print the SAME wrong value, so it is the element POINTER that is wrong, not a field offset.

Lead — not confirmed, and it is a named one

[[refactor-p-a-parameters-own-kind-and-its-element-kind-are-one-field-and-the-name-says-neither]]: Procs[pi].Params[j].TypeKind is the parameter's OWN kind when IsArray is False and its ELEMENT kind when True. That ticket cleared the pasparser_* increment and explicitly records 142 raw readers remaining in IR + lowering plus 12 sites annotated suspect. A parameter of a named dynamic array of class is exactly the shape where the two readings differ, so start with the suspect list rather than with a fresh hypothesis. Its own ablation note is also the warning: ParamElemKind's refusal is load-bearing and removing it segfaults ifclist.pas in a corpus --tier quick does not see.

What it costs today

Coordinate warning for whoever takes it

Both errors were reported with in: pscanner.pp while the construct is in pparser.pp, and the near: window was quoting pscanner text beside a pparser LINE NUMBER. pscanner.pp compiles CLEAN on its own at this binary, which is the cheap discriminator and how the misattribution was found. Third arrangement of this corpus's coordinate problem — right line, wrong file — after "stale near: across a unit boundary" and "line equal to the file length". Grep the identifier across the corpus before trusting in:.

Also: the mangled suffix in no overload of PeekOper$62774 matches is not stable — the same source at a different build printed PeekOper$62727. Do not use it as a search key or a ticket title.

Not caused by the fix that unmasked it

The three errors appeared only after [[bug-p-a-parameterless-method-is-undefined-as-a-by-ref-argument]] and the ENotSupportedException declaration removed the earlier walls. Isolated by building the RTL fix WITHOUT the parser fix: all six errors are present together, so these three are pre-existing and were hidden by a recovery cascade, not introduced. Binary 0426b285ba35 for that arm.

Log