The Delphi Bug List

Compiler

Bad code


The color codes indicate in which version(s) of Delphi the bug occurs and what its status is.
Latest update: 23 July 1998
Bug # Delphi versions Description
110 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Exception on converting IntToStr(Low(Integer)) back into an integer
119 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Passing an open array as a parameter to a constructor can damage the exception stack
120 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Delphi forgets to decrement the reference-count for long strings when assigning whole records
121 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
ObjectRef IS ClassRef sometimes returns TRUE when ObjectRef = Nil.
123 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
try..finally and try..except may generate bad code
133 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Bad code is generated for for loops starting at 1 and counting downto a value <=1 that is stored in a variable.
135 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
"Pascal" calling convention gives trouble with "Object" types
137 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Range check error with subrange types in a for loop
This is not a bug; this behaviour is documented
140 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
A specific case where the optimizer produces bad code.
144 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Missed range check in Abs(smallint)
146 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
D3 generates bad code for logical (bit-wise) operations
151 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
A class with a dynamic constructor created through a class reference variable will cause an access violation.
388 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
The code optimizer makes a serious mistake when optimizations are turned on and the body of a simple function is enclosed in a Try..Except block where the Except block includes a raise statement.
418 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Compiler generates invalid code when calling events returned by an indexed property.
393 1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02 optimiser
Handling an exception trashes any local variable stored (by the optimiser) in EBX, ESI or EDI.

Bug #110; last modified: before April 1998
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha Gotcha
Bad code

Exception on converting IntToStr(Low(Integer)) back into an integer

Description
By Wolfgang Rohdewald

The following code results in an EConvertError "-2147483648 is not a valid integer":
  s := IntToStr(Low(Integer));

  i := StrToInt(s);
Steve Schafer's answer was:
No, it's not a bug. Most compilers (not just Delphi) parse a string representing a negative integer as a negated positive integer. That is, the compiler sees -12345, so it evaluates 12345, and then negates it. This obviously won't work for the minimum integer, since there is no corresponding positive integer.
However I still would say it is a bug. It doesn't help if other compilers have the same bug.

Bug #119; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
N/A Exists Exists Exists Unknown Fixed Fixed Fixed
Bad code

Passing an open array as a parameter to a constructor can damage the exception stack

Description
Reported by Martin Liesén; checked by Chris Rankin
The problem is illustrated with the following program:
{$A+,B-,G+,H+,I+,J-,M-,O+,T-,U-,V+,W-,X+,Z1}
{$MINSTACKSIZE $00004000}
{$MAXSTACKSIZE $00100000}
{$IMAGEBASE $00400000}
{$APPTYPE Console}

{$WARNINGS On}
{$HINTS On}

{$DEFINE Bug}
{$IFDEF Bug}
  {$DEFINE Fix1}
  {$UNDEF Fix2}
{$ENDIF}

program Construct;
uses Windows, SysUtils;

type
  TTest = class
    constructor Create({$IFDEF Fix1} const {$ENDIF} T: array of Integer);
  end;

constructor TTest.Create({$IFDEF Fix1} const {$ENDIF} T: array of Integer);
begin
  inherited Create;
{$IFDEF Fix2}
  asm
    MOV EAX, [EBP+TYPE Pointer + TYPE Integer]
    INC EAX
    LEA ESP, [ESP+4*EAX]
  end
{$ENDIF}
end;

var
  Te: TObject;
begin
  try
    Te := TTest.Create([10,20]);
    try
      raise Exception.Create('BUG TEST');
    finally
      Te.Free
    end
  except
    MessageBox(0,'Caught exception','Construction Error',MB_OK)
  end
end.
Note from checker:
What is happening here (more or less) is that Delphi is creating a local copy of the open array on the stack, and then forgetting to clean up after itself later. Every constructor contains a exception handler so that Delphi can call the destructor automatically should the construction fail. Unfortunately, the elements of the open array are interfering with the cleanup of this exception handler.
Solution / workaround
There are two possible workarounds. The first (and best) is to pass the open array as "const". This means that Delphi doesn't have to make a local copy in the first place, neatly avoiding the problem. The second involves cleaning the stack up manually using inline assembler, and requires a bit of knowledge as to how Delphi passes open arrays. Basically, Delphi pushes each element onto the stack as a 32 bit value, and then passes the address of the first element followed by the array-index of the last element. Open arrays are zero-based, so the total number of elements in the array is this index + 1. Finally, you add this number of DWORDs to the stack pointer ESP. Of course, you have to find where on the stack the constructor's open array parameter is yourself.

