Search code examples
delphirecordcompiler-bug

Why does a record constructor misbehave in inline functions?


In the following code the record constructor does something strange.
It works OK in all instances, except in the line marked below:

program Project9;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections;

type
  TIntegerPair = TPair<Integer, Integer>;

type
  TMiniStack<T> = record
  public
    Items: array[0..10] of T;
    SP: integer;
    procedure Init;
    procedure PushInline(const Item: T); inline;
    procedure PushNormal(const Item: T);
  end;

procedure TMiniStack<T>.Init;
begin
  FillChar(Items, SizeOf(Items), #0);
  SP:= 0;
end;

procedure TMiniStack<T>.PushInline(const Item: T);
begin
  Items[SP]:= Item;
  Inc(SP);
end;

procedure TMiniStack<T>.PushNormal(const Item: T);
begin
  Items[SP]:= Item;
  Inc(SP);
end;


procedure RecordConstructorFail;
var
  List1: TMiniStack<TIntegerPair>;
  List2: array[0..2] of TIntegerPair;
  Pair: TIntegerPair;
  a: string;
begin
  Writeln('start test...');
  FillChar(List1, SizeOf(List1), #0);
  List1.Init;
  List1.PushInline(TIntegerPair.Create(1, 1));
  List1.PushInline(Pair.Create(2, 2));   <<--- Failure
  List2[0]:= TIntegerPair.Create(1, 1);
  List2[1]:= Pair.Create(2, 2);
  if (List1.Items[0].Key <> 1) or (List1.Items[1].Key <> 2) then Writeln('something is wrong with List1-Inline');
  if (List2[0].Key <> 1) or (List2[1].Key <> 2) then Writeln('something is wrong with List1');
  List1.Init;
  List1.PushNormal(TIntegerPair.Create(1, 1));
  List1.PushNormal(Pair.Create(2, 2));
  if (List1.Items[0].Key <> 1) or (List1.Items[1].Key <> 2) then Writeln('something is wrong with List1-Normal');
  Writeln('Done');
  Readln(a);
  Writeln(a);
end;

begin
  RecordConstructorFail;
end.

Why does this line cause a failure?

List1.PushInline(Pair.Create(2, 2)); <<- Failure: Dumps the data somewhere else.

Is it a compiler bug?
Or am I missing something?

I'm using Delphi XE6.


Solution

  • In my view, inlined or not, an expression that is a constructor call on an instance does not evaluate to a value. And so cannot be passed as an argument. Think of

    Pair.Create(...)
    

    as though it is a procedure. It yields no value.

    In my view the code should be rejected by the compiler and the fact that it is not rejected is the bug.

    That the code appears to work when you don't inline it is, in my view, down to chance.


    This is all guesswork though. Record constructors aren't properly documented. The documentation for constructors is for classes and has not been updated to cover records. The documentation for classes says that the instance form is an expression that is a reference to the instance.

    When a constructor is called using an object reference (rather than a class reference), it does not create an object. Instead, the constructor operates on the specified object, executing only the statements in the constructor's implementation, and then returns a reference to the object.

    But this doesn't make much sense for a record because there are value types rather than reference types.

    My best guess is that the parser regards this expression as being the same type as the instance, because that's how it is for constructors of classes. But the codegen doesn't actually yield a value.