Search code examples
delphiparametersundefined-behaviormethod-chaining

Value used as both output and (non-reference) input in a chain of methods


Consider the following minimal example of method chaining, where a floating-point variable is set (using an out parameter) by an early method and then passed (using a const parameter) to a later method in the chain:

program ChainedConundrum;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  ValueType = Double;
  TRec = record
    function GetValue(out AOutput: ValueType): TRec;
    procedure ShowValue(const AInput: ValueType);
  end;

function TRec.GetValue(out AOutput: ValueType): TRec;
begin
  AOutput := 394;
  Result := Self;
end;

procedure TRec.ShowValue(const AInput: ValueType);
begin
  Writeln(AInput);
end;

var
  R: TRec;
  Value: ValueType = 713;

begin
  R.GetValue(Value).ShowValue(Value);
  Readln;
end.

I initially expected this to print the floating-point number 394 (in some format), but it doesn't (necessarily); when I build the program using the 32-bit compiler of Delphi 10.3.2, the program prints 713. Stepping through the program using the debugger confirms that the initial, pre-GetValue value of Value is passed to ShowValue.

However, if I build this using the 64-bit compiler, 394 is printed. Similarly, if I change ValueType from Double to Int32, I get 394 in both versions. Int64 yields 394 in 64-bit and 713 in 32-bit. Strings yield the updated value. Classes work just like records. Class methods, however, as opposed to instance methods, always give me the updated value. And, of course, abandoning method chaining (R.GetValue(Value); R.ShowValue(Value)) does the same.

Not surprisingly, if I change the AInput parameter of ShowValue from a const (or undecorated value) parameter to a var parameter, I always get the updated value.

My conclusion is that either

  1. it is not allowed to both set and pass a variable in a chain of methods like this, or
  2. there's a bug in the compiler.

My question is: which is it? And if it isn't allowed, where does the documentation state this? I have so far not been able to find the relevant passage. (The phrase "sequence point" seems very rarely to occur anywhere near the phrase "Delphi" on the WWW.)


Solution

  • Everyone who has commented on this issue here or elsewhere agrees that this either "feels like" or "clearly" is a compiler bug.

    I've created issue RSP-29733 at the Embarcadero Jira.

    Turning to possible workarounds, notice that the issue seems to be that an old value of a variable is used by the compiler. Hence, the problem arise when the value is changed close to the use of the variable.

    However, the variable's address isn't changed, so if you pass the variable by reference instead of by value, the problem disappears. One way is to use a var parameter when the value is passed the second time, even though you don't need that, or even want that semantically.

    Hence, a more natural approach seems to be to use a const [Ref] parameter:

    procedure ShowValue(const [Ref] AInput: ValueType);
    

    This has the same semantics as an undecorated const parameter but forces the compiler to pass the variable by reference, thus avoiding the bug.