Bug #120; last modified: before April 1998
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
N/A Exists Fixed Fixed Fixed Fixed Fixed Fixed
Bad code

Delphi forgets to decrement the reference-count for long strings when assigning whole records

Description
Reported by Andrew Baker; checked by Chris Rankin
Example code that shows the problem:
{$APPTYPE Console}
program BadRefs;

uses
  SysUtils;

type
  PInteger = ^Integer;

  PLongString = ^TLongString;
  TLongString = record
                  RefCount: Integer;
                  Length:   Integer;
                  Data:     array[0..1023] of Char
                end;

type
  rBug = record
           TextStr : string
         end;

procedure ShowRefCount(const s: string);
begin
  Writeln( 'Ref count = ', PLongString(Integer(s)-2*SizeOf(Integer))^.RefCount )
end;

procedure xxx;
var
  lProblem: rBug;
begin
  lProblem.TextStr := lProblem.TextStr + 'Append';
  ShowRefCount(lProblem.TextStr);       // Reference count = 1

  lProblem.TextStr := lProblem.TextStr;
  ShowRefCount(lProblem.TextStr);       // Reference count still = 1

  lProblem := lProblem;
  ShowRefCount(lProblem.TextStr)        // Reference count now = 2 ! BUG!
end;

begin
  xxx
end.
The first line is getting lProblem.TextStr to have a reference count of non-zero. I know it is an unusual first line of a procedure, but I'm trying to make things simple. For example, lProblem.TextStr := AControl.Caption would also work OK. lProblem.TextStr := 'Append' on its own is no good, since there isn't a reference count for constant strings. The problem is in the [third] line, lProblem := lProblem. Again, it is pretty pointless on its own, but it show up the problem. Basically, the 'TextStr' field of the record doesn't get its reference count decremented when you do a complete record copy. ( lProblem.TextStr := lProblem.TextStr would be OK ).

Note from checker: This bug will bite you every time you assign a record-value (with a long string field) to another record:
lProblem2 := lProblem1;
If the long string originally in lProblem2 has a reference count of 1 then it will be leaked when the entire record is overwritten by lProblem1.
Solution / workaround
A D2 workaround is to explicitly decrement the reference counts yourself, e.g.:
  Finalize(lProblem2);
  lProblem2 := lProblem1;

Bug #121; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Exists Exists Exists Unknown Unknown Fixed Fixed Fixed
Bad code

ObjectRef IS ClassRef sometimes returns TRUE when ObjectRef = Nil.

Description
Reported by Ed Eichman; checked and edited by Chris Rankin
Consider the following test application:
{$APPTYPE Console}
program IsObjBug;

uses
  SysUtils;

type
  TBaseClass = class of TBaseObject;

  TBaseObject = class
                public
                  Field1: Integer;
                end;

procedure CheckIsClass(UnknownObj: TBaseObject; UnknownClass: TBaseClass);
begin
  if UnknownObj is UnknownClass then
    Writeln( 'Object IS of this class.' )
  else
    Writeln( 'Object is NOT of this class.' )    // Delphi executes this line
end;

var
  BaseObj: TBaseObject = Nil;
begin
{
  Test the type of BaseObj using "is" ...
}
  if BaseObj is TBaseObject then
    Writeln( 'BaseObj is of type TBaseObj.' )      // Delphi executes this line
  else
    Writeln( 'BaseObj is NOT of type TBaseObj.' );
{
  Test "is"-type using procedure ...
}
  CheckIsClass(BaseObj,TBaseObject);
end.
According to the Object Pascal Language Guide, with reference to the statement "ObjectRef is ClassRef", "If ObjectRef is nil, the result is always False". However, the output shows that the first "is"-operation effectively returns True.

A study of the assembler shows that the real bug here is that Delphi "optimises away" the first "is"-operation and hardcodes the True result. This isn't completely unreasonable since this operation basically reads

"if (Instance of TBaseObject) is TBaseObject then"

and the class-type of BaseObj is known at compile-time. However, this does forget the case when the BaseObj variable has not yet been assigned.
Note: the bug also occurs with optimization off!

Solution / workaround
The problem only appears when Delphi mistakenly thinks it can resolve the "is-operation" at compile-time. When this happens, you should use
  if Assigned(ObjectRef) then
