← board

Advanced records: methods (and public/private) inside a record

What is missing

A record with methods does not parse at all:

type
  TPt = record
    X, Y: Longint;
    procedure Init(ax, ay: Longint);     { Expected: :, but got: procedure }
    function Sum: Longint;
  end;

Nor do visibility sections or constructors inside a record (public, constructor Create(...)).

This is FPC's {$modeswitch advancedrecords}, which the RTL itself leans on: TPoint, TSize, TRect in rtl/inc/typshrdh.inc are all advanced records with constructors and operators, and types.pp re-exports them. So today those declarations only partly land.

Why it matters beyond syntax

It is the standing lead on [[bug-pascal-unknown-type-silently-integer]]: with the unknown-type fallback turned into an error, the fgl chain fails with unknown type: TPoint, and typshrdh.inc's TPoint is exactly one of these advanced records (methods, public, and a self-referencing constructor Create(apt: TPoint)). How much of that declaration currently lands is the first thing to check there.

Shape

A record method is an ordinary method whose implicit Self is the RECORD, passed BY REFERENCE (records are carried by address in this IR already, so this is the natural lowering — no value-copy semantics to invent). Concretely:

Operators (class operator Add) are a separate, later slice — do not fold them in.

Gate

make test + self-host byte-identical + cross.

Log

Landed 2026-07-13

Methods inside a record now work: declaration, implementation, calls, mutation, and record-valued results. Visibility sections (public / private / strict private) and class function/class procedure inside a record are consumed too.

Self is the RECORD, passed BY REFERENCE. That is not a detail — the first attempt passed it by value and p.SetX silently mutated a COPY, leaving the receiver unchanged. A by-value Self loses every write.

No VMT: records have no inheritance, so every call is static, which is why this ended up far smaller than the class-method path. The implementation side reuses ParseSubroutine unchanged — FindUClass already finds record rows.

The bug that actually cost the time

The implicit-Self field access hardcoded Self as tyClass. A record Self was therefore dereferenced as an object POINTER, and the field access segfaulted. It now takes the symbol's real type (Syms[selfIdx].TypeKind) — which is the correct thing for the class path too, it was simply never exercised by anything but a class.

Not included (deliberately)

class operator (Add / = / Explicit …) and record CONSTRUCTORS. Both are separate slices; folding them in would have widened this well past a reviewable change. FPC's typshrdh.inc uses both, so a full parse of it still needs them.

Gate

make test green, make lib-test green, self-host byte-identical, testmgr --tier full 1215/1215 GREEN. Regression b268 pins the by-ref receiver (a by-value Self would silently pass every other assertion).