Issue with in operator in Delphi 64 bits project - delphi

I'm porting a Delphi project to 64 bits and I have a problem with a line of code which has the IN operator.
The compiler raise this error
E2010 Incompatible types: 'Integer' and 'Int64'
I wrote this sample app to replicate the problem.
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils;
Var
I : Integer;
L : Array of string;
begin
try
if I in [0, High(L)] then
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
readln;
end.
This code works ok in 32 bits, but why doesn't it compile in Delphi XE2 64 bits? How I can fix this issue?
*UPDATE *
It seems which my post caused a lot of confusion (sorry for that) , just for explain the original code which i'm porting is more complex, and I just wrote this code as a sample to illustrate the issue. the original code uses an in operator to check if a value (minor than 255) belongs to a group of values (all minor or equal to 255) like so
i in [0,1,3,50,60,70,80,127,High(LArray)]

This code can't be compiled because the High function is returning a 8 byte value, which is not a ordinal value. and the In operator can be only used in sets with ordinal values.
FYI, the size of the results returned by the High function is different depending of the parameter passed as argument.
Check this sample
Writeln(SizeOf(High(Byte)));
Writeln(SizeOf(High(Char)));
Writeln(SizeOf(High(Word)));
Writeln(SizeOf(High(Integer)));
Writeln(SizeOf(High(NativeInt)));
Writeln(SizeOf(High(TBytes)));
Finally, you can fix your code casting the result of High function to integer.
if I in [0, Integer(High(L))] then
UPDATE
Check the additional info provided by David and remember to be very careful when you use the in operator to check the membership of a value in set with variable values. The in operator only checks the least significant byte of each element (in delphi 32 bits).
Check this sample
i:=257;
Writeln( 1 in [i]);
This return true because the low byte of 257 is 1.
And in Delphi 64 bits, the values greater than 255 are removed of the set. So this code
i:=257;
Writeln( 1 in [i]);
will return false because is equivalent to
Writeln( 1 in []);

What RRUZ says is quite correct.
To add a little bit more explanation, in 64 bit Delphi, dynamic array indices can be 64 bits wide. This is clearly needed, for example, when working with a large TBytes memory block. And so the high function must return a value of a type wide enough to hold all possible indices. So, high when applied to a dynamic array, returns a value of type Int64.
Once you start compiling 64 bit code the in operator is unsuited to the problem you are trying to solve. Whilst you could use the cast that RRUZ suggests, it may be clearer to write the code like this
if (I=low(L)) or (I=high(L)) then
Whilst the in operator makes for quite readable code, it is my opinion that a cast to Integer is not acceptable here. That will simply set a trap for you to fall into when you first have an array with more than high(Integer) elements. When that happens the code with the cast will stop working.
But in fact the problems run far deeper than this. The in version of the code fails long before you reach high(Integer) elements. It turns out that your code, whilst it compiles, does not really work. For example, consider this program:
program WeirdSets;
{$APPTYPE CONSOLE}
uses
SysUtils;
var
a: array of Integer;
begin
SetLength(a, 257);
Writeln(BoolToStr(Length(a) in [0, Length(a)], True));
end.
You would expect this program to output True but in fact it outputs False. If instead you were to write
Writeln(BoolToStr(Length(a) in [0, 257], True));
then the compiler reports:
[DCC Error] WeirdSets.dpr(9): E1012 Constant expression violates subrange bounds
The fundamental issue here is that sets are limited to 256 elements so as soon as you have an array with length greater than that, your code stops working.
Sadly, Delphi's support for sets is simply inadequate and is in urgent need of attention.
I also wonder whether you actually meant to write
if I in [0..High(L)] then
If so then I would recommend that you use the InRange function from Math.
if InRange(I, 0, High(L)) then
or even better
if InRange(I, low(L), High(L)) then