since this is the only check that "is" has left to make anyway.
Simply turing optimization off does *not* do the trick.

Bug #123; last modified: before April 1998
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Unknown Exists Fixed Fixed Fixed Fixed Fixed Fixed
Bad code

try..finally and try..except may generate bad code

Description
Reported by Ray Lischner
The following example code demonstrates the bug:
function Bug: Integer;
begin
  try
    Result := 42;
    Exit;
  finally
    ;
  end;
  Result := 0;
end;
Notice that the code assigns 42 to Result and then executes the Exit statement. This causes the function to return to the caller. In this case, it should return the value 42. Before returning, the finally block executes, but it should not affect the value of Result.

Running this example reveals that this bug returns a garbage value. Looking at the code that Delphi 2 generates, one sees that the value of Result is not saved when exiting the try-finally block. This is a compiler bug.

A similar problem exists for the case where

  try
    Result := StrToInt('Hello') = 0;;
  except
    Result := false;
  end;
Solution / workaround
Using
  try
    Result := StrToInt('Hello') = 0;;
  except
    on Exception do
      Result := false;
  end;
Solves this particular problem.
Bug #133; last modified: before April 1998
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Unknown Exists Fixed Fixed Fixed Fixed Fixed Fixed
Bad code

Bad code is generated for for loops starting at 1 and counting downto a value <=1 that is stored in a variable.

Description
Reported by Jordan Russell
This is a potentially dangerous compiler bug that I discovered in both Delphi 2.0 and 2.01. The problem: Delphi 2.0 and 2.01 cannot use a 'for' loop that begins at a constant 1 and ends at a value <=1. The easist way to describe this is to run this code for yourself (for simplicity, I just use a console application):
var
  D, N: Integer;
begin
  D := -5;
  for N := 1 downto D do
    Writeln (N);
  Readln;
end.
What should happen is it counts from 1 down to -5. But instead, it is skipping the last two counts, which is making it go to -3. Note that the -5 is an example, any number <=1 will produce the same result. If you look at generated code in assembly language you can see this is definately a code generation bug..
I (Reinier Sterkenburg) also received a more elaborated message with commented generated code by Hallvard Vassbotn. If anyone is interested in it, let me know
Solution / workaround
The workarounds I (Jordan Russell) have discovered are:
1. Don't use a for loop when counting from 1 down.
2. Use a variable 1 instead of a constant 1.

Bug #135; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Unknown Gotcha Fixed Fixed Fixed Fixed Fixed Fixed
Bad code

"Pascal" calling convention gives trouble with "Object" types

Description
Reported by Vadim V. Shcolin; checked by Rune Moberg
In Delphi 3 and 4, this is documented; under the D4 topic "constructors (Object Pascal Reference)" it says: "Constructors must use the default register calling convention." The compiler also checks for this.

In D2, the following sample code illustrates the problem:

type
  PSomeObject=^TSomeObject;
  TSomeObject=object
    i: Integer;
    constructor Init; pascal;
  end;

var
  SomeObject: PSomeObject;
When I declare the constructor with calling conv. pascal, calling
  New(SomeObject,Init);
doesn't reserve memory for SomeObject (its value will become nil) and the call to the constructor gives a GPF.
If calling conv. is standard (registers), all work OK.
Solution / workaround
Use the default (register) calling convention.

Bug #137; last modified: 23-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
N/A N/A N/A N/A N/A N/A Unknown Unknown
Bad code

Range check error with subrange types in a for loop
This is not a bug; this behaviour is documented

Description
Reported by Paul van der Eijk; checked by Rune Moberg
I submitted the following to Borland; using Delphi 2.01.
With the following declarations, calling procedure test with 1 as value for the argument, a range check error occurs:
const
  maxn = 30;

type
  tindex = 1..maxn;
  tarr = array[tindex] of integer;

procedure test(r: tindex); 
var
  k: tindex;
  v1,v2: tarr;
  i: integer; 
begin 
  for k := 1 to r - 1 
  do begin
    MessageDlg('Should not be here',mtError,[mbOk],0);
    v1[k] := v2[k];
  end; 
end;
The problem disappears when I declare TIndex = 0..maxn.
I also verified the same code in Delphi 1 and BP7, both work as expected.

