Recent

Author Topic: nested helpers - a call for help  (Read 6242 times)

avra

  • Hero Member
  • *****
  • Posts: 2514
    • Additional info
nested helpers - a call for help
« on: August 22, 2018, 01:38:30 pm »
I am looking for help with nested helpers usage problem.

Let's say that I implement helpers which can get/set bytes from a word type, and bytes and words from a longword type. Then I have a problem that nested type helpers change only copy of the initial variable and not the variable it self. Let me explain that with a simple example:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.Button1Click(Sender: TObject);
  2. var
  3.   MyLongword: longword;
  4. begin
  5.   MyLongword := 0;                   // MyLongword equals 0
  6.   MyLongword.Word[0] := 250;         // MyLongword equals 250
  7.   MyLongword.Word[1].Byte[0] := 100; // MyLongword still equals 250 <<< PROBLEM!!! This DOES NOT set a byte in MyLongword and helper users might be confused!!!
  8.   MyLongword.Byte[1] := 4;           // MyLongword equals 1274 (250 + 2^(8 + 2), 2^2 = 4)
  9.   Memo1.Append(IntToStr(MyLongword));
  10. end;

This behavior is fine with me and I can live with that, but I see it as a confusion and obstacle for many users. I am aware why code in line 6 changes MyLongword and why code in line 7 doesn't, but I am looking for ways to either make line 7 possible or to forbid users to use such nested/propagated helpers. Since at this moment I do not see any solution, I am calling for help and out of the box thinking in hope to get some fresh ideas.

If you would like to take a deeper look, you just need to create a new Lazarus project, create a button and a memo, copy/paste Unit1 code, and attach Button1Click event to a button. I have also attached the complete project if that is more convenient for you.

Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4. {$modeswitch typehelpers}
  5.  
  6. interface
  7.  
  8. uses
  9.   Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;
  10.  
  11. type
  12.  
  13.   TWordBytes = 0..1;
  14.  
  15.   TWordOverlay = bitpacked record case integer of     // for fast extraction of bytes
  16.     1: (AsByte: array[TWordBytes] of byte);
  17.     2: (AsWord: word);
  18.   end;
  19.  
  20.   TWordBitHelper = type helper(TWordHelper) for Word
  21.   public
  22.     function  GetByte(const aIndex: TWordBytes): byte;
  23.     procedure SetByte(const aIndex: TWordBytes; const NewValue: byte);
  24.     property  Byte[aIndex: TWordBytes]: byte read GetByte write SetByte;
  25.   end;
  26.  
  27.   TLongwordBytes = 0..3;
  28.   TLongwordWords = 0..1;
  29.  
  30.   TLongwordOverlay = bitpacked record case integer of     // for fast extraction of words and bytes
  31.     1: (AsByte:     array[TLongwordBytes] of byte);
  32.     2: (AsWord:     array[TLongwordWords] of word);
  33.     3: (AsLongword: longword);
  34.   end;
  35.  
  36.   TLongwordBitHelper = type helper(TCardinalHelper) for cardinal
  37.   public
  38.     function  GetByte(const aIndex: TLongwordBytes): byte;
  39.     procedure SetByte(const aIndex: TLongwordBytes; const NewValue: byte);
  40.     property  Byte[aIndex: TLongwordBytes]: byte read GetByte write SetByte;
  41.     function  GetWord(const aIndex: TLongwordWords): word;
  42.     procedure SetWord(const aIndex: TLongwordWords; const NewValue: word);
  43.     property  Word[aIndex: TLongwordWords]: word read GetWord write SetWord;
  44.   end;
  45.  
  46.  
  47.   { TForm1 }
  48.  
  49.   TForm1 = class(TForm)
  50.     Button1: TButton;
  51.     Memo1: TMemo;
  52.     procedure Button1Click(Sender: TObject);
  53.   private
  54.  
  55.   public
  56.  
  57.   end;
  58.  
  59. var
  60.   Form1: TForm1;
  61.  
  62. implementation
  63.  
  64. {$R *.lfm}
  65.  
  66. { TForm1 }
  67.  
  68. procedure TForm1.Button1Click(Sender: TObject);
  69. var
  70.   MyLongword: longword;
  71. begin
  72.   MyLongword := 0;                   // MyLongword equals 0
  73.   MyLongword.Word[0] := 250;         // MyLongword equals 250
  74.   MyLongword.Word[1].Byte[0] := 100; // MyLongword equals 250 (Beware!!! This DOES NOT set a byte in MyLongword !!!)
  75.   MyLongword.Byte[1] := 4;           // MyLongword equals 1274 (250 + 2^(8 + 2), 2^2 = 4)
  76.   Memo1.Append(IntToStr(MyLongword));
  77.   Memo1.Append('');
  78. end;
  79.  
  80. function TWordBitHelper.GetByte(const aIndex: TWordBytes): byte;
  81. begin
  82.   Result := TWordOverlay(Self).AsByte[aIndex];
  83. end;
  84.  
  85. procedure TWordBitHelper.SetByte(const aIndex: TWordBytes; const NewValue: byte);
  86. begin
  87.   TWordOverlay(Self).AsByte[aIndex] := NewValue;
  88. end;
  89.  
  90. function TLongwordBitHelper.GetByte(const aIndex: TLongwordBytes): byte;
  91. begin
  92.   Result := TLongwordOverlay(Self).AsByte[aIndex];
  93. end;
  94.  
  95. procedure TLongwordBitHelper.SetByte(const aIndex: TLongwordBytes; const NewValue: byte);
  96. begin
  97.   TLongwordOverlay(Self).AsByte[aIndex] := NewValue;
  98. end;
  99.  
  100. function TLongwordBitHelper.GetWord(const aIndex: TLongwordWords): word;
  101. begin
  102.   Result := TLongwordOverlay(Self).AsWord[aIndex]
  103. end;
  104.  
  105. procedure TLongwordBitHelper.SetWord(const aIndex: TLongwordWords; const NewValue: word);
  106. begin
  107.   TLongwordOverlay(Self).AsWord[aIndex] := NewValue;
  108. end;
  109.  
  110. end.
  111.  

