Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-pascal / 2019-11

Generic type conflicts

26 messages · 6 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Ryan Joseph

I've wanted to make a generic version of a vector for a while but I always give up because it seems not very possible. It's probably not even a great idea because many methods don't translate between float and integer but I wanted to prevent other code duplication if possible.

Here's an example of how things break down. Are there any solutions for this currently? I feel like generics need to support some compiler directives so different blocks of code can specialize different depending on the type.

{$mode objfpc}
{$modeswitch advancedrecords}
program generic_vector_2;
uses
  Math;
type
  generic TVec2<TScalar> = record
    x, y: TScalar;
    function Normalize: TVec2;
  end;
  TVec2f = specialize TVec2<Float>;
  TVec2i = specialize TVec2<Integer>;
function TVec2.Normalize: TVec2;
var
  fac: TScalar;
begin
  // Can't determine which overloaded function to call
  // Incompatible types: got "Extended" expected "LongInt"
  fac:=Sqrt(Sqr(x) + Sqr(y));
  if fac0.0 then begin
    // Incompatible types: got "Single" expected "LongInt"
    fac:=1.0/fac;
    result.x:=x*fac;
    result.y:=y*fac;
  end else begin
    result.x:=0;
    result.y:=0;
  end;
end;
begin
end.

Regards,
Ryan Joseph

Reply by email

Cyrax
to Ryan Joseph
Ryan Joseph wrote 47 lines
I've wanted to make a generic version of a vector for a while but I always give up because it seems not very possible. It's probably not even a great idea because many methods don't translate between float and integer but I wanted to prevent other code duplication if possible. Here's an example of how things break down. Are there any solutions for this currently? I feel like generics need to support some compiler directives so different blocks of code can specialize different depending on the type. {$mode objfpc} {$modeswitch advancedrecords} program generic_vector_2; uses Math; type generic TVec2<TScalar> = record x, y: TScalar; function Normalize: TVec2; end; TVec2f = specialize TVec2<Float>; TVec2i = specialize TVec2<Integer>; function TVec2.Normalize: TVec2; var fac: TScalar; begin // Can't determine which overloaded function to call // Incompatible types: got "Extended" expected "LongInt" fac:=Sqrt(Sqr(x) + Sqr(y)); if fac0.0 then begin // Incompatible types: got "Single" expected "LongInt" fac:=1.0/fac; result.x:=x*fac; result.y:=y*fac; end else begin result.x:=0; result.y:=0; end; end; begin end. Regards, Ryan Joseph _______________________________________________ fpc-pascal maillist - https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-pascal

You need to do a explicit typecasting.

...
fac:=Sqrt(Sqr(TScalar(x)) + Sqr(TScalar(y)));
...
fac:=1.0/TScalar(fac);

Reply by email

Ryan Joseph
to Cyrax
Show quoted text 8 lines
On Nov 2, 2019, at 11:06 AM, Cyrax via fpc-pascal wrote: You need to do a explicit typecasting. ... fac:=Sqrt(Sqr(TScalar(x)) + Sqr(TScalar(y))); ... fac:=1.0/TScalar(fac);

Doesn't work. Try running the example.

Regards,
Ryan Joseph

Reply by email

Ben Grasset
to Ryan Joseph: I've wanted to make a generic version of a vector for a while but I always give up…

On Sat, Nov 2, 2019 at 11:51 AM Ryan Joseph via fpc-pascal <

Show quoted text 6 lines
wrote: Are there any solutions for this currently? I feel like generics need to support some compiler directives so different blocks of code can specialize different depending on the type.

There's one, that works *exactly* like you want (although it's not currently present in trunk FPC) which is to apply the (still-working) patch Sven posted in this mailing list thread a few years ago:

It introduces a const-evaluated "ternary if" of the form "Variable := if Expression else Expression;" where only the true branch is taken into consideration by the it with handy compiler intrinsics like GetTypeKind makes stuff like the following modified version of your code possible:

{$mode objfpc}
{$modeswitch advancedrecords}
program generic_vector_2;
type
  generic TVec2<TScalar> = record
    X, Y: TScalar;
    class function Create(const AX, AY: TScalar): TVec2; static; inline;
    class operator / (const Left, Right: TVec2): TVec2; inline;
  end;
  class function TVec2.Create(const AX, AY: TScalar): TVec2;
  begin
    with Result do begin
      X := AX;
      Y := AY;
    end;
  end;
  class operator TVec2./(const Left, Right: TVec2): TVec2;
  begin
    // GetTypeKind is evaluated at compile time, so the following works
perfectly with "ternary if".
    Result :=
      if GetTypeKind(TScalar) = tkFloat then
        TVec2.Create(Left.X / Right.X, Left.Y / Right.Y)
      else
        TVec2.Create(Left.X div Right.X, Left.Y div Right.Y);
  end;
