← board

.Free / .Destroy off anything but a simple variable

Repro

program af;
uses SysUtils;
type
  TFoo = class n: Integer; end;
  TBox = record f: TFoo; end;
  THolder = class f: TFoo; end;
var r: TBox; h: THolder; a: array[0..1] of TFoo; d: array of TFoo; v: TFoo;
begin
  ...
end.
expression pxx
v.Free ok
v.Destroy error: "Destroy": no such member on this record/class
r.f.Free (record field) error: "Free": no such member ...
h.f.Free (class field) error: "Free": no such member ...
a[0].Free (static array) error: "Free": no such member ...
d[0].Free (dynamic array) error: "Free": no such member ...
FreeAndNil(r.f) ok — the workaround
a[0].ClassName ok — other TObject members are fine on any designator

FPC compiles all of them.

Why this shape matters

FList[i].Free, Workers[k].Free, FOwner.Child.Free are everyday Pascal. The failure is at least loud — a compile error, never a wrong value or a missed destructor — but it forces FreeAndNil or a temporary at every site, which is exactly the kind of reshaping CLAUDE.md's platonic-code rule says not to do quietly.

That a[0].ClassName works while a[0].Free does not is the tell: this is not about member lookup on an indexed base in general.

Root cause (FIXED, except Destroy)

Free is not a member of any class the frontend knows; it was recognised by ad-hoc token-shape special cases in compiler/parser.inc — the load-bearing one being an identifier followed literally by . Free ;. Any base with a selector in front of it — an index, a field, an as-cast — misses that shape, falls through to ordinary member lookup, and there is no Free to find.

The fix adds the desugar at the two general member-access fall-throughs, right before the RequireRecMember call that would reject it: one in ParseLValueAST (which is where a[0].Free, d[0].Free, r.f.Free, h.f.Free land) and one in ParseClassRecordSelectors (where a (-led statement like (o as T).Free; lands). Both route into the existing GenMakeFreeObjectExpr, the same generator TClass(expr).Free already used.

Two predicates carry the conditions:

Still open: Destroy

v.Destroy on a class with no declared destructor is still an error. It is not folded in on purpose: Free is the nil-guarded wrapper, and desugaring a direct .Destroy to the same thing would change what it means. Filed nowhere yet — raise it if real code wants it.

Verification

test/test_free_designator.pas, wired into make test, asserts the semantics and not just that it compiles: five objects freed through five different designator shapes each log from their destructor (d1d2d3d4d5), a user-declared Free wins (U), and nil.Free stays a no-op.

Gate

Track P: make test + self-host fixedpoint (byte-identical).

Log