Thanks!
ct2laz - Conversion between Lazarus and CodeTyphon
bithelpers - Bit manipulation for standard types
pasettimino - Siemens S7 PLC lib

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9791
  • Debugger - SynEdit - and more
    • wiki
Re: nested helpers - a call for help
« Reply #1 on: August 22, 2018, 02:40:15 pm »
return a different type
Code: Pascal  [Select][+][-]
  1. type
  2.   TMyHelperWord = type word;
  3.  
  4.   TLongwordBitHelper = type helper(TCardinalHelper) for cardinal
  5.   public
  6. ...
  7.     property  Word[aIndex: TLongwordWords]: TMyHelperWord read GetWord write SetWord;
  8.   end;
  9.  
  10.  

The TWordBitHelper will no longer work on the result.

avra

  • Hero Member
  • *****
  • Posts: 2514
    • Additional info
Re: nested helpers - a call for help
« Reply #2 on: August 22, 2018, 03:36:48 pm »
return a different type
...
The TWordBitHelper will no longer work on the result.
I have tried exactly as you typed and it doesn't work like that on my Laz 1.8.5 + FPC 3.0.5. If I understood well your reply, Line 7 shouldn't compile any more after this change - but it does. I have also tried to put TMyHelperWord into proper places in getter method, but the result was the same. Maybe I misunderstood something?
ct2laz - Conversion between Lazarus and CodeTyphon
bithelpers - Bit manipulation for standard types
pasettimino - Siemens S7 PLC lib

avra

  • Hero Member
  • *****
  • Posts: 2514
    • Additional info
