Compiler Elision

Whilst working on some outcome-based types in Delphi Florence, I noticed something unexpected.

The TResult and TMaybe types I was developing were designed to be immutable.

To avoid record duplication — creating a new record and assigning it to Result — both class factory functions and instance setters were provided. To prevent mutation, I relied upon the class operator Initialize, which is supposed to run whenever the record is initialized:

class operator TResult<T>.Initialize;
begin
  fState := rsUnknown;
end;

The mutating instance methods then relied upon that initial state:

// class factory methods

class function TResult<T>.Ok(const aValue: T): TResult<T>;
begin  
  Result.SetOk(aValue);
end;

class function TResult<T>.Err(const aMessage: string): TResult<T>;
begin  
  Result.SetErr(aMessage);
end;

// instance setters to avoid duplicating Result in functions

procedure TResult<T>.SetOk(const aValue: T);
begin
  Ensure.IsTrue(fState = rsUnknown, INIT_ERROR);

  fState := rsOk;
  fValue := aValue;
end;

procedure TResult<T>.SetErr(const aMessage: string);
begin
  Ensure.IsTrue(fState = rsUnknown, INIT_ERROR);

  fState := rsErr;
  fError := aMessage;
end;

To my surprise, I was getting the INIT_ERROR on the second assignment of res in a test:

procedure TResultFixture.TestOrElse;
begin
  var res := TResult<integer>.Err('error');
  var val := res.OrElse(3);

  Assert.AreEqual(3, val);

  res := TResult<integer>.Ok(4);  // error occurs here
  val := res.OrElse(3);

  Assert.AreEqual(4, val);
end;

It turns out that there is no semantic requirement for a separate record instance to be created.

The compiler is free to construct the return value directly in the assignment target, which may already contain state from its previous value. Consequently, Result may begin with the existing state of the assignment target rather than the state normally established by Initialize.

In this situation, the class Initialize operator did not run.


Inline - A Solution?

In this particular case, adding the inline directive to the function declaration causes Initialize to run:

class function Ok(const aValue: T)
  : TResult<T>; static; inline;
            
class function Err(const aMessage: string = '')
  : TResult<T>; static; inline;

With these functions inlined, the test passes again.

Once the function body is inlined into the caller, the compiler may:

  • introduce a temporary for expression evaluation order,
  • generate different cleanup code for managed fields (strings, interfaces, dynarrays),
  • or simply produce different debug-visible temporaries.

So it appears that a separate record is being created and then assigned to res.


Defensive Coding

It's still dangerous to rely on this behaviour because inline is just a compiler hint and might be ignored.

It's best to be defensive:

// class factory methods

class function TResult<T>.Ok(const aValue: T): TResult<T>;
begin  
  Result.fState := rsUnknown; // don't rely on Initialize here
  Result.SetOk(aValue);
end;

class function TResult<T>.Err(const aMessage: string): TResult<T>;
begin  
  Result.fState := rsUnknown; // don't rely on Initialize here
  Result.SetErr(aMessage);
end;