The most serious problem with the OP code is that in operator is limited to set size, i.e. [0..255]. Try this in any 32 bit version of Delphi to avoid the 64 bit issue:
var
I: Integer;
L: array of Integer;
begin
SetLength(L, 1000);
I:= 999;
Assert(I in [0, High(L)]); // fails !
end;
The OP is lucky if Length(L) <= 256 always, otherwise it is a bug you probably never thought of.
To find this bug switch range checking on:
{$R+}
procedure TForm1.Button2Click(Sender: TObject);
var
I: Integer;
A: array of Integer;
begin
SetLength(A, 1000);
I:= 999;
if I in [0, High(A)] then ShowMessage('OK!'); // Project .. raised exception
// class ERangeError with message 'Range check error'.
end;

Related

Unexpected result from Max function in FreePascal

The running example is really simple to understand:
program Project1;
uses
SysUtils, Math;
var
fValue: double;
fValueMax: double;
begin
fValue := 7.0207503445953527;
fValueMax := Max(0, fValue);
writeln(fValue);
writeln(fValueMax);
readln;
end.
However the result is completely unexpected. For some reason, the Max function does not only return the larger number from the two arguments but also changes it's value.
In the example code above, the expected value of fValueMax is exactly fValue, but instead fValueMax is bigger. The difference is approximately E-7, so small, but still unexpected and crashes my following code (which is not published here to keep the question clear and simple).
I should state upfront that the last time I used Pascal was close to 25 years ago. But I pulled down Free Pascal out of curiosity and tried this:
program Project1;
uses
SysUtils, Math;
var
fValue: double;
fValueMax: double;
fSingle: single;
fValue2: double;
fValue2b: double;
fValueMax2: double;
begin
fValue := 7.0207503445953527;
fSingle := 7.0207503445953527;
fValueMax := Max(0, fValue);
writeln(fValue); // prints 7.0207503445953527E+000
writeln(fValueMax); // prints 7.0207505226135254E+000
writeln(fSingle); // prints 7.020750523E+00
fValue2 := 7.0207503445953527;
fValue2b := 0.0;
fValueMax2 := Max(fValue2b, fValue2);
writeln(fValue2); // prints 7.0207503445953527E+000
writeln(fValueMax2); // prints 7.0207503445953527E+000
readln;
end.
My first two writeln commands show the same result that you reported seeing. I suspected that perhaps Max was returning a value with less precision that the double you expected to get back, so I created fSingle and assigned it the same literal as you assigned to fValue, and sure enough, its value looks very close to what you're getting back in fValueMax.
So finally, instead of invoking Max with fValue and the literal 0, I called it with two variables of type double, one of which I had set to 0.0. In this case you can see that the input (fValue2) and the output (fValueMax2) have exactly the same value. So while I don't know exactly what Pascal's rules are for determining which overload to call, I wonder if your original call to Max was somehow resolving to the version that takes two single values and returns the same.
While you may be aware of this, I feel compelled to throw in the usual caution about how floating-point types like single and double won't always be able to exactly represent the values you want them to. Here's a good overview.

Error Delphi XE2 - Exception class $C00000005