Re: nested helpers - a call for help
« Reply #3 on: August 27, 2018, 09:18:35 am »
ping   :-[
ct2laz - Conversion between Lazarus and CodeTyphon
bithelpers - Bit manipulation for standard types
pasettimino - Siemens S7 PLC lib

Thaddy

  • Hero Member
  • *****
  • Posts: 14197
  • Probably until I exterminate Putin.
Re: nested helpers - a call for help
« Reply #4 on: August 27, 2018, 12:28:03 pm »
Point in case is: for the setter to persist it needs the pointer-to-value, not a copy of the value as in your case.
Maybe it can be solved with pointers and pointermath.
Specialize a type, not a var.

ASerge

  • Hero Member
  • *****
  • Posts: 2222
Re: nested helpers - a call for help
« Reply #5 on: August 27, 2018, 07:27:28 pm »
The TWordBitHelper will no longer work on the result.
I have tried exactly as you typed and it doesn't work like that on my Laz 1.8.5 + FPC 3.0.5. If I understood well your reply, Line 7 shouldn't compile any more after this change - but it does. I have also tried to put TMyHelperWord into proper places in getter method, but the result was the same. Maybe I misunderstood something?
This code is not compiled (on line 85) when you uncomment {type} on line 25:
Code: Pascal  [Select][+][-]
  1. {$APPTYPE CONSOLE}
  2. {$IFDEF FPC}{$MODE OBJFPC}{$MODESWITCH TYPEHELPERS}{$ENDIF}
  3.  
  4. uses SysUtils;
  5.  
  6. type
  7.   TWordBitHelper = {$IFDEF FPC}type{$ELSE}record{$ENDIF} helper for Word
  8.   strict private
  9.   type
  10.     TWordBytes = 0..SizeOf(Word) - 1;
  11.     TWordOverlay = packed record
  12.       case Integer of
  13.         1: (AsByte: array[TWordBytes] of Byte);
  14.         2: (AsWord: Word);
  15.     end;
  16.     function GetByte(const AIndex: TWordBytes): Byte;
  17.     procedure SetByte(const AIndex: TWordBytes; const NewValue: Byte);
  18.   public
  19.     property Byte[const AIndex: TWordBytes]: Byte read GetByte write SetByte;
  20.   end;
  21.  
  22.   TLongwordBitHelper = {$IFDEF FPC}type{$ELSE}record{$ENDIF} helper for LongWord
  23.   strict private
  24.   type
  25.     TStrictWord = {type }Word;
  26.     TLongwordBytes = 0..SizeOf(LongWord) - 1;
  27.     TLongwordWords = 0..SizeOf(LongWord) div SizeOf(Word) - 1;
  28.     TLongwordOverlay = packed record
  29.       case Integer of
  30.         1: (AsByte: array[TLongwordBytes] of Byte);
  31.         2: (AsWord: array[TLongwordWords] of Word);
  32.         3: (AsLongword: LongWord);
  33.     end;
  34.     function GetByte(const AIndex: TLongwordBytes): Byte;
  35.     function GetWord(const AIndex: TLongwordWords): TStrictWord;
  36.     procedure SetByte(const AIndex: TLongwordBytes; const NewValue: Byte);
  37.     procedure SetWord(const AIndex: TLongwordWords; const NewValue: TStrictWord);
  38.   public
  39.     property Byte[const AIndex: TLongwordBytes]: Byte read GetByte write SetByte;
  40.     property Word[const AIndex: TLongwordWords]: TStrictWord read GetWord write SetWord;
  41.   end;
  42.  
  43.  
  44. { TWordBitHelper }
  45.  
  46. function TWordBitHelper.GetByte(const AIndex: TWordBytes): Byte;
  47. begin
  48.   Result := TWordOverlay(Self).AsByte[AIndex];
  49. end;
  50.  
  51. procedure TWordBitHelper.SetByte(const AIndex: TWordBytes; const NewValue: Byte);
  52. begin
  53.   TWordOverlay(Self).AsByte[AIndex] := NewValue;
  54. end;
  55.  
  56. { TLongwordBitHelper }
  57.  
  58. function TLongwordBitHelper.GetByte(const AIndex: TLongwordBytes): Byte;
  59. begin
  60.   Result := TLongwordOverlay(Self).AsByte[AIndex];
  61. end;
  62.  
  63. function TLongwordBitHelper.GetWord(const AIndex: TLongwordWords): TStrictWord;
  64. begin
  65.   Result := TLongwordOverlay(Self).AsWord[AIndex]
  66. end;
  67.  
  68. procedure TLongwordBitHelper.SetByte(const AIndex: TLongwordBytes;
  69.   const NewValue: Byte);
  70. begin
  71.   TLongwordOverlay(Self).AsByte[AIndex] := NewValue;
  72. end;
  73.  
  74. procedure TLongwordBitHelper.SetWord(const AIndex: TLongwordWords;
  75.   const NewValue: TStrictWord);
  76. begin
  77.   TLongwordOverlay(Self).AsWord[AIndex] := NewValue;
  78. end;
  79.  
  80. var
  81.   MyLongword: LongWord = 0;
  82. begin
  83.   MyLongword.Word[0] := 250;
  84.   Writeln(MyLongword, ', expected 250');
  85.   MyLongword.Word[1].Byte[0] := 100;
  86.   Writeln(MyLongword, ', expected 100 shl 16 or 250 = 6553850');
  87.   MyLongword.Byte[1] := 4;
  88.   Writeln(MyLongword, ', expected 250 + 4 shl 8 = 1274');
  89.   Readln;
  90. end.

In Delphi all the same (this code is Delphi compatible).

Thaddy

  • Hero Member
  • *****
  • Posts: 14197
  • Probably until I exterminate Putin.
Re: nested helpers - a call for help
« Reply #6 on: August 27, 2018, 07:36:15 pm »
@ASerge:
The output is still wrong. It is still 1274. What he means is that 250 should have changed into 100.
That is not the case, because of copy semantics.

That code compiles, doesn't mean code does what you want.
We had code compiling:
Here's what I did so far, never mind the pointers, I need that later for a wordptr property to expose the true word, if at all possible with type helpers.
It outputs what you did and AVRA himself did..Still not what is wanted...
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$H+}
  2. {$modeswitch typehelpers}{$pointermath on}
  3. uses
  4.   SysUtils;
  5.  
  6. type
  7.   TWordBytes = 0..1;
  8.   TLongwordBytes = 0..3;
  9.   TLongwordWords = 0..1;
  10.  
  11.   TLongwordBitHelper = type helper(TCardinalHelper) for cardinal
  12.   type
  13.     TWordBitHelper = type helper(TWordHelper) for Word
  14.       public
  15.       function  GetByte(const aIndex: TWordBytes): byte;
  16.       procedure SetByte(const aIndex: TWordBytes; const NewValue: byte);
  17.       property  Byte[aIndex: TWordBytes]: byte read GetByte write SetByte;
  18.     end;
  19.   public
  20.     function  GetByte(const aIndex: TLongwordBytes): byte;
  21.     procedure SetByte(const aIndex: TLongwordBytes; const NewValue: byte);
  22.     property  Byte[aIndex: TLongwordBytes]: byte read GetByte write SetByte;
  23.     function  GetWord(const aIndex: TLongwordWords): word;
  24.     procedure SetWord(const aIndex: TLongwordWords; const NewValue: word);
  25.     property  Word[aIndex: TLongwordWords]: word read GetWord write SetWord;
  26.    end;
  27.  
  28. function TLongwordBitHelper.TWordBitHelper.GetByte(const aIndex: TWordBytes): byte;
  29. begin
  30.   Result := PByte(PByte(@Self)+aIndex)^;
  31. end;
  32.  
  33. procedure TLongwordBitHelper.TWordBitHelper.SetByte(const aIndex: TWordBytes; const NewValue: byte);
  34. begin
  35.   PByte(PByte(@Self)+aIndex)^ := NewValue;
  36. end;
  37.  
  38. function TLongwordBitHelper.GetByte(const aIndex: TLongwordBytes): byte;
  39. begin
  40.   Result := PByte(PByte(@Self)+aIndex)^;
  41. end;
  42.  
  43. procedure TLongwordBitHelper.SetByte(const aIndex: TLongwordBytes; const NewValue: byte);
  44. begin
  45.   PByte(PByte(@Self)+aIndex)^ := NewValue;
  46. end;
  47.  
  48. function TLongwordBitHelper.GetWord(const aIndex: TLongwordWords): word;
  49. begin
  50.   Result := PWord(Pword(@Self)+aIndex)^;
  51. end;
  52.  
  53. procedure TLongwordBitHelper.SetWord(const aIndex: TLongwordWords; const NewValue: word);
  54. begin
  55.   PWord(Pword(@Self)+aIndex)^ := NewValue;
  56. end;
  57.  
  58. var
  59.   MyLongword: longword;
  60. begin
  61.   MyLongword := 0;                      // MyLongword equals 0
  62.   MyLongword.Word[0] := 250;            // MyLongword equals 250
  63.   MyLongword.Word[1].Byte[0] := 100;    // MyLongword equals 250 (Beware!!! This DOES NOT set a byte in MyLongword !!!)
  64.   MyLongword.Byte[1] := 4;              // MyLongword equals 1274 (250 + 2^(8 + 2), 2^2 = 4)
  65.   writeln(MyLongword);
  66. end.
  67. end.