type
  TVec2f = specialize TVec2<Single>;
  TVec2i = specialize TVec2<Integer>;
begin
end.

Reply by email

Sven Barth
to Ben Grasset

Ben Grasset via fpc-pascal schrieb am
Di., 5. Nov. 2019, 19:11:

Show quoted text 58 lines
On Sat, Nov 2, 2019 at 11:51 AM Ryan Joseph via fpc-pascal < > wrote: > Are there any solutions for this currently? I feel like generics need to > support some compiler directives so different blocks of code can specialize > different depending on the type. > There's one, that works *exactly* like you want (although it's not currently present in trunk FPC) which is to apply the (still-working) patch Sven posted in this mailing list thread a few years ago: It introduces a const-evaluated "ternary if" of the form "Variable := if Expression else Expression;" where only the true branch is taken into consideration by the compiler at all. Combining it with handy compiler intrinsics like GetTypeKind makes stuff like the following modified version of your code possible: {$mode objfpc} {$modeswitch advancedrecords} program generic_vector_2; type generic TVec2<TScalar> = record X, Y: TScalar; class function Create(const AX, AY: TScalar): TVec2; static; inline; class operator / (const Left, Right: TVec2): TVec2; inline; end; class function TVec2.Create(const AX, AY: TScalar): TVec2; begin with Result do begin X := AX; Y := AY; end; end; class operator TVec2./(const Left, Right: TVec2): TVec2; begin // GetTypeKind is evaluated at compile time, so the following works perfectly with "ternary if". Result := if GetTypeKind(TScalar) = tkFloat then TVec2.Create(Left.X / Right.X, Left.Y / Right.Y) else TVec2.Create(Left.X div Right.X, Left.Y div Right.Y); end; type TVec2f = specialize TVec2<Single>; TVec2i = specialize TVec2<Integer>; begin end.

Does this really work? Cause the compiler should nevertheless typecheck the code in the other branch and thus without casts that shouldn't compile. Also if it should indeed work, it would also work without the if-expression, but with an if-statement.

Regards,
Sven

Reply by email

Ben Grasset
to Sven Barth

On Tue, Nov 5, 2019 at 5:24 PM Sven Barth via fpc-pascal <

Show quoted text 7 lines
wrote: Does this really work? Cause the compiler should nevertheless typecheck the code in the other branch and thus without casts that shouldn't compile. Also if it should indeed work, it would also work without the if-expression, but with an if-statement.

Hm, it actually doesn't quite for that particular example, testing it for real. It definitely did for a few other generic things I experimented with though (that were impossible otherwise.) I'll see if I can find the source files anywhere.

Why would it work the same way as a normal if statement, though? Isn't the non-evaluation of the false branch pretty much the only point of the ternary "form"? E.G. you'd specifically use it when you had two branches that you knew would never *both* be compilable.
You'd be able to do the same thing with an {$IFDEF}, for example, if {$IFDEFS} specifically carried through across generic specializations.

Reply by email

Sven Barth
to Ben Grasset

Ben Grasset via fpc-pascal schrieb am
Mi., 6. Nov. 2019, 04:49:

Show quoted text 21 lines
On Tue, Nov 5, 2019 at 5:24 PM Sven Barth via fpc-pascal < > wrote: > Does this really work? Cause the compiler should nevertheless typecheck > the code in the other branch and thus without casts that shouldn't compile. > Also if it should indeed work, it would also work without the > if-expression, but with an if-statement. > Hm, it actually doesn't quite for that particular example, testing it for real. It definitely did for a few other generic things I experimented with though (that were impossible otherwise.) I'll see if I can find the source files anywhere. Why would it work the same way as a normal if statement, though? Isn't the non-evaluation of the false branch pretty much the only point of the ternary "form"? E.G. you'd specifically use it when you had two branches that you knew would never *both* be compilable. You'd be able to do the same thing with an {$IFDEF}, for example, if {$IFDEFS} specifically carried through across generic specializations.

A normal if-statements has the same non-evaluation. However that it might not be evaluated does not mean that it is not type checked. Especially as the type of the expression is taken from the if-branch. Imagine this:

=== code begin ===

SomeLongint := if SomethingFalse then SomeOtherLongint else SomeString;

=== code end ===

The if-branch is obviously not taken here. But nevertheless the type of the whole condition is LongInt, because the type of the if-branch is LongInt. Thus there will be a type error in the else-branch even before the compiler determines that the if-condition is constant and False.

And the if-expression behaves like an if-clause, because the compiler transforms it as such. The above example becomes (in the node tree) essentially the following (general case without further optimizations):