I am getting this error will debugging a project, which used to be in Delphi 7 and I have been upgrading to Delphi XE2, the same error happens in several methods.
First chance exception at $006DC660. Exception class $C0000005 with message 'access violation at 0x006dc660 read of address 0xffffffff'
This is one of the methods:
PFI = ^TFI;
TFI = record
Id : TToken;
Name : TName;
Parameters : string;
end;
function TListFI.IsIn(S: PChar): PFI;
function SearchName2(Item: PFI):Boolean;
var N1, N2: PChar;
begin
N1:= StrNew(Item^.Name);
N2:= StrNew(S); //Here is the issue
SearchName2:= (StrComp(StrUpper(N1), StrUpper(N2)) = 0);
StrDispose(N1);
StrDispose(N2);
end;
begin
IsIn:= PFI(FirstThat(#SearchName2));
end;
I have googled and I found someone describing a similar problem, and he affirms that when the incremental linker is disabled it works, can someone tell me what and where is it or give some advice to solve this situation.
[EDIT]
Removing the # now gives me the following error in IsIn:= PFI(FirstThat(SearchName2));
E2010 Incompatible types: 'TObject' and 'PFI'
I am adding the FirstThat procedure to see if it may help.
TFuncionColeccion = function (Elemento: TObject): Boolean;
function TColeccion.FirstThat (Rutina: TFuncionColeccion): TObject;
var
i: Integer;
begin
For i:=0 to Count-1 do
if Rutina(Items[i]) then
begin
FirstThat:=Items[i];
exit;
end;
FirstThat:=nil;
end;
It is (and always has been) an error to call local (nested) procedures by pointer, which is clearly what your FirstThat function does. The compiler has to do special things with the stack to call local functions and give them access to the parent scope's variables (S in your code), but the compiler can only know to do those special things when the local function is called directly. The compiler cannot know that the argument to FirstThat will be a local function, so it doesn't include the special code when FirstThat invokes the pointed-to function.
The bottom line is that the stack inside the function doesn't get set up the way it's supposed to, and that means any number of strange symptoms may appear. You'll have to use some other way. Maybe make SearchName2 be a two-argument function, and then write FirstThat to accept S as a parameter that it can forward to the function argument.
You shouldn't need to use the # operator when constructing a function pointer. When you do, the compiler tends to skip type checking, which is what allowed you to pass a local function pointer to FirstThat in the first place. When the function you're passing really matches the required prototype, the compiler will allow you to pass it without the # operator.
You are reporting an access violation in
StrNew(S)
where S is of type PChar. The explanation for that, with probability very close to 1, is that S is not in fact a pointer to null terminated array of WideChar.
In Delphi 7, PChar is an alias for PAnsiChar. That is a pointer to null terminated array of AnsiChar, i.e. 8 bit characters. In Delphi XE2, PChar is an alias for PWideChar, a pointer to null terminated array of WideChar, i.e. 16 bit characters.
It helps to understand what StrNew does. It walks the array until it finds a null character. For 8 bit text that is a single zero byte. For 16 bit text, the null is a zero 16 bit word. Then it allocates a new block of memory of the same length as the input string, and makes a copy into that new memory. The source code is:
function StrNew(const Str: PWideChar): PWideChar;
var
Size: Cardinal;
begin
if Str = nil then Result := nil else
begin
Size := StrLen(Str) + 1;
Result := StrMove(WideStrAlloc(Size), Str, Size);
end;
end;
The only plausible failure mode is that when StrLen walks the array, it attempts an invalid memory read. And that can only happen if your input parameter is invalid. In other words, this must be a programming error on your part.
One possible explanation is that you are in fact passing 8 bit text to this function despite promising to pass 16 bit text. An easy mistake to make, especially if you are not yet fully familiar with the Unicode change. The 8 bit text has a zero terminator, but the byte that follows happens not to be zero. Or the zero byte falls at an odd numbered offset from the start. And then StrNew continues walking the buffer, but now it is off the end and it so happens that it doesn't find a zero word before overrunning into an address that has not been allocated. And that is an access violation.
If that is so then solution will be either:
Change the function's parameter to be of type PAnsiChar, and fix the dubious casting at the call site.
Pass the function 16 bit text as it requires.
In your update you include the address which cannot be read, 0xffffffff. This is -1 in hex. And that would seem to be the most prosaic of errors. Your pointer is completely bogus! Your exact error message can be reproduced with this code: StrNew(PChar(-1)).
I don't have enough information here to tell you why your pointer is bogus. Hopefully you've learnt some debugging and diagnostic techniques that will enable you to solve the problem. At least you now know that the error is in your code.
Assuming that BuscaName2 and SearchName2 are one and the same thing, then you need look no further. Local procedures can only be called from a containing function. As #Rob correctly says, the use of # with procedures is almost always incorrect and is a warning sign of serious problems with your code.

Is it safe to type-cast TArray<X> to array of X?

Today I discovered a compiler bug (QC#108577).
The following program fails to compile:
program Project1;
{$APPTYPE CONSOLE}
procedure P(M: TArray<TArray<Integer>>);
begin
SetLength(M, 1, 2);
end;
begin
end.
The compiler gags on the SetLength line and says:
[dcc32 Error] E2029 ')' expected but ',' found
I know I could fix it like this:
procedure P(M: TArray<TArray<Integer>>);
var
i: Integer;
begin
SetLength(M, 1);
for i := low(M) to high(M) do
SetLength(M[i], 2);
end;
but naturally I'm keen to avoid having to resort to this.
The following variant compiles and seems to work:
procedure P(M: TArray<TArray<Integer>>);
type
TArrayOfArrayOfInteger = array of array of Integer;
begin
SetLength(TArrayOfArrayOfInteger(M), 1, 2);
end;
I don't know enough about the implementation details of dynamic arrays, TArray<T> casting, reference counting etc. to be confident that this is safe.
Is there anybody out there who does know enough to say one way or another whether or not this will produce the correct code at runtime?
The compiler intrinsic procedure SetLength constructs an array of dimensions on the fly on the stack and calls DynArraySetLength for any dynamic array, be it generic or not. If a generic array wouldn't be structurally compatible with a regular dynamic array, the same implementation for setting the length possibly wouldn't be called.
In fact documentation of DynArraySetLength offers SetLength as an alternative for multi-dimensional arrays. DynArraySetLength could also be used instead of a typecast, but I don't see any reason to prefer one or the other.
By design of the generics implementation, using a manual map to array of array of Integer will work.
But there is no benefit of using generics here!
Just code:
type
TArrayOfArrayOfInteger = array of array of Integer;
procedure P(M: TArrayOfArrayOfInteger);
begin
SetLength(TArrayOfArrayOfInteger, 1, 2);
end;
Note also that such TArray<> or array of .. are passed by value, and copied on the stack, unless you specify const or var:
procedure P(var M: TArrayOfArrayOfInteger);
begin
SetLength(TArrayOfArrayOfInteger, 1, 2);
end; // now caller instance of the parameter will be resized
var A: TArrayOfArrayOfInteger;
...
A := nil;
P(A);
assert(length(A)=1);
assert(length(A[0])=2);
I was recently bitten by the fact that DynamicArray<T> and TArray<T> in C++ are actually implemented differently (DynamicArray is a standalone class, whereas TArray is a TObject descendant), which implies that array of T and TArray<T> do have some implementation differences in Delphi as well. They certainly produce different types of RTTI, at least. Which was the root cause of a problem in some of my C++ code that started failing when the Delphi compiler started outputting TArray typedefs in HPP files for Delphi array of ... types instead of DynamicArray typedefs.

File size calculation, Int64, and differences between 32bit and 64bit

I had problems with the following code:
var
FileSize : Int64;
...
FileSize := Info.nFileSizeLow or (Info.nFileSizeHigh shl 32);
I expected it to work because of the Int64 type of the left side of the assignment. But it does not. The partial calculation containing the shl seems to produce an overflow.
So I changed it to:
FileSize := Info.nFileSizeLow or (Int64 (Info.nFileSizeHigh) shl 32);
which works on my 32 bit operating system, but does not work on Vista 64 bit!
Finally,
FileSize := Info.nFileSizeHigh;
FileSize := FileSize shl 32;
FileSize := Info.nFileSizeLow or FileSize;
works on both systems.
Can someone explain the differences in these three versions?
Generally speaking, the type of the expression a * b, where a and b are of type Integer and * is an operator that applies to Integer, is an integer type with the same range as Integer. (I say generally, as an exception is /.) In order for an operator to use 64-bit operations, one or more of the operands must have a range that is only expressible with a 64-bit type. That should cause all the operands to be promoted to 64-bit, and a 64-bit operation performed.
The fact that the left hand side of an assignment is a 64-bit location generally has no effect on the interpretation and typing of the expression on the right hand side of the assignment operator. This is the way it is in almost all languages that I'm aware of that have statically dispatched 32-bit and 64-bit operator overloads (as opposed to polymorphically dispatched operators on arbitrary precision integers or numeric towers etc.); making things behave otherwise would be very surprising behaviour.
For example, arguments to procedure calls are effectively implicit assignments to the parameters. If the left hand side of an assignment could change the interpretation of the expression on the right, we would not know how to interpret the argument to a procedure call without already knowing the definition:
var a, b: Integer;
// ...
P((a shl 16) or b); // 32-bit operation or 64-bit operation?
I do not know why you are seeing different behaviour with your second and third versions of the code. As far as I can see, they should be interpreted the same, and in my tests, they are interpreted the same. If you could provide sample code that works on 32-bit Windows but fails on 64-bit Windows, I could investigate further.
Actually, this is pretty well documented in Delphi 7's help file, under "Integer types":
In general, arithmetic operations on integers return a value of type Integer--which, in its current implementation, is equivalent to the 32-bit Longint. Operations return a value of type Int64 only when performed on one or more Int64 operand. Hence the following code produces incorrect results.
The code example provided:
var
I: Integer;
J: Int64;
...
I := High(Integer);
J := I + 1;
To get an Int64 return value in this situation, cast I as Int64:
...
J := Int64(I) + 1;
First of all FileSize must be defined as UInt64 and not Int64...
UInt64 (not available in early Delphi versions) is an unsigned 64 bit integer, aka a QWORD. This is the expected type for the FileSize (you won't expect a negative file size, won't you?).
IMHO you could have coded - using UInt64 because we don't want to have some values reported as negative:
FileSize := UInt64(Info.nFileSizeLow) or (UInt64(Info.nFileSizeHigh) shl 32));
But under Delphi 7 it produces the same exact code as yours.
FileSize := Info.nFileSizeLow or (Int64(Info.nFileSizeHigh) shl 32));
So there is perhaps some compiler regression. Could you take a look at the asm generated code (step debugger then Alt+F2), and see if there is a difference. But it's unlikely...
In all cases, here is a better (and faster) code:
with Int64Rec(FileSize) do
begin
Lo := Info.nFileSizeLow;
Hi := Info.nFileSizeHigh;
end;
The official MSDN documentation states about the WIN32_FIND_DATA Structure:
nFileSizeHigh: The high-order DWORD value of the file size, in bytes.
This value is zero unless the file size is greater than MAXDWORD.
The size of the file is equal to
(nFileSizeHigh * (MAXDWORD+1)) +
nFileSizeLow.
nFileSizeLow: The low-order DWORD value of the file size, in bytes.
Here is the resulting code:
FileSize := UInt64(Info.nFileSizeLow)+(UInt64(Info.nFileSizeHigh)*UInt64(1 shl 32));
Quite a funny definition, indeed...
This is not really an answer, but it's too long for a comment.
I noticed Delphi gets confused when the result of an expression is to be written into a 64 bit variable, but the operands are 32 bit. I ran into this bug when I was implementing a hash function returning an 64 bit number. Your third variant works because you're first assigning the 64 bit variable, helping Delphi figure out it really needs to do 64 bit arithmetic.
I'm tempted to say both variants (1) and (2) are actually failing because Delphi generates 32 bit arithmetic and then assignes the result to the 64 bit variable. I'm tempted to say the variant that works well on your 32 bit machine benefits from some sort of "unlucky non-failure" (ie: the code is bad, but none the less it produces good results for the given test). The trouble is, COMPILED code doesn't change when moved from a 32bit machine to a 64 bit machine. If the code is the same, the input is the same, you'd have to pin the error on the CPU, but you know you didn't find an bug in your CPU, so you have to fall back and re-think your tests, or pin it on the "unluck non-failure".
test on Delphi 7 and version 2 is OK. Must be bug of later version

Delphi 2009 - Bug? Adding supposedly invalid values to a set

First of all, I'm not a very experienced programmer. I'm using Delphi 2009 and have been working with sets, which seem to behave very strangely and even inconsistently to me. I guess it might be me, but the following looks like there's clearly something wrong:
unit test;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
private
test: set of 1..2;
end;
var Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
test := [3];
if 3 in test then
Edit1.Text := '3';
end;
end.
If you run the program and click the button, then, sure enough, it will display the string "3" in the text field. However, if you try the same thing with a number like 100, nothing will be displayed (as it should, in my opinion). Am I missing something or is this some kind of bug? Advice would be appreciated!
EDIT: So far, it seems that I'm not alone with my observation. If someone has some inside knowledge of this, I'd be very glad to hear about it. Also, if there are people with Delphi 2010 (or even Delphi XE), I would appreciate it if you could do some tests on this or even general set behavior (such as "test: set of 256..257") as it would be interesting to see if anything has changed in newer versions.
I was curious enough to take a look at the compiled code that gets produced, and I figured out the following about how sets work in Delphi 2010. It explains why you can do test := [8] when test: set of 1..2, and why Assert(8 in test) fails immediately after.
How much space is actually used?
An set of byte has one bit for every possible byte value, 256 bits in all, 32 bytes. An set of 1..2 requires 1 byte but surprisingly set of 100..101 also requires one byte, so Delphi's compiler is pretty smart about memory allocation. On the othter hand an set of 7..8 requires 2 bytes, and set based on a enumeration that only includes the values 0 and 101 requires (gasp) 13 bytes!
Test code:
TTestEnumeration = (te0=0, te101=101);
TTestEnumeration2 = (tex58=58, tex101=101);
procedure Test;
var A: set of 1..2;
B: set of 7..8;
C: set of 100..101;
D: set of TTestEnumeration;
E: set of TTestEnumeration2;
begin
ShowMessage(IntToStr(SizeOf(A))); // => 1
ShowMessage(IntToStr(SizeOf(B))); // => 2
ShowMessage(IntToStr(SizeOf(C))); // => 1
ShowMessage(IntToStr(SizeOf(D))); // => 13
ShowMessage(IntToStr(SizeOf(E))); // => 6
end;
Conclusions:
The basic model behind the set is the set of byte, with 256 possible bits, 32 bytes.
Delphi determines the required continuous sub-range of the total 32 bytes range and uses that. For the case set of 1..2 it probably only uses the first byte, so SizeOf() returns 1. For the set of 100.101 it probably only uses the 13th byte, so SizeOf() returns 1. For the set of 7..8 it's probably using the first two bytes, so we get SizeOf()=2. This is an especially interesting case, because it shows us that bits are not shifted left or right to optimize storage. The other interesting case is the set of TTestEnumeration2: it uses 6 bytes, even those there are lots of unusable bits around there.
What kind of code is generated by the compiler?
Test 1, two sets, both using the "first byte".
procedure Test;
var A: set of 1..2;
B: set of 2..3;
begin
A := [1];
B := [1];
end;
For those understand Assembler, have a look at the generated code yourself. For those that don't understand assembler, the generated code is equivalent to:
begin
A := CompilerGeneratedArray[1];
B := CompilerGeneratedArray[1];
end;
And that's not a typo, the compiler uses the same pre-compiled value for both assignments. CompiledGeneratedArray[1] = 2.
Here's an other test:
procedure Test2;
var A: set of 1..2;
B: set of 100..101;
begin
A := [1];
B := [1];
end;
Again, in pseudo-code, the compiled code looks like this:
begin
A := CompilerGeneratedArray1[1];
B := CompilerGeneratedArray2[1];
end;
Again, no typo: This time the compiler uses different pre-compiled values for the two assignments. CompilerGeneratedArray1[1]=2 while CompilerGeneratedArray2[1]=0; The compiler generated code is smart enough not to overwrite the bits in "B" with invalid values (because B holds information about bits 96..103), yet it uses very similar code for both assignments.
Conclusions
All set operations work perfectly well IF you test with values that are in the base-set. For the set of 1..2, test with 1 and 2. For the set of 7..8 only test with 7 and 8. I don't consider the set to be broken. It serves it's purpose very well all over the VCL (and it has a place in my own code as well).
In my opinion the compiler generates sub-optimal code for set assignments. I don't think the table-lookups are required, the compiler could generate the values inline and the code would have the same size but better locality.
My opinion is that the side-effect of having the set of 1..2 behave the same as set of 0..7 is the side-effect of the previous lack of optimization in the compiler.
In the OP's case (var test: set of 1..2; test := [7]) the compiler should generate an error. I would not classify this as a bug because I don't think the compiler's behavior is supposed to be defined in terms of "what to do on bad code by the programmer" but in terms of "what to do with good code by the programmer"; None the less the compiler should generate the Constant expression violates subrange bounds, as it does if you try this code:
(code sample)
procedure Test;
var t: 1..2;
begin
t := 3;
end;
At runtime, if the code is compiled with {$R+}, the bad assignment should raise an error, as it does if you try this code:
(code sample)
procedure Test;
var t: 1..2;
i: Integer;
begin
{$R+}
for i:=1 to 3 do
t := i;
{$R-}
end;
According to the official documentation on sets (my emphasis):
The syntax for a set constructor is: [
item1, ..., itemn ] where each item is
either an expression denoting an
ordinal of the set's base type
Now, according to Subrange types:
When you use numeric or character
constants to define a subrange, the
base type is the smallest integer or
character type that contains the
specified range.
Therefore, if you specify
type
TNum = 1..2;
then the base type will be byte (most likely), and so, if
type
TSet = set of TNum;
var
test: TSet;
then
test := [255];
will work, but not
test := [256];
all according to the official specification.
I have no "inside knowledge", but the compiler logic seems rather transparent.
First, the compiler thinks that any set like set of 1..2 is a subset of set of 0..255. That is why set of 256..257 is not allowed.
Second, the compiler optimizes memory allocation - so it allocates only 1 byte for set of 1..2. The same 1 byte is allocated for set of 0..7, and there seems to be no difference between the both sets on binary level. In short, the compiler allocates as little memory as possible with alignment taken into account (that means for example that compiler never allocates 3 bytes for set - it allocates 4 bytes, even if set fits into 3 bytes, like set of 1..20).
There is some inconsistency in a way the compiler treats sets, which can be demonstrated by the following code sample:
type
TTestSet = set of 1..2;
TTestRec = packed record
FSet: TTestSet;
FByte: Byte;
end;
var
Rec: TTestRec;
procedure TForm9.Button3Click(Sender: TObject);
begin
Rec.FSet:= [];
Rec.FByte:= 1; // as a side effect we set 8-th element of FSet
// (FSet actually has no 8-th element - only 0..7)
Assert(8 in Rec.FSet); // The assert should fail, but it does not!
if 8 in Rec.FSet then // another display of the bug
Edit1.Text := '8';
end;
A set is stored as a number and can actually hold values that are not in the enumeration on which the set is based. I would expect an error, at least when Range Checking is on in the compiler options, but this doesn't seem to be the case. I'm not sure if this is a bug or by design though.
[edit]
It is odd, though:
type
TNum = 1..2;
TSet = set of TNum;
var
test: TSet;
test2: TNum;
test2 := 4; // Not accepted
test := [4]; // Accepted
From the top of my head, this was a side effect of allowing non contiguous enumeration types.
The same holds for .NET bitflags: because in both cases the underlying types are compatible with integer, you can insert any integer in it (in Delphi limited to 0..255).
--jeroen
As far as I'm concerned, no bugs there.
For exemple, take the following code
var aByte: Byte;
begin
aByte := 255;
aByte := aByte + 1;
if aByte = 0 then
ShowMessage('Is this a bug?');
end;
Now, you can get 2 result from this code. If you compiled with Range Checking TRUE, an exception will be raise on the 2nd line. If you did NOT compile with Range Checking, the code will execute without any error and display the message dialogs.
The situation you encountered with the sets is similar, except that there is no compiler switch to force an exception to be raised in this situation (Well, as far as I know...).
Now, from your exemple:
private
test: set of 1..2;
That essentially declare a Byte sized set (If you call SizeOf(Test), it should return 1). A byte sized set can only contain 8 elements. In this case, it can contains [0] to [7].
Now, some exemple:
begin
test := [8]; //Here, we try to set the 9th bit of a Byte sized variable. It doesn't work
Test := [4]; //Here, we try to set the 5th bit of a Byte Sized variable. It works.
end;
Now, I need to admit I would kind of expect the "Constant expression violates subrange bounds" on the first line (but not on 2nd)
So yeah... there might be a small issue with the compiler.
As for your result being inconsistent... I'm pretty sure using set values out of the set's subrange values isn't guaranteed to give consistent result over different version of Delphi (Maybe not even over different compiles... So if your range is 1..2, stick with [1] and [2].

Resources