Note that this code compiles too, is a lot cleaner, but still gives the wrong (as in undesired) answer....
See line 63. Only line 62 and 64 actually operate on the underlying longword.

Unless I mis-understood Avra.. that is...

I personally don't think we can succeed with type helpers, but at least the code is cleaned up  8-) O:-) :P
« Last Edit: August 27, 2018, 07:59:19 pm by Thaddy »
Specialize a type, not a var.

ASerge

  • Hero Member
  • *****
  • Posts: 2222
Re: nested helpers - a call for help
« Reply #7 on: August 27, 2018, 08:50:10 pm »
The output is still wrong. It is still 1274. What he means is that 250 should have changed into 100.
That is not the case, because of copy semantics.
That code compiles, doesn't mean code does what you want.
I just explain by example the idea of @Martin_fr to prevent incorrect use of such a construction. The code does not compile if you do what I said above. Because from @avra "...but I am looking for ways to either make line 7 possible or to forbid users to use such nested/propagated helpers..."
« Last Edit: August 27, 2018, 08:53:37 pm by ASerge »

Thaddy

  • Hero Member
  • *****
  • Posts: 14197
  • Probably until I exterminate Putin.
Re: nested helpers - a call for help
« Reply #8 on: August 27, 2018, 10:01:02 pm »
@ASerge
Yes, I understand that. Hard to find a way around it for (nested) simple types. If we could expose the pointer to the actual underlying data the problem can be solved.
As yet, I can't find a way how to do that with nested type helpers. Also, why has the nested type helper copy semantics while the outer helper doesn't?
You would expect it to use the actual underlying data.

