[fpc-devel] A call to help test pure functions

J. Gareth Moreton gareth at moreton-family.com
Fri Sep 29 21:28:19 CEST 2023


Hi everyone,

This has been something that's been in development for some time now, 
and I invite other Free Pascal users and developers to test the new 
feature... pure functions.  Its closest equivalent in C++ would be 
"constexpr".

A pure function has no side-effects - it doesn't affect the machine 
state outside of its scope.  More broadly, it is analogous to a 
mathematical function, so for a given input(s), will always return the 
same output.  The idea is that if a function is marked as pure, and 
confirmed to be by the compiler, then in certain situations, its result 
can be computed at compile time and the call replaced with the 
calculated result as a literal.  I've attached two examples showing pure 
functions in action, one that unrolls a for-loop, and one that handles 
recursion.  As the examples imply, to mark as a function as pure, simply 
use the new "pure" directive.  By default, subroutines are not 
considered pure because, while the compiler would easily be able to 
defermine if a function is actually pure or not, this would cause 
significant slowdown to the compilation process.

You can access my repository here: 
https://gitlab.com/CuriousKit/optimisations/-/tree/pure?ref_type=heads

Note that pure functions are subtly different to inline functions, and 
while there is a lot of overlap, they have different use cases.  Some 
pure functions can theoretically be extremely complex and would not be 
something you'd want to inline, but is guaranteed to produce a 
deterministic result for a given input.  A theoretical example would be 
a hash function (although my pure functions do not support pointers 
because the data they point to is not deterministic until you 
dereference it). Similarly, a small inline function that accesses a 
global variable (e.g. to read or modify a reference count) cannot be a 
pure function because the compiler has no way of knowing what that 
variable will be set to at compile time.

Currently there's no support to assign the result of a pure function to 
a constant, although I intend to find a means to support it one day.  
Additionally, procedures with "out" parameters unfortunately tend to not 
be evaluated correctly and will error out (the compiler might consider 
them impure even though they actually are.  If the "out" parameter is 
passed into another function through a call, even if it's to another 
pure function or itself (recursion), the parameter is considered 
"escaped" and so can't be optimised.  So far I haven't worked out how to 
get around this because the load nodes are marked as "address taken" as 
well as the definition of the formal parameter itself.  Temporarily 
modifying these definitions during pure function analysis is somewhat 
dangerous and error-prone.  I'll solve this eventually though.

One of my current goals is to make "IntToStr", which calls "Str" 
internally, a pure function.  One might question the point of this, 
since who in their right mind would write something like "Output := 
IntToStr(5);"  The reasul is that another pure function (one that may 
generate a string for a notification message, for example) may call 
IntToStr with an actual parameter that's a variable, but said variable 
is deterministic within the function and so would be replaced with an 
integer literal by the time "IntToStr" is evaluated.  If "IntToStr" can 
be successfully made a pure function, then that can be considered a 
milestone and other similar functions can follow.

Other possibilities:

for N := 1 to 10 do
   Y := N * SomePureFunc(X);

Here, X is an unknown local variable.  However, if it is not modified 
within the for-loop, SomePureFunc(X) will always return the same value 
(if it is actually a pure function), so the compiler could theoretically 
optimise it to the equivalent code:

Z := SomePureFunc(X);
for N := 1 to 10 do
   Y := N * Z;

This could be achieved through the tempcreate / tempref / tempdelete 
nodes that are often used when expanding inline functions.

Any suggestions, requests, bug reports or, heck, even a merge request, 
please send them my way!

Yours faithfully,

J. Gareth "Kit" Moreton
-------------- next part --------------
program pure1a;
{$MODE OBJFPC}
{$COPERATORS ON}

function Factorial(N: Cardinal): Cardinal; pure;
  var
    X: Integer;
  begin
    Result := 1;
    for X := N downto 2 do
      Result *= X;
  end;

begin
  WriteLn(Factorial(5));
end.
-------------- next part --------------
program pure1b;
{$MODE OBJFPC}

function Factorial(N: Cardinal): Cardinal; pure;
  begin
    if N < 2 then
	  Result := 1
	else
      Result := N * Factorial(N - 1);
  end;
  
begin
  WriteLn(Factorial(5));
end.


More information about the fpc-devel mailing list