=== code begin ===

if SomethingFalse then
  tmpLongInt := SomeOtherLongint
else
  tmpLongInt := SomeString;
SomeLongint := tmpLongInt;

===code end ===

Regards,
Sven

Reply by email

Ben Grasset
to Sven Barth

On Wed, Nov 6, 2019 at 2:01 AM Sven Barth via fpc-pascal <

wrote: A normal if-statements has the same non-evaluation.

Not in the way I meant, though.

Like, I thought the difference between "normal if" and "ternary if" was supposed to be the same as the difference between the existing IfThen() function and the intrinsic version of IfThen(), on which you based the "if-then-else" syntax I think.

By which I mean, something similar to the difference between "if" and "static if" in the D programming language (at least when given things that are possible to evaluate at compile time, like GetTypeKind is in Pascal). For example:

import std.stdio;
import std.traits;
// Would not compile, because everything is evaluated fully,
// and in this case the parameters we pass aren't
// compatible with all branches.
void PrintSomething(T)(T value) {
  if (isFloatingPoint!(T)) {
    writefln("Floating point value: %f", value * value);
  } else if (isIntegral!(T)) {
    writefln("Integral value: %d", value * value);
  } else if (isSomeString!(T)) {
    writefln("String value: %s", value ~ value);
  }
}

// Compiles fine, as only the relevant branch for
// a given parameter is evaluated.
void StaticPrintSomething(T)(T value) {
static if (isFloatingPoint!(T)) {

    writefln("Floating point value: %f", value * value);
  }
  else static if (isIntegral!(T)) {
    writefln("Integral value: %d", value * value);
  }
  else static if (isSomeString!(T)) {
    // "~" does string concatenation in D
    writefln("String value: %s", value ~ value ~ value);
  }

}

void main() {
  StaticPrintSomething(1);
  StaticPrintSomething(1.0);
  StaticPrintSomething("hey");
}

The point being that the type-checking is neither useful or necessary in scenarios where the branch being evaluated is statically known to be unreachable ahead of time.

Reply by email

Sven Barth
to Ben Grasset

Ben Grasset via fpc-pascal schrieb am
Mi., 6. Nov. 2019, 15:43:

Show quoted text 13 lines
On Wed, Nov 6, 2019 at 2:01 AM Sven Barth via fpc-pascal < > wrote: > A normal if-statements has the same non-evaluation. > Not in the way I meant, though. Like, I thought the difference between "normal if" and "ternary if" was supposed to be the same as the difference between the existing IfThen() function and the intrinsic version of IfThen(), on which you based the "if-then-else" syntax I think.

No. The difference between the currently provided IfThen generic is the same in regards to evaluation to the rejected IfThen intrinsic, an if-expression and an if-statement: All four typecheck their branches, but only the later three don't execute the statements in the branch that is not taken (and also have a branch optimised away if the condition is const). The only difference between the if-expression and the if-statement is that the former can be used as an expression (duh!).

The point being that the type-checking is neither useful or necessary in

scenarios where the branch being evaluated is statically known to be unreachable ahead of time.

Pascal has a strong type safety, thus something like the if-expression won't be used/allowed to weaken that.

If that means that some things can't be implemented in generics the "easy" way, then so be it.

Regards,
Sven

Reply by email

Ben Grasset
to Sven Barth

On Wed, Nov 6, 2019 at 12:44 PM Sven Barth via fpc-pascal <

Show quoted text 8 lines
wrote: Pascal has a strong type safety, thus something like the if-expression won't be used/allowed to weaken that. If that means that some things can't be implemented in generics the "easy" way, then so be it.

I agree that Pascal has strong type safety.

IMO conditional branching based on compile-time constant evaluation, that cannot fail, does not do anything resembling weakening it, however.

It strengthens it, and has no downsides, because it's verified by the compiler.

Encouraging typecasting (which cares only about the sizes of the types involved, nothing else) at the programmer level is far more error-prone in a variety of ways.

Reply by email

Ben Grasset
to Ben Grasset
PM Ben Grasset wrote
Encouraging typecasting (which cares only about the sizes of the types involved, nothing else) at the programmer level is far more error-prone in a variety of ways.

Also: it's slower in many cases, because it tends to involve "if" statements that *remain* as if statements in the final generated assembly code, whereas a static check would allow for simply generating *only* the code for the path that's actually taken.

Reply by email

Sven Barth
to Ben Grasset
Ben Grasset wrote 12 lines
On Wed, Nov 6, 2019 at 7:33 PM Ben Grasset < > wrote: Encouraging typecasting (which cares only about the sizes of the types involved, nothing else) at the programmer level is far more error-prone in a variety of ways. Also: it's slower in many cases, because it tends to involve "if" statements that *remain* as if statements in the final generated assembly code, whereas a static check would allow for simply generating *only* the code for the path that's actually taken.

