← board

RTTI read of a getter-method property answers 0

Repro

uses typinfo;
type
  TThing = class(TObject)
  private
    FI: Integer; FS: string;
    function GetI: Integer;
    function GetS: string;
  published
    property ByField: Integer read FI write FI;
    property ByMethod: Integer read GetI write FI;
    property StrByMethod: string read GetS write FS;
  end;
function TThing.GetI: Integer; begin GetI := FI + 100; end;
function TThing.GetS: string; begin GetS := 'via-' + FS; end;

with t.FI = 7, t.FS = 'x':

property read directly read through RTTI
ByField 7 7
ByMethod 107 0
StrByMethod via-x ''

No error, no diagnostic — a plausible zero.

Cause

GetOrdProp and GetStrProp are written as

if p^.GetKind = 0 then
begin
  ... read the field at p^.GetRef ...
end;
{ no else }

GetKind = 1 means GetRef is the getter method's code pointer, and the compiler does emit it — verified: property Dbl: Double read GetD write SetD emits GetKind=1 SetKind=1, and even read GetI write FI emits GetKind=1 SetKind=0. The function falls off the end with its result still at the initialising := 0.

One arm of a double case. SetOrdProp and SetStrProp in the same file already handle SetKind = 1 by calling through a TOrdSetter / TStrSetter procedural type — the write direction was finished and the read direction was not. devdocs/dev/normalise-dont-special-case.md: the second path is the one that stays broken.

Second defect in the same lines

SetOrdProp declares its setter trampoline as

TOrdSetter = procedure(Self: Pointer; v: Integer);

and calls setter(instance, Integer(v)) for every width. An Int64 property with a setter method therefore has its value truncated to 32 bits on the way in. Same shape of defect (the width is known from p^.OrdType and is being ignored), so it is fixed here rather than filed separately.

Fix

Dispatch the read side through width-correct procedural types, mirroring what the write side does, and give the write side the same width discrimination. A getter's return width must be declared exactly — reading an Integer-returning getter through an Int64-typed trampoline reads whatever the ABI left in the high half of the return register.

Gate

Track B's: make lib-test, plus a test/lib_typinfo_props.pas asserting every property shape (field/method read, field/method write) round-trips through the accessors and agrees with the direct property read.

Resolution (2026-08-28, frankB)

Fixed in lib/rtl/typinfo.pas alongside [[feature-typinfo-facade-unit]], which is what surfaced it — the facade reads properties, so it could not be built on accessors that answered 0.

The single TOrdSetter = procedure(Self: Pointer; v: Integer) was replaced by a trampoline per width (TOrdGetter8s/8u/16s/16u/32s/32u/64, the matching setters, plus string, Single, Double and object shapes), because a procedural type is how the ABI is told what to read and write: an Integer-returning getter called through an Int64-shaped one reads whatever the callee left in the high half of the return register. GetOrdProp and GetStrProp grew the GetKind = 1 branch they never had; SetOrdProp stopped casting every setter argument to Integer.

Both defects were one shape — a width or a dispatch decision available in p^.OrdType and ignored.

Verified against $(PXX_STABLE) v389 (325b4479070a): the ticket's own repro now answers 107 and via-x where it answered 0 and ''.

Regression: test/lib_typinfo_props.pas, wired into make lib-test. Every property kind appears TWICE, once read FField and once read GetIt, and each getter perturbs the value (+100, 'via-', doubling) so a silently-skipped method call reads as the raw field rather than as the right answer by luck. The m_int64_setter_not_truncated row is the second defect's.

Gate: make lib-test green, make demos 35/35 (typinfo is under all of lib/pcl), both against v389.

Log