fix(codegen): load class pointer before offsetting in var-param L-value

EmitLValueAddr handled the leaf TFieldAccessExpr form (no .Base) only
for inline-record and var-record-param cases.  For a class field --
where the variable's slot stores a pointer to the heap object -- the
base was emitted as VarRef(...), so a var-param call site emitted
`add &N, offset` instead of `add (loadl N), offset`.  Reads through
the var pointer hit the variable's own storage and the write never
reached the field.

EmitInstancePtr already had the symmetric branch (Fld.IsClassAccess ->
load through VarRef); add the missing case to EmitLValueAddr so calls
like `Fill(N.Value, N.IsBig)` reach the heap object.

Regression coverage:
  * TVarParamTests.TestCodegen_VarParam_ClassFieldLeaf_LoadsObjectPointer
    asserts the IR contains `loadl $N` and no bare `add $N, ...`.
  * TE2EClasses2Tests.TestRun_VarParam_ClassFields_WritebackVisible
    compiles and runs the documented reproducer and checks stdout
    shows 4096 / 1 instead of 0 / 0.
This commit is contained in:
Graeme Geldenhuys 2026-05-22 16:02:34 +01:00
parent 7dbb981665
commit ca622e258a
3 changed files with 86 additions and 0 deletions

View file

@ -3687,6 +3687,16 @@ begin
FldAcc := TFieldAccessExpr(AExpr);
if FldAcc.Base <> nil then
BaseAddr := EmitInstancePtr(FldAcc.Base)
else if FldAcc.IsClassAccess then
begin
{ Class field leaf: the variable's slot holds a pointer to the heap
object — load it so the offset addition reaches the field, not a
location adjacent to the slot itself. }
T := AllocTemp;
EmitLine(Format(' %s =l loadl %s',
[T, VarRef(FldAcc.RecordName, FldAcc.IsGlobal)]));
BaseAddr := T;
end
else if FldAcc.IsVarParam then
begin
{ Var-record param leaf: dereference the param slot. }

View file

@ -49,6 +49,7 @@ type
procedure TestRun_Supports_ThreeArg_AssignsAndCalls;
procedure TestRun_ConstructorOverload_PicksCorrectArity;
procedure TestRun_MethodReadsProgramGlobal;
procedure TestRun_VarParam_ClassFields_WritebackVisible;
end;
implementation
@ -484,6 +485,29 @@ const
end.
''';
SrcVarParamClassFields =
'''
program VarParamClassFields;
procedure Fill(var V: Int64; var F: Boolean);
begin
V := 4096;
F := True
end;
type
TNode = class
Value: Int64;
IsBig: Boolean;
end;
var N: TNode;
begin
N := TNode.Create;
Fill(N.Value, N.IsBig);
WriteLn(N.Value);
WriteLn(N.IsBig);
N.Free
end.
''';
SrcCtorOverloadArity =
'''
program P;
@ -853,6 +877,19 @@ begin
AssertEquals('global read by method', '42' + LE, Output);
end;
procedure TE2EClasses2Tests.TestRun_VarParam_ClassFields_WritebackVisible;
var Output: string; RCode: Integer;
begin
{ Regression for bugs.txt BUG-001 — Fill(N.Value, N.IsBig) where N is a
class reference must address into the heap object, not into the storage
slot of N itself. Before the fix the writes landed in unrelated memory
and the WriteLn calls printed 0 / False. }
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcVarParamClassFields, Output, RCode));
AssertEquals('exit code 0', 0, RCode);
AssertEquals('var-param writeback visible through class field', '4096' + LE + '1' + LE, Output);
end;
initialization
RegisterTest(TE2EClasses2Tests);

View file

@ -57,6 +57,14 @@ type
where the parameter is var-typed. }
procedure TestSemantic_VarParam_FieldAccess_OK;
procedure TestSemantic_VarParam_DerefField_OK;
{ Var-param call where the actual argument is a CLASS field
(C.Field where C is a class reference, not a record). The address
must be computed as (loadl C) + offset, not (&C + offset), because
the variable holding the class reference stores a pointer to the
heap object — adding the offset to the variable's address points
at unrelated memory. See BUG-001 in bugs.txt. }
procedure TestCodegen_VarParam_ClassFieldLeaf_LoadsObjectPointer;
end;
implementation
@ -400,6 +408,37 @@ begin
Prog.Free;
end;
procedure TVarParamTests.TestCodegen_VarParam_ClassFieldLeaf_LoadsObjectPointer;
const
SrcClassFieldVarArg =
'''
program ClassFieldVar;
procedure Fill(var V: Integer);
begin V := 4096 end;
type
TNode = class
Pad: Integer;
Value: Integer;
end;
var N: TNode;
begin
N := TNode.Create;
Fill(N.Value)
end.
''';
var
IR: string;
begin
IR := GenIR(SrcClassFieldVarArg);
{ N is a global class reference, so the slot is $N and stores a pointer
to the heap object. Computing the address of N.Value must first load
that pointer; `add $N, <offset>` would point at unrelated memory. }
AssertTrue('loads the class pointer from $N before offsetting',
Pos('loadl $N', IR) > 0);
AssertFalse('must NOT add a field offset to $N (the slot address)',
Pos('add $N,', IR) > 0);
end;
initialization
RegisterTest(TVarParamTests);