Jim Berg pointed out that this behaviour is as documented:
"It was a bug when the code worked in version 1.02. If you look under the for loop documentation it says that the final value expression must be assignment compatible with the loop variable. 0 (zero) is not assignment compatible with K, which has a type of tindex, thus the range check error."
I (Reinier Sterkenburg) have now checked it also under Delphi 1 and there the range check appears as well. So there's no bug at all. Probably this bug report should be deleted from teh bug list altogether...


Bug #140; last modified: before April 1998
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
N/A Exists Fixed Fixed Fixed Fixed Fixed Fixed
Bad code

A specific case where the optimizer produces bad code.

Description
Reported by Duncan Murdoch
The optimizer can still produce bad code. A disassembly of the following routine shows that when compiled with optimization on, the value of w[k] is always w[1].
procedure TForm1.Button1Click(Sender: TObject);
var
  k : integer;
  w : array[1..3] of integer;
label
  2;
begin
  for k:=1 to 3 do
    w[k] := k;
  k:=0;
  FOR k:= 1 to 3 do begin
     GOTO 2;
     raise exception.create('Impossible!');
  2: if k=2 then
       showmessage(format('k, w[2], w[k] = %d, %d, %d (should all be
2)',[k,w[2],w[k]]));
  end;
end;
This appears to require very special conditions: a for loop control variable as the index, a goto that lands just after a raised exception. Unfortunately, this actually came up in some old real code: the singular value decomposition routine from Numerical Recipes had a construction like this (except it was just an error message printed, not an exception raised; I changed it to an exception).

Bug #144; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Absent Unknown Exists Unknown Unknown Fixed Fixed Fixed
Bad code

Missed range check in Abs(smallint)

Description
Reported by Steve Guidos; checked by Duncan Murdoch
In Delphi 1, after evaluating the abs function there was a check for overflow; in Delphi 3 there isn't (because the abs of a smallint is done in 32 bit arithmetic, and can't possibly overflow), but there is also no check that assigning the result to a 16 bit variable stays in range.
Step-by-step instruction on how to re-produce the bug:
Compile and run the following code with both overflow and range checks enabled:
var
  Big: longint;
  Small: smallint;
begin
  Big := low(smallint);  { -32768 }
  Small := low(smallint);
  Big := abs(Big);       { Big = 32768 }
  Small := abs(Small)    { Small = -32768 ! }
  ShowMessage('big='+IntToStr(big)+' small='+IntToStr(small));
end;
In D1, you'll get an integer overflow exception on the abs(Small) calculation. In D3, no exception will be raised, and the message displayed will say "big=32768 small=-32768".

Bug #146; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Unknown Unknown Exists Exists Exists Fixed Fixed Fixed
Bad code

D3 generates bad code for logical (bit-wise) operations

Description
Reported by Tim Knipe and Hans Bischoff; checked and edited by Chris Rankin
Consider the following example:
program BitTest;
uses Windows;
var
  intval: Integer;
begin
  intval := 128+1;
  if (intval and 128) > 0 then
    MessageBox(0, 'We never get here!', 'Bug', MB_OK)
end.
According to the Object Pascal specification, Delphi should promote 128 and intval to the smallest integer-type that contains both their ranges. In this case, since 128 is a Byte and intval is an Integer, and the Byte range is entirely contained within an Integer, this logical AND should be performed as an Integer. Instead, the following code is produced:
mov   eax,81h
test  al,80h   // The AND is being done as a Byte!! This is the bug.
This code sets the sign flag, clears the overflow and zero flags, and so convinces the CPU that the result is negative. Therefore we never see the message box.

Note by Reinier Sterkenburg (12 Jul 98):
Is this really the same bug as the one Hector Santos complains about? 'His bug' depends on the optimizer setting; strange enough, the bug is only present if optimization is off.

Solution / workaround
Use unsigned arithmetic for the comparison e.g. "if (intval and 128) <> 0 then"

Bug #151; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Unknown Unknown Exists Unknown Unknown Fixed Fixed Fixed
Bad code

A class with a dynamic constructor created through a class reference variable will cause an access violation.

Description
Reported by William Brooks; checked by Rune Moberg
Example Code:
program BugMe;

uses Windows;

type
  TBugClass = class of TBugObject;
  TBugObject = class
    constructor Create; dynamic;
  end;

constructor TBugObject.Create;
begin
  MessageBox(0, 'TBugObject.Create', 'Notice', MB_OK);
end;

var BugClass  : TBugClass;
    BugObject : TBugObject;