Specialize a type, not a var.

soerensen3

  • Full Member
  • ***
  • Posts: 213
Re: nested helpers - a call for help
« Reply #9 on: August 28, 2018, 12:04:34 am »
@ASerge
Yes, I understand that. Hard to find a way around it for (nested) simple types. If we could expose the pointer to the actual underlying data the problem can be solved.
As yet, I can't find a way how to do that with nested type helpers. Also, why has the nested type helper copy semantics while the outer helper doesn't?
You would expect it to use the actual underlying data.
Maybe because first GetWord is called, that returns a new value and then on that value SetByte is called. Just a guess!

Maybe somebody who knows a little more assembler than I do can tell. If line 6 writes back the value to the variable then SetByte is called after that and the change is lost but I only getting half of the explanation of the assembler code:https://www.i8086.de/asm/8086-88-asm-lea.html so I might be wrong.
Code: Text  [Select][+][-]
  1. MyLongword.Word[1].Byte[0] := 100;
  2. 0000000000469E98 e893010000               callq  0x46a030 <GETWORD>
  3. 0000000000469E9D 66894584                 mov    %ax,-0x7c(%rbp)
  4. 0000000000469EA1 ba64000000               mov    $0x64,%edx
  5. 0000000000469EA6 31f6                     xor    %esi,%esi
  6. 0000000000469EA8 488d7d84                 lea    -0x7c(%rbp),%rdi
  7. 0000000000469EAC e8ef000000               callq  0x469fa0 <SETBYTE>
Lazarus 1.9 with FPC 3.0.4
Target: Manjaro Linux 64 Bit (4.9.68-1-MANJARO)

ASerge

  • Hero Member
  • *****
  • Posts: 2222
Re: nested helpers - a call for help
« Reply #10 on: August 28, 2018, 12:26:37 am »
Maybe because first GetWord is called, that returns a new value and then on that value SetByte is called. Just a guess!
That's not guess. This is normal behavior, fully consistent with the fpc language. Delphi behaves similarly.
What is MyLongword.Byte[1] := 4. It is TLongwordBitHelper(MyLongword).SetByte(1, 4).
What is MyLongword.Word[1].Byte[0] := 100. It is TLongwordBitHelper(MyLongword).GetWord(1).SetByte(0). The result and the variable are not related.

Thaddy

  • Hero Member
  • *****
  • Posts: 14197
  • Probably until I exterminate Putin.
