← board

bug-subclass-field-offset-calculation (Track A)

Description

The compiler miscalculates the field offset of subclass fields under class casting or type resolving when the subclass inherits from a base class with fields (like TControl inheriting from TComponent).

Specifically, in lib/pcl/gtk3widgets.pas, when querying the Handle field of TControl via:

h := TControl(AControl).Handle;

where AControl is typed as TComponent, the returned value is incorrect (e.g. 4273562 instead of the actual 166408768 written to the FHandle field during TForm.CreateHandle).

It appears the compiler resolves the field offset of FHandle relative to 0 instead of relative to sizeof(TComponent) when compiling the subclass access, or the class-to-class typecasting miscalculates the base offset.

Reproduction / Evidence

In test/gui/test_pcl_window.pas, compiling and running prints:

TGtk3WidgetSet.CreateForm: win=166408768
TForm.CreateHandle: Handle=166408768
created form
TGtk3WidgetSet.SetText: AControl=139436287328344
TGtk3WidgetSet.SetText: h=4273562

Done Criteria

TControl(AControl).Handle retrieves the correct FHandle value, and all GUI tests using widgetset handle access run without GTK assertion failures.

Log

DONE 2026-06-20 (commit f9e0069)

Root cause: a class typecast as an EXPRESSION (TClass(expr)) had no handler — it fell through to the pointer-type-alias cast path, whose AN_PTR_CAST node ResolveNodeRec cannot map to a class record, so every trailing .field resolved at offset ~0 (reading the VMT pointer). Affected ALL class casts, not just inherited fields (even TBase(d).a on the first field broke).

Fix: new AN_CLASS_CAST AST node (hard reinterpret, no runtime check unlike as). The instance pointer passes through unchanged; ResolveNodeRec maps it to REC_UCLASS_BASE+ci, so .field/[i] resolve offsets against TClass (incl. inherited base size). Wired into the expression parser (reads) and the statement lvalue parser (writes), reusing ParseClassRecordSelectors. Files: defs.inc, symtab.inc (ResolveNodeRec), ir.inc (passthrough lowering), parser.inc (both parse sites). Validated x86-64 + i386; test/test_class_cast_field.pas in test-core; self-host byte-identical.

Method calls on a hard cast — TClass(x).Method(args), static + virtual — landed in a follow-up commit (355192a, pinned v22): ParseClassRecordSelectors now also dispatches method calls (Self = the cast node; virtual stays dynamic on the real object; ref/array args by lvalue), and the statement parser accepts a method-call result as a call statement. Validated x86-64 + i386 incl. dynamic dispatch through an upcast. No remaining known gap.