begin
  BugClass := TBugObject;

  {
    Next Line assembles to:

    mov    dl, 01          // setup for constructor call
    mov    eax, [BugClass] // get class reference (VMT)
    mov    bx, FFFF        // constructor's dynamic ID (-1)
    call   CallDynaInst    // this is where the error occurs, should be CallDynaClass
  }

  BugObject := BugClass.Create;
  MessageBox(0, 'After BugClass.Create', 'Notice', MB_OK);
end.
Further Description:
The problem is that the compiler is generating a call to CallDynaInst (you can find this routine in the System.pas file, called _CallDynaInst). The code should be generated as CallDynaClass, which assumes that the EAX register then contains the VMT address instead of a pointer to the VMT address (as would be the first pointer in an instantiated class).
I reported this problem to Borland a while back, but as of yet have not received any status information on it other than a reply that the message was received.
Solution / workaround
So far I have come up with two workarounds, the first being the most simplistic:
  1. Make the constructor virtual instead of dynamic. The compiler will properly handle this method.
  2. Copy the CallDynaClass code from the System.PAS file and construct the object yourself.

Bug #388; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Unknown Unknown Unknown Unknown Exists Fixed Fixed Fixed
Bad code

The code optimizer makes a serious mistake when optimizations are turned on and the body of a simple function is enclosed in a Try..Except block where the Except block includes a raise statement.

Description
Reported by Greg Martin; checked by Reinier Sterkenburg
The code below demonstrates the code optimizer making a serious mistake when optimizations are turned on and the body of a simple function is enclosed in a Try..Except block where the Except block includes a raise statement. The code below should speak for itself. Simply create a form, put a button on it, and put the following event handler and other functions in the unit to demonstrate the problem.
// Always returns 12345, yet appears to return -1 based on
// return value from the RetBadValue function.
function Returns12345: Integer;
begin
  Result := 12345;  // <-- Any value demonstrates the bug.
end;

// This function returns 0 instead of 12346 when optimizations
// are turned on!!!  All other compiler flags make no difference
// as far as I can tell.
function RetBadValue: Integer;
var
  ValueOf12345: Integer;
begin
  try
    ValueOf12345 := Returns12345;
    Result := ValueOf12345 + 1;
  except
    raise;
  end;
end;

// This version works because the local variable is not used.
function RetGoodValue1: Integer;
//var
//  ValueOf12345: Integer;  // <-- Leaving this in also works.
begin
  try
    Result := Returns12345 + 1;  // <-- Not using local variable.
  except
    raise;
  end;
end;

// This version works because the raise inside the Except block
// is removed.  I can't even guess why.
function RetGoodValue2: Integer;
var
  ValueOf12345: Integer;
begin
  try
    ValueOf12345 := Returns12345;
    Result := ValueOf12345 + 1;
  except
//    raise;  <-- Get rid of raise (Generates compiler warning)
  end;
end;

// This version works because another function could get called
// from the normally error optimized function.  This must force
// the code to use a stack variable instead of a register to
// store the return value?
function RetGoodValue3: Integer;
var
  ValueOf12345: Integer;
begin
  try
    ValueOf12345 := Returns12345;
    Result := ValueOf12345 + 1;

    if Result = 0 then begin
      // ShowMessage is never called because Result is
      // never zero.  Just threatening the call straightens
      // out the optimizer.
      ShowMessage('Returns12345 + 1 = 0');
    end;
  except
    raise;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  ShowMessage('RetBadValue := ' + IntToStr(RetBadValue));
  ShowMessage('RetGoodValue1 := ' + IntToStr(RetGoodValue1));
  ShowMessage('RetGoodValue2 := ' + IntToStr(RetGoodValue2));
  ShowMessage('RetGoodValue3 := ' + IntToStr(RetGoodValue3));
end;

Bug #418; last modified: 10-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
N/A Exists Exists Exists Exists Fixed Fixed Fixed
Bad code

Compiler generates invalid code when calling events returned by an indexed property.

Description
Reported by Jon Shemitz; checked by Hallvard Vassbotn
The compiler generates invalid code that causes it to call a more or less random address. This could generate anything from NOPs, to access violations and in Win95, complete system crash. The code compiles without warning, but it seems the compiler gets confused about what is stored in each register, causing it to generate invalid code.

