feat(semantic+codegen): implicit-Self write through a default array property

Field[I] := V inside a method, where Field is a class field of the current
class carrying a writable default array property, reported "Undeclared variable
'Field'": AnalyseStaticSubscriptAssign's implicit-Self branch only handled
array-typed fields, so a class field with a default property fell through.

Extend that branch: when the Self field is a class with a writable default
property, set IsImplicitSelf + ImplicitFieldInfo + PropWriteInfo and lower to
the default property's setter (mirroring the variable-receiver path). Both
codegen backends gain an implicit-Self receiver in the static-subscript
property-write path: load Self, reach the field slot, dereference to the
field's object, then call the setter.

(The implicit-Self read Field[I] already worked.) e2e test writes and reads a
default property through a Self field from inside methods, verified on QBE and
native.
This commit is contained in:
Andrew Haines 2026-06-24 16:50:59 -04:00 committed by Graeme Geldenhuys
parent 1bd52ec39a
commit dc3b7a1c2e
4 changed files with 104 additions and 9 deletions

View file

@ -12701,12 +12701,24 @@ begin
Self.Emit(#9'pushq %rax');
Self.EmitExprToEax(SSA.IndexExpr); { index }
Self.Emit(#9'pushq %rax');
if Self.IsLocal(SSA.ArrayName) then
Self.Emit(Format(#9'movq %s, %%rdi', [Self.VarOperand(SSA.ArrayName)]))
if SSA.IsImplicitSelf then
begin
{ Self.Field[I] := V load Self, reach the field slot, deref to the
field's class object (the setter receiver). }
Self.Emit(Format(#9'movq %s, %%rdi', [Self.VarOperand('Self')]));
if SSA.ImplicitFieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rdi', [SSA.ImplicitFieldInfo.Offset]));
Self.Emit(#9'movq (%rdi), %rdi');
end
else
Self.Emit(Format(#9'movq %s(%%rip), %%rdi', [SSA.ArrayName]));
if SSA.IsVarParam then
Self.Emit(#9'movq (%rdi), %rdi'); { var-param: slot -> instance }
begin
if Self.IsLocal(SSA.ArrayName) then
Self.Emit(Format(#9'movq %s, %%rdi', [Self.VarOperand(SSA.ArrayName)]))
else
Self.Emit(Format(#9'movq %s(%%rip), %%rdi', [SSA.ArrayName]));
if SSA.IsVarParam then
Self.Emit(#9'movq (%rdi), %rdi'); { var-param: slot -> instance }
end;
Self.Emit(#9'popq %rsi'); { index }
Self.Emit(#9'popq %rdx'); { value }
Self.EmitPropAccessorCallNative(SSA.PropOwnerType,

View file

@ -14703,14 +14703,34 @@ begin
if AStmt.PropWriteInfo <> nil then
begin
DefProp := TPropertyInfo(AStmt.PropWriteInfo);
RecvTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %s',
[RecvTemp, VarRef(AStmt.ArrayName, AStmt.IsGlobal)]));
if AStmt.IsVarParam then
if AStmt.IsImplicitSelf then
begin
{ Self.Field[I] := V load Self, reach the field slot, deref to the
field's class object (the setter receiver). }
RecvTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %%_var_Self', [RecvTemp]));
if AStmt.ImplicitFieldInfo.Offset > 0 then
begin
ElemPtr := AllocTemp();
EmitLine(Format(' %s =l add %s, %d',
[ElemPtr, RecvTemp, AStmt.ImplicitFieldInfo.Offset]));
RecvTemp := ElemPtr;
end;
ElemPtr := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [ElemPtr, RecvTemp]));
RecvTemp := ElemPtr;
end
else
begin
RecvTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %s',
[RecvTemp, VarRef(AStmt.ArrayName, AStmt.IsGlobal)]));
if AStmt.IsVarParam then
begin
ElemPtr := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [ElemPtr, RecvTemp]));
RecvTemp := ElemPtr;
end;
end;
IdxW := EmitExpr(AStmt.IndexExpr);
IdxQType := QbeTypeOf(DefProp.IndexTypeDesc);

View file

@ -12115,6 +12115,31 @@ begin
Format('''%s'' element', [AStmt.ArrayName]), AStmt.Line, AStmt.Col);
Exit;
end;
{ Implicit Self.Field[I] := V where Field is a class with a writable
default array property lower to its setter on the field's object. }
if (BaseInfo <> nil) and (BaseInfo.TypeDesc <> nil) and
(BaseInfo.TypeDesc.Kind = tyClass) then
begin
DefProp := TRecordTypeDesc(BaseInfo.TypeDesc).FindDefaultProperty();
if (DefProp <> nil) and (DefProp.WriteMethod <> '') then
begin
AStmt.IsImplicitSelf := True;
AStmt.ImplicitFieldInfo := BaseInfo;
AStmt.PropWriteInfo := DefProp;
AStmt.PropOwnerType := PropAccessorOwner(
TRecordTypeDesc(BaseInfo.TypeDesc).Name, DefProp.WriteMethod);
AStmt.PropAccessorVSlot := PropAccessorVSlot(
TRecordTypeDesc(BaseInfo.TypeDesc).Name, DefProp.WriteMethod);
IdxType := AnalyseExpr(AStmt.IndexExpr);
if DefProp.IndexTypeDesc <> nil then
CheckTypesMatch(DefProp.IndexTypeDesc, IdxType, 'default property index',
AStmt.Line, AStmt.Col);
ValType := AnalyseExpr(AStmt.ValueExpr);
CheckTypesMatch(DefProp.TypeDesc, ValType, 'default property assignment',
AStmt.Line, AStmt.Col);
Exit;
end;
end;
end;
SemanticError(
Format('Undeclared variable ''%s''', [AStmt.ArrayName]),

View file

@ -43,6 +43,9 @@ type
Obj.Field[I] := V and Obj.Prop[I] := V. }
procedure TestRun_DefaultProperty_WriteThroughField;
procedure TestRun_DefaultProperty_WriteThroughProperty;
{ Implicit-Self write/read: Field[I] := V / Field[I] inside a method, where
Field is a class with a default array property. }
procedure TestRun_DefaultProperty_WriteThroughImplicitSelf;
{ Static-array field accessed from inside a method (implicit Self). }
procedure TestRun_StaticArrayField_ReadInMethod;
procedure TestRun_StaticArrayField_WriteInMethod;
@ -439,6 +442,41 @@ begin
AssertRunsOnAll(SrcDefaultWriteProp, '9' + LE, 0);
end;
const
{ Implicit Self.Field[I] := V and Field[I] read, inside methods. }
SrcDefaultWriteImplicitSelf = '''
program P;
type
TVec = class
FData: array[0..4] of Integer;
function Get(i: Integer): Integer; begin Result := FData[i] end;
procedure Put(i: Integer; v: Integer); begin FData[i] := v end;
property Items[i: Integer]: Integer read Get write Put; default;
end;
TOwner = class
FVec: TVec;
constructor Create;
procedure Fill;
function Sum: Integer;
end;
constructor TOwner.Create; begin inherited Create; FVec := TVec.Create end;
procedure TOwner.Fill; begin FVec[0] := 3; FVec[1] := 4; end;
function TOwner.Sum: Integer; begin Result := FVec[0] + FVec[1]; end;
var o: TOwner;
begin
o := TOwner.Create;
o.Fill();
WriteLn(o.Sum());
o := nil
end.
''';
procedure TE2EPropertyTests.TestRun_DefaultProperty_WriteThroughImplicitSelf;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcDefaultWriteImplicitSelf, '7' + LE, 0);
end;
initialization
RegisterTest(TE2EPropertyTests);