Re: nested helpers - a call for help
« Reply #11 on: August 28, 2018, 07:10:36 am »
That's not guess. This is normal behavior, fully consistent with the fpc language. Delphi behaves similarly.
What is MyLongword.Byte[1] := 4. It is TLongwordBitHelper(MyLongword).SetByte(1, 4).
What is MyLongword.Word[1].Byte[0] := 100. It is TLongwordBitHelper(MyLongword).GetWord(1).SetByte(0). The result and the variable are not related.
Correct. Today I am going to expose a (protected) longwordptr and wordptr property and a longwordptr helper. Maybe I can expose the actual data that way.
The nested helper can than work on the typed pointers and expose the word and byte properties that way.
Theoretically that should be possible. If that fails, I give up.  :( :o
« Last Edit: August 28, 2018, 07:12:47 am by Thaddy »
Specialize a type, not a var.

avra

  • Hero Member
  • *****
  • Posts: 2514
    • Additional info
Re: nested helpers - a call for help
« Reply #12 on: August 28, 2018, 08:54:41 am »
Unless I mis-understood Avra..
You understood well Thaddy. I already did what I could with pointers and gave up. That's why I asked for help. This example is simplified problem that I face with BitHelpers. If that remains unsolved then I would prefer to see them in FPC as they are now (in separate unit), because no matter how well this behavior gets documented it would confuse people who don't read much docs. If problem eventualy gets solved then I would like to extend original FPC helpers (in sysutils as you have suggested), and make BitHelpers more embedded into FPC.

The output is still wrong. It is still 1274. What he means is that 250 should have changed into 100.
That is not the case, because of copy semantics.
That code compiles, doesn't mean code does what you want.
I just explain by example the idea of @Martin_fr to prevent incorrect use of such a construction. The code does not compile if you do what I said above. Because from @avra "...but I am looking for ways to either make line 7 possible or to forbid users to use such nested/propagated helpers..."
ASerge, please forgive me for not being clear enough. By forbiding the users use such constructions I ment to not allow them to use helpers for nested writing (that goes to limbo instead of original variable) - user's code should not compile, but helpers themself should compile. Something like this:
Code: Pascal  [Select][+][-]
  1. MyLongword.Byte[0] := 250;         // this should compile since it does as expected
  2. MyLongword.Word[1].Byte[1] := 250; // this should not compile since writing goes to limbo instead of MyLongWord

I am very greatful for all efforts on this topic, and I do enjoy looking at some amazing ideas you people have !  8)  :D  8)
ct2laz - Conversion between Lazarus and CodeTyphon
bithelpers - Bit manipulation for standard types
pasettimino - Siemens S7 PLC lib

avra

  • Hero Member
  • *****
  • Posts: 2514
    • Additional info
Re: nested helpers - a call for help
« Reply #13 on: August 28, 2018, 09:12:10 am »
ASerge, I think I misunderstood your solution initially. I see now that compilation gets an error just on this line:
Code: Pascal  [Select][+][-]
  1. MyLongword.Word[1].Byte[0] := 100;
and that is the goal - to prevent users from using nested helpers for writing. Unfortunatelly, it also forbids using nested helpers for reading like this:
Code: Pascal  [Select][+][-]
  1. x := MyLongword.Word[1].Byte[0]; \\ this should compile but it doesn't
which limits usage a lot. I will think more about it, but at the moment I do not see how to jump over that wall so sadly this nice idea will probably have to be rejected. :(

Thanks anyway!   :)
« Last Edit: August 28, 2018, 09:16:14 am by avra »
ct2laz - Conversion between Lazarus and CodeTyphon
bithelpers - Bit manipulation for standard types
pasettimino - Siemens S7 PLC lib

Thaddy

  • Hero Member
  • *****
  • Posts: 14197
  • Probably until I exterminate Putin.
Re: nested helpers - a call for help
« Reply #14 on: August 28, 2018, 03:28:21 pm »
My experiments show it is possible to maintain the actual data, but not with your required syntax.
I give up. :(
Maybe Sven has an enlightening idea?

I enjoyed the journey... ::) This is one of the things you really want to put some effort in, because it is...
« Last Edit: August 28, 2018, 03:32:34 pm by Thaddy »
Specialize a type, not a var.

 

TinyPortal © 2005-2018