To reproduce the bug:

  1. Compile and run the following simple project:
    program TestBug;
    uses
      Windows,
      SysUtils,
      Classes;
    type
      TMethodArray = array[0..9] of TMethod;
      TEventList = class
      private
        Count: integer;
        FEvents: TMethodArray;
        function GetItems(Index: integer): TMethod;
        procedure HandleEvent(Sender: TObject);
      public
        constructor Create;
        procedure NotifyOk(Sender: TObject);
        procedure NotifyBug(Sender: TObject);
        property Items[Index: integer]: TMethod read GetItems;
      end;
    
    constructor TEventList.Create;
    var
      i : integer;
    begin
      inherited Create;
      Count := 10;
      for i := 0 to Count-1 do
        TNotifyEvent(FEvents[i]) := Self.HandleEvent;
    end;
    
    procedure TEventList.HandleEvent(Sender: TObject);
    begin
      MessageBeep(-1);
    end;
    
    function TEventList.GetItems(Index: integer): TMethod;
    begin
      Result := FEvents[Index];
    end;
    
    procedure TEventList.NotifyBug(Sender: TObject);
    var
      i : integer;
    begin
      for i := 0 to Count-1 do
        TNotifyEvent(Items[i])(Sender);  // << This generates invalid code 
                                         //  that calls a "random" address
    end;
    
    procedure TEventList.NotifyOk(Sender: TObject);
    var
      i : integer;
      Event: TNotifyEvent;
    begin
      for i := 0 to Count-1 do
      begin
        Event := TNotifyEvent(Items[i]);
        Event(Sender);
      end;
    end;
    
    var
      EventList: TEventList;
    begin
      EventList := TEventList.Create;
      try
        EventList.NotifyOk(nil);
        EventList.NotifyBug(nil);
      finally
        EventList.Free;
      end;
    end.
  2. When you run the project you should hear 10 beeps from the speaker, then the program should show some kind of undefined behaviour (most of the time I get an access violation on my NT4 system)
The invalid code is generated in the NotifyBug method:
    TNotifyEvent(Items[i])(Sender);  // << This generates invalid code
The workaround is to use a temporary variable to hold the result from the call to GetItems, then call the event through this variable as demonstrated in the NotifyOk method:
    Event := TNotifyEvent(Items[i]);
    Event(Sender);
Details: The invalid code generated for NotifyBug is:
testbug.46:  TNotifyEvent(Items[i])(Sender);
:0040A83F 8D4C2404       lea    ecx,[esp+04]   // ECX = @Temp (compiler adds 
extra var Temp: TMethod)
:0040A843 8BD6           mov    edx,esi        // EDX = i
:0040A845 8BC7           mov    eax,edi        // EAX = Self
:0040A847 E8C8FFFFFF     call   testbug.TEventList.GetItems
:0040A84C 8D6C2404       lea    ebp,[esp+04]   // EBP = @Temp
:0040A850 8B1424         mov    edx,[esp]      // EDX = Sender
:0040A853 8B4508         mov    eax,[ebp+08]   // EAX = ??? (Random)
:0040A856 FF5504         call   [ebp+04]       // Call [Temp.Data]
The two last instructions should have been compiled into:
:0040A853 8B4504         mov    eax,[ebp+04]   // EAX = Temp.Data
:0040A856 FF5500         call   [ebp]          // Call [Temp.Code]
Note that the offsets in the invalid code are off by 4 bytes. This causes the call to be made to the address of the event's object address (i.e. it tries to execute data).
The correct code in NotifyOk is:
testbug.56:  Event := TNotifyEvent(Items[i]);
:0040A87E 8D4C2408       lea    ecx,[esp+08]     // ECX = @Temp  (compiler adds
extra var Temp: TMethod)
:0040A882 8BD6           mov    edx,esi          // EDX = i
:0040A884 8BC7           mov    eax,edi          // EAX = Self
:0040A886 E889FFFFFF     call   testbug.TEventList.GetItems
:0040A88B 8B442408       mov    eax,[esp+08]     //
:0040A88F 890424         mov    [esp],eax        // Event.Code := Temp.Code
:0040A892 8B44240C       mov    eax,[esp+0C]
:0040A896 89442404       mov    [esp+04],eax     // Event.Data := Temp.Data
testbug.57:  Event(Sender);
:0040A89A 8BD5           mov    edx,ebp          // EDX = Sender
:0040A89C 8B442404       mov    eax,[esp+04]     // EAX = Event.Data
:0040A8A0 FF1424         call   [esp]            // Call [Event.Code]
Issues:
It could well be that this is a construct that is not supported by the compiler. For instance this code does not compile in D1 and changing the result type of GetItems and the Items property from TMethod to TNotifyEvent still requires a type-cast to compile in the NotifyBug example.