This is not true. If the compiler can prove at compile time that the if-condition is constant then the branch that is not taken is removed at the node level. It doesn't even remotely reach the code generation phase. (I just checked that yesterday when I implemented IsManagedType() and wondered about missing "unreachable code" warnings).

Take a look at the test for IsManagedType():

=== code begin ===

program tismngd1;
{$mode objfpc}
{$modeswitch advancedrecords}
uses
   TypInfo;
var
   gError: LongInt = 0;
function NextErrorCode: LongInt; inline;
begin
   Inc(gError);
   Result := gError;
end;
generic procedure TestType<T>(aIsMngd: Boolean); inline;
begin
   if IsManagedType(T)  aIsMngd then begin
     Writeln('IsManagedType(', PTypeInfo(TypeInfo(T))^.Name, ') failure;
expected: ', aIsMngd, ', got: ', IsManagedType(T));
     Halt(NextErrorCode);
   end;
   NextErrorCode;
end;
type
   TTestLongInt = record
     a: LongInt;
   end;
   TTestAnsiString = record
     a: AnsiString;
   end;
   TTestManaged = record
     a: LongInt;
     class operator Initialize(var aTestManaged: TTestManaged);
   end;
   TTestObj = object
     a: LongInt;
   end;
   TTestObjAnsiString = object
     a: AnsiString;
   end;
class operator TTestManaged.Initialize(var aTestManaged: TTestManaged);
begin
   aTestManaged.a := 42;
end;
type
   TProcVar = procedure;
   TMethodVar = procedure of object;
   TDynArrayLongInt = array of LongInt;
   TStaticArrayLongInt = array[0..4] of LongInt;
   TStaticArrayAnsiString = array[0..4] of AnsiString;
   TEnum = (eOne, eTwo, eThree);
   TSet = set of (sOne, sTwo, sThree);
begin
   specialize TestType<LongInt>(False);
   specialize TestType<Boolean>(False);
   specialize TestType<ShortString>(False);
   specialize TestType<AnsiString>(True);
   specialize TestType<UnicodeString>(True);
   specialize TestType<WideString>(True);
   specialize TestType<Single>(False);
   specialize TestType<TProcVar>(False);
   specialize TestType<TMethodVar>(False);
   specialize TestType<Pointer>(False);
   specialize TestType<IInterface>(True);
   specialize TestType<TObject>(False);
   specialize TestType<TTestLongInt>(False);
   specialize TestType<TTestAnsiString>(True);
   specialize TestType<TTestManaged>(True);
   specialize TestType<TTestObj>(False);
   specialize TestType<TTestObjAnsiString>(True);
   specialize TestType<TDynArrayLongInt>(True);
   specialize TestType<TStaticArrayLongInt>(False);
   specialize TestType<TStaticArrayAnsiString>(True);
   specialize TestType<TEnum>(False);
   specialize TestType<TSet>(False);
   Writeln('Ok');
end.

=== code end ===

Thanks to the node level optimization I mentioned the assembly code of the main function will look like this (in this case x86_64-win64):

=== code begin ===

.section .text.n_main,"ax"
    .balign 16,0x90
.globl    main
main:
.globl    PASCALMAIN
PASCALMAIN:
.Lc122:
.Lc123:
# Temps allocated between rbp-8 and rbp+0
.seh_proc main
# [62] begin
    pushq    %rbp
.seh_pushreg %rbp
.Lc124:
.Lc125:
    movq    %rsp,%rbp
.Lc126:
    leaq    -48(%rsp),%rsp
.seh_stackalloc 48
    movq    %rbx,-8(%rbp)
.seh_savereg %rbx, 40
.seh_endprologue
    call    fpc_initializeunits
# [63] specialize TestType<LongInt>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [64] specialize TestType<Boolean>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [65] specialize TestType<ShortString>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [66] specialize TestType<AnsiString>(True);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [67] specialize TestType<UnicodeString>(True);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [68] specialize TestType<WideString>(True);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [69] specialize TestType<Single>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [70] specialize TestType<TProcVar>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [71] specialize TestType<TMethodVar>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [72] specialize TestType<Pointer>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [73] specialize TestType<IInterface>(True);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [74] specialize TestType<TObject>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [75] specialize TestType<TTestLongInt>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [76] specialize TestType<TTestAnsiString>(True);     addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [77] specialize TestType<TTestManaged>(True);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [78] specialize TestType<TTestObj>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [79] specialize TestType<TTestObjAnsiString>(True);     addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [80] specialize TestType<TDynArrayLongInt>(True);     addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [81] specialize TestType<TStaticArrayLongInt>(False);     addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [82] specialize TestType<TStaticArrayAnsiString>(True);     addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [83] specialize TestType<TEnum>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [84] specialize TestType<TSet>(False);
    addl    $1,TC_$P$TISMNGD1_$$_GERROR(%rip)     movl    TC_$P$TISMNGD1_$$_GERROR(%rip),%eax # [85] Writeln('Ok');
    call    fpc_get_output
    movq    %rax,%rbx
    leaq    _$TISMNGD1$_Ld3(%rip),%r8
    movq    %rbx,%rdx
    movl    $0,%ecx
    call    fpc_write_text_shortstr
    call    fpc_iocheck
    movq    %rbx,%rcx
    call    fpc_writeln_end
    call    fpc_iocheck
# [86] end.
    call    fpc_do_exit
    movq    -8(%rbp),%rbx
    leaq    (%rbp),%rsp
    popq    %rbp
    ret
.seh_endproc
.Lc121:

=== code end ===

As you can see the compiler basically optimized everything except the increment of the gError variable away, because it determined it can do so. And this was *without* any explicit optimizations enabled.

Regards,
Sven

Reply by email

Sven Barth
to Ben Grasset: On Wed, Nov 6, 2019 at 12:44 PM Sven Barth via fpc-pascal < I agree that Pascal has…
Ben Grasset wrote 22 lines
On Wed, Nov 6, 2019 at 12:44 PM Sven Barth via fpc-pascal < > wrote: Pascal has a strong type safety, thus something like the if-expression won't be used/allowed to weaken that. If that means that some things can't be implemented in generics the "easy" way, then so be it. I agree that Pascal has strong type safety. IMO conditional branching based on compile-time constant evaluation, that cannot fail, does not do anything resembling weakening it, however. It strengthens it, and has no downsides, because it's verified by the compiler. Encouraging typecasting (which cares only about the sizes of the types involved, nothing else) at the programmer level is far more error-prone in a variety of ways.

If there is no type checking, then it is *not* verified by the compiler. If I have an if-expression with an always true if-clause I could write any syntactically correct garbage in the else-clause if type checking would be disabled, because the compiler would not verify it. And this is not how Pascal works. And also not how FPC's parser works. We're taking huge care that generics are correctly type checked and that as many errors as possible are caught when writing the generic (instead of when specializing), so we're not going to introduce something like this. The if-expression is intended to be like C/C++'s ternary operator nothing more, nothing less cause that is what most people want to use it for.

Regards,
Sven

Reply by email

Ben Grasset
to Sven Barth

On Thu, Nov 7, 2019 at 3:03 AM Sven Barth via fpc-pascal <

wrote: If there is no type checking, then it is *not* verified by the compiler.

Perhaps "no type checking" was not the write way to put it. A better way to describe it might be:

Since the compiler *always* knows exactly which type a generic type constraint currently amounts to, it is clearly capable of properly choosing specific code paths based on that, if given some kind of constant-evaluatable boolean condition that is specifically based on TTypeKind.

In general, it's *exactly* the same concept as something like using SizeOf in an {$IF} / {$ELSEIF} directive pair (where the compiler does indeed completely ignore everything in the "false" section):

program Example;
begin
  // You can in fact put stuff that isn't even close to valid code in
whichever block is false for you, below.
  {$IF SizeOf(Pointer) = 4}
    WriteLn('The size of a pointer is 4 bytes.');
  {$ELSEIF SizeOf(Pointer) = 8}
    WriteLn('The size of a pointer is 8 bytes.');
  {$ENDIF}
end.

Not that it has to do precisely the same thing for a hypothetical TTypeKind-based choice.

E.G. it could still do the full checking, as the only thing that really matters is that it does not actively raise error messages only relevant for Integer under code blocks only entered for tkFloat.

Reply by email

Michael Van Canneyt
to Ben Grasset
Ben Grasset wrote 9 lines
On Thu, Nov 7, 2019 at 3:03 AM Sven Barth via fpc-pascal < > wrote: > If there is no type checking, then it is *not* verified by the compiler. > Perhaps "no type checking" was not the write way to put it. A better way to describe it might be:

[snip]

E.G. it could still do the full checking, as the only thing that really matters is that it does not actively raise error messages only relevant for Integer under code blocks only entered for tkFloat.

If I understood Sven's example correct, then the compiler does exactly this already.

Michael.

Reply by email

Sven Barth
to Michael Van Canneyt

Michael Van Canneyt schrieb am Do., 7. Nov. 2019, 16:23:

Show quoted text 25 lines
On Thu, 7 Nov 2019, Ben Grasset via fpc-pascal wrote: > On Thu, Nov 7, 2019 at 3:03 AM Sven Barth via fpc-pascal < > > wrote: > >> If there is no type checking, then it is *not* verified by the compiler. >> > > Perhaps "no type checking" was not the write way to put it. A better way to > describe it might be: > [snip] > E.G. it could still do the full checking, as the only thing that really > matters is that it does not actively raise error messages only relevant for > Integer under code blocks only entered for tkFloat. If I understood Sven's example correct, then the compiler does exactly this already.

Not quite. If the generic parameter T is a LongInt, then the branch for tkFloat *must* also be valid for LongInts. Because the type checking is done *before* the branch is discarded.

Regards,
Sven

Reply by email

Ben Grasset
to Michael Van Canneyt: [snip] If I understood Sven's example correct, then the compiler does exactly this…
AM Michael Van Canneyt wrote
If I understood Sven's example correct, then the compiler does exactly this already.

It does in the sense of *code generation* for things that are specifically compiler intrinsics (which I was aware of), but not in a sense that makes the issue Ryan was posting about here avoidable. Maybe this will explain what I'm trying to get at a bit better:

program Example;
{$mode ObjFPC}
// Without actually specializing and using GDiv,
// this program compiles fine.
generic function GDiv<T>(const A, B: T): T; inline;
begin
  if GetTypeKind(T) in [tkInteger, tkInt64, tkQWord] then
    Result := A div B
  else if GetTypeKind(T) = tkFloat then
    Result := A / B
  else
    Result := Default(T);
end;
// However, once we do specialize it...
procedure UseGDiv;
begin
  // Example.pas(13,17) Error: Incompatible types: got "Double" expected
"Int64"
  WriteLn(specialize GDiv<Int64>(1, 2));
  // Example.pas(11,17) Error: Operator is not overloaded: "Double" div
"Double"
  WriteLn(specialize GDiv<Double>(1, 2));
end;
// Note: I'm fully aware that having the *normal* if-statement work in such
a way that the
// the above would compile is neither feasible or a good idea. However,
imagine something
// like the following hypothetical version of GDiv, which would rely on the
conditional
// compilation aspect of the scanner to be aware of more than it currently
is, and also
// for the conditional compilation syntax to be somewhat more advanced:
generic function GDiv<T>(const A, B: T): T; inline;
begin
  // Here, we're doing precisely the same kind of thing that is currently
  // possible with SizeOf, Declared, Defined, and so on.
  {$IF GetTypeKind(T) in [tkInteger, tkInt64, tkQWord]}
    Result := A div B
  {$ELSEIF GetTypeKind(T) = tkFloat}
    Result := A / B
  {$ELSE}
    Result := Default(T);
  {$ENDIF}
end;
begin
end.

Of course, implementing that kind of thing in the scanner would likely be far more difficult than implementing it as something that happens in "normal" code, via some syntax or perhaps intrinsic that makes it distinctively a fully-statically-considered conditional, as opposed to a partially-statically considered one as is the case with normal "if".

Reply by email

Michael Van Canneyt
to Ben Grasset
Ben Grasset wrote 40 lines
On Thu, Nov 7, 2019 at 10:23 AM Michael Van Canneyt wrote: > If I understood Sven's example correct, then the compiler does exactly this > already. > It does in the sense of *code generation* for things that are specifically compiler intrinsics (which I was aware of), but not in a sense that makes the issue Ryan was posting about here avoidable. Maybe this will explain what I'm trying to get at a bit better: program Example; {$mode ObjFPC} // Without actually specializing and using GDiv, // this program compiles fine. generic function GDiv<T>(const A, B: T): T; inline; begin if GetTypeKind(T) in [tkInteger, tkInt64, tkQWord] then Result := A div B else if GetTypeKind(T) = tkFloat then Result := A / B else Result := Default(T); end; // However, once we do specialize it... procedure UseGDiv; begin // Example.pas(13,17) Error: Incompatible types: got "Double" expected "Int64" WriteLn(specialize GDiv<Int64>(1, 2)); // Example.pas(11,17) Error: Operator is not overloaded: "Double" div "Double" WriteLn(specialize GDiv<Double>(1, 2)); end;

Thanks for the explanation. All is clear.

As an aside:
In my opinion (keep in mind I am not a big fan of generics) the above code would be a prime example where one should not be using generics, but simple overloads. If you need to use GetTypeKind in a generic, I think you're on the wrong path. Use of IsManagedType() in a generic is stretching it, but GetTypeKind() is over the line.

Michael.

Reply by email

Ben Grasset
to Michael Van Canneyt
AM Michael Van Canneyt wrote 9 lines
As an aside: In my opinion (keep in mind I am not a big fan of generics) the above code would be a prime example where one should not be using generics, but simple overloads. If you need to use GetTypeKind in a generic, I think you're on the wrong path. Use of IsManagedType() in a generic is stretching it, but GetTypeKind() is over the line.

I agree to an extent that it's not a *huge* problem and that there are workarounds in many cases, however there's definitely certain things that simply cannot be achieved in an equally performant way via any alternative such as normal overloading.

That said, Ryan's currently-in-limbo patch for constant generic parameters actually itself in theory already provides proper solutions for quite a few aspects of the general problem, so even as it stands right now we're at least making progress towards improving the situation I'd say.

For example, I recently did a translation of the fairly well-known "PDQSort" algorithm from C++ to Pascal:

https://github.com/Akira13641/PasPDQSort

The original C++ version used "constexpr" template parameters to determine at compile time whether or not the user-provided comparison function was one for which it should use the "branchless" version of its code for item partitioning or not, and based on that passed the boolean "true" or "false" result as another constant template parameter.

The end result of course being that the compiled code is always tuned precisely to the given comparison function, without any kind of runtime selection based on non-constant boolean function parameters (which is what I've currently had to write the "branchless" choice as, defaulting to "true" unless the user specifies otherwise.)

With Ryan's patch, I'll at least be able to write that choice as a constant parameter instead, although I'm unaware of any way to replicate actually having the compiler *make* the choice.

So perhaps what's needed is not even necessarily something like "static if" support, but rather just more compiler intrinsics along the lines of GetTypeKind / IsManagedType that evaluate different things and return constant values, which in combination with Ryan's patch will go a long way as far as increasing the granularity of control available to users of FPC as far as this kind of stuff.

Reply by email

Jonas Maebe
to Ben Grasset
Ben Grasset wrote 15 lines
The original C++ version used "constexpr" template parameters to determine at compile time whether or not the user-provided comparison function was one for which it should use the "branchless" version of its code for item partitioning or not, and based on that passed the boolean "true" or "false" result as another constant template parameter. The end result of course being that the compiled code is always tuned precisely to the given comparison function, without any kind of runtime selection based on non-constant boolean function parameters (which is what I've currently had to write the "branchless" choice as, defaulting to "true" unless the user specifies otherwise.) With Ryan's patch, I'll at least be able to write that choice as a constant parameter instead, although I'm unaware of any way to replicate actually having the compiler *make* the choice.

You can't. It's the main difference between C++ templates, which is a Turing-complete programming language, and generics, which is simply a parametrising mechanic.

Jonas

Reply by email

Ben Grasset
to Jonas Maebe
AM Jonas Maebe wrote
You can't. It's the main difference between C++ templates, which is a Turing-complete programming language, and generics, which is simply a parametrising mechanic.

I know what you mean, and I'm aware, but you actually kind of can to the fairly straightforward extent that I'm concerned about with Ryan's patch (i.e. simply passing constant results as constraints rather than function parameters.)

Reply by email

Jonas Maebe
to Ben Grasset
Ben Grasset wrote 11 lines
On Fri, Nov 8, 2019 at 11:04 AM Jonas Maebe < > wrote: You can't. It's the main difference between C++ templates, which is a Turing-complete programming language, and generics, which is simply a parametrising mechanic. I know what you mean, and I'm aware, but you actually kind of can to the fairly straightforward extent that I'm concerned about with Ryan's patch (i.e. simply passing constant results as constraints rather than function parameters.) 

It does explain why templates can contain invalid C++: it's because the invalid C++ gets discarded before it is (semantically) presented to the C++ parser. This does not work with generics, because generics are Pascal themselves.

Jonas

Reply by email

Ben Grasset
to Ben Grasset: I know what you mean, and I'm aware, but you actually kind of can to the fairly…
AM Ben Grasset wrote 5 lines
I know what you mean, and I'm aware, but you actually kind of can to the fairly straightforward extent that I'm concerned about with Ryan's patch (i.e. simply passing constant results as constraints rather than function parameters.)

Also, if you were referring specifically to the "making the choice part", looking at the PDQSort C++ code again, all it was actually doing is evaluating the *types* involved, not the comparison function itself. Which would be fairly straightforward to replicate in FPC using something like a constant TTypeKind constraint.

Reply by email

Sven Barth
to Ryan Joseph: I've wanted to make a generic version of a vector for a while but I always give up…
Ryan Joseph wrote 39 lines
I've wanted to make a generic version of a vector for a while but I always give up because it seems not very possible. It's probably not even a great idea because many methods don't translate between float and integer but I wanted to prevent other code duplication if possible. Here's an example of how things break down. Are there any solutions for this currently? I feel like generics need to support some compiler directives so different blocks of code can specialize different depending on the type. {$mode objfpc} {$modeswitch advancedrecords} program generic_vector_2; uses Math; type generic TVec2<TScalar> = record x, y: TScalar; function Normalize: TVec2; end; TVec2f = specialize TVec2<Float>; TVec2i = specialize TVec2<Integer>; function TVec2.Normalize: TVec2; var fac: TScalar; begin // Can't determine which overloaded function to call // Incompatible types: got "Extended" expected "LongInt" fac:=Sqrt(Sqr(x) + Sqr(y)); if fac0.0 then begin // Incompatible types: got "Single" expected "LongInt" fac:=1.0/fac; result.x:=x*fac; result.y:=y*fac; end else begin result.x:=0; result.y:=0; end; end; begin end.

First of Sqrt always returns a ValReal (aka the best precision floating point), thus you should declare fac as ValReal. Thus you should cast x and y to ValReal before passing them to Sqr to avoid overload troubles.

Then you only need to ensure that the adjusted vector components are passed correctly to the result which will lead to code like this:

=== code begin ===

program tgenvec;
{$mode objfpc}
{$modeswitch advancedrecords}
uses
   Math;
type
   generic TVec2<TScalar> = record
   private type
     PScalar = ^TScalar;
   public
     x, y: TScalar;
     function Normalize: TVec2;
   end;
function TVec2.Normalize: TVec2;
var
   fac, tmpx, tmpy: ValReal;
begin
   fac := Sqrt(Sqr(ValReal(x)) + Sqr(ValReal(y)));
   if fac  0.0 then begin
     fac := 1.0 / fac;
     tmpx := x * fac;
     tmpy := y * fac;
     if GetTypeKind(TScalar) in [tkInteger, tkInt64, tkQWord] then begin
       Result.x := Round(tmpx);
       Result.y := Round(tmpy);
     end else if GetTypeKind(TScalar) = tkFloat then begin
       Result.x := PScalar(@tmpx)^;
       Result.y := PScalar(@tmpy)^;
     end;
   end else
     Result := Default(TVec2);
end;
type
   TVec2f = specialize TVec2<Double>;
   TVec2i = specialize TVec2<LongInt>;
var
   vf: TVec2f;
   vi: TVec2i;
begin
   vf.x := 2.5;
   vf.y := 3.5;
   vf := vf.Normalize;
   Writeln(vf.x, ' ', vf.y);
   // on Win64 this prints 5.8123819371909646E-001 8.1373347120673500E-001
   vi.x := 2;
   vi.y := 4;
   vi := vi.Normalize;
   Writeln(vi.x, ' ', vi.y);
   // on Win64 this prints 0 1
end.

=== code end ===

The path not taken for the GetTypeKind inside the TVec.Normalize will be optimized away. The pointer conversion is needed, because a floating point type can not be assigned to an integer type. Sadly the compiler does not realize that it does not really need to take the address there, so that will stay even in O4 for the non-floating point case. But hey, you've got a generic Normalize then...

Regards,
Sven

Reply by email

Ryan Joseph
to Ben Grasset: On Thu, Nov 7, 2019 at 10:23 AM Michael Van Canneyt It does in the sense of *code…
Show quoted text 9 lines
On Nov 7, 2019, at 12:28 PM, Ben Grasset via fpc-pascal wrote: {$IF GetTypeKind(T) in [tkInteger, tkInt64, tkQWord]} Result := A div B {$ELSEIF GetTypeKind(T) = tkFloat} Result := A / B {$ELSE} Result := Default(T); {$ENDIF}

This is what I hinted in my post. Has anything like this been considered before? It seems necessary to me but it looks like Sven had some fancy work around using pointers. Either way the compile time directive would be faster and avoid the conditional statements. Regards,
Ryan Joseph

Reply by email

Sven Barth
to Ryan Joseph

Ryan Joseph via fpc-pascal schrieb am
So., 10. Nov. 2019, 02:11:

Show quoted text 18 lines
> On Nov 7, 2019, at 12:28 PM, Ben Grasset via fpc-pascal < > wrote: > > {$IF GetTypeKind(T) in [tkInteger, tkInt64, tkQWord]} > Result := A div B > {$ELSEIF GetTypeKind(T) = tkFloat} > Result := A / B > {$ELSE} > Result := Default(T); > {$ENDIF} This is what I hinted in my post. Has anything like this been considered before? It seems necessary to me but it looks like Sven had some fancy work around using pointers. Either way the compile time directive would be faster and avoid the conditional statements.

The token recorder does not support preprocessor conditions. Not to mention that this mixes preprocessor and parser stuff. Generics are part of the parser.

Regards,
Sven

Reply by email