However, if the construct is not supported, the compiler should flag this as a compile-time error. Currently it compiles without any warning and then crashes hard at run-time. This could potentially be a very hard bug to find an a given application, because normally you don't suspect the code generated by the compiler.

Rune Moberg added to this on 10 July 1998: The generated code by Delphi 4 is

        mov    eax,[ebp+04]   
        call   [ebp]
and no access violations occur.
Solution / workaround
Already described above

Bug #393; last modified: 12-Jul-98
1.02 2.01 3.0 3.01 3.02 4.0 4.01 4.02
Unknown Exists Exists Exists Unknown Fixed Fixed Fixed
Bad code - optimiser

Handling an exception trashes any local variable stored (by the optimiser) in EBX, ESI or EDI.

Description
Reported by Steven D'Abrosca; checked and edited by Chris Rankin
Note from checker:
1. This bug has been confirmed by Borland
2. This bug is different from the other optimiser bug which was reported by Greg Martin

Note from Rune Moberg (12 Jul 98):
The bug has been fixed in Delphi 4: The optimizer seems to be less aggressive, the local variable in the source provided is no longer stored in a register. (will this have a negative speed impact on Delphi programs in general? I guess compute intensive apps have avoided exception handling for some time already)
Consider this example program:
{$APPTYPE Console}
program BadOptim;

uses
  Windows, SysUtils;

{$R *.RES}

const MaxSayNo = 5;

{$O+}
procedure TestRaise;
var
  WatchMe: Integer;
begin
  WatchMe := 0;
  while WatchMe < MaxSayNo do
    begin
      repeat
        try
          if MessageBox(0,'Raise an exception?',
                          'Bad Optimisation',MB_YESNO) = ID_YES then
            raise Exception.Create('Exception raised!');
          Break
        except
        {
          Catch-all exception handler - swallows the exception whole,
          as it should. Control now passes to the "until" statement.

          WatchMe is stored in EBX by the optimiser - this should have
          been OK, except that the internal Delphi system function
          _RaiseExcept trashes EBX (and ESI, EDI) ... oops!
        }
        end
      until False;
      Inc(WatchMe)
    end
end;

begin
  TestRaise
end.
When you run this, you will be continually asked whether or not you wish to raise an exception. A counter records the number of times you press "No", and when you have said "No" enough times the program will end. If you press "Yes" then Delphi will raise an exception, catch it immediately and then keep on looping. However, with optimisation on, Delphi destroys the WatchMe variable as soon as the exception is raised! This effectively fills WatchMe with random garbage, i.e. a big number a lot greater than MaxSayNo(=5), causing the outer loop to terminate immediately.

The upshot of this is that:
a) when compiled with optimisation ON, pressing "No" will always terminate the application once you have raised at least one exception
b) when compiled with optimisation OFF, you must always press "No" MaxSayNo times to terminate the application, no matter how many exceptions you raise.
This obvious difference in behaviour proves that this is a bug.

Solution / workaround
You are at risk whenever you handle an exception in a try...except block. Once the handler has caught its exception and executed, all local variables in the registers will have been destroyed.
In other words once your routine has finished handling its exceptions, it should EXIT as soon as humanly possible unless you are ABSOLUTELY SURE that all the variables it will use are either global or on the stack.
For instance, the above program could be patched by rewriting it as follows:
{$O+}
procedure GenerateExceptions;
begin
  repeat
    try
      if MessageBox(0,'Raise an exception?',
                      'Bad Optimisation',MB_YESNO) = ID_YES then
        raise Exception.Create('Exception raised!');
      Break
    except
    {
      Catch-all exception handler - swallows the exception whole,
      as it should. Control now passes to the "until" statement.
    }
    end
  until False
end;

procedure TestRaise;
var
  WatchMe: Integer;
begin
  WatchMe := 0;
  while WatchMe < MaxSayNo do
    begin
      GenerateExceptions;
      Inc(WatchMe)
    end
end;
I cannot think of a less drastic workaround for this problem. Comments are welcome, but the only ones who can really solve this are Borland.

Index page
The Delphi Bug Lists are maintained by Reinier Sterkenburg, with help from the DeBug Team.
All feedback is appreciated. See also the feedback section of the Delphi Bug List home page.