Cause of Range Check Error (Delphi)
Asked Answered
D

2

12

Here's a condensed version of some code that causes both a Range check error and an overflow error, should I turn on those compiler check directives. I understand why this would cause an overflow, on the multiplication of C1, it seems likely it might exceed the data-type's max valude. But why would this also trigger a Range-check error? Delphi's documentation and other posts on stack overflow make it sound like range-check errors are usually for array accesses that are out of bounds. But I'm not accessing an array on the line it's saying is causing the range-check error. Perhaps its on the assignment to param1? But why would that be a range-check and not an overflow error, if so?

const
  C1 = 44001;
  C2 = 17999;

function fxnName(..other params...; param1: Word): String;
var
  someByte: byte;
begin
  // some code
  // by now we're in a loop. the following line is where it breaks to in the debugger: 
  param1 := (someByte + param1) * C1 + C2;
  // more code
end;

If it's relevant, when it breaks on that line in the debugger, all the values look as expected, except param1, which shows "Undeclared identifier: 'param1'" when I ask Delphi to evaluate it.

Dynamometry answered 25/7, 2012 at 20:58 Comment(1)
I'd suspect it's just because the range check occurs before the overflow check, and once the range check exception is raised the overflow check never occurs. (Not sure of the order, so it's just a suspicion.)Sedimentation
C
22

The documents about range-checking states:

The $R directive enables or disables the generation of range-checking code. In the {$R+} state, all array and string-indexing expressions are verified as being within the defined bounds, and all assignments to scalar and subrange variables are checked to be within range. If a range check fails, an ERangeError exception is raised (or the program is terminated if exception handling is not enabled).

So the reason here is the assignment to a scalar value, which is handed a value that has passed the upper range.

See also docwiki Simple Types about range-checking errors on simple types and subrange types.

Example:

{$R+} // Range check on
var
  w1,w2 : word;
begin
  w1 := High(word);
  w1 := w1 + 10; // causes range-check error on assignment to w1 (upper range passed)
  w2 := 0;
  w2 := w2 - 10; // causes range-check error on assignment to w2 (lower range passed)
end;

A summary test of all combinations of $R and $Q for all platform-independent integer types:

            R+Q+  R+Q-  R-Q+
 ShortInt    R     R     x
 SmallInt    R     R     x
 Integer     O     x     O
 LongInt     O     x     O
 Int64       O     x     O
 Byte        R     R     x
 Word        R     R     x
 LongWord    O     x     O
 Cardinal    O     x     O
 UInt64      O     x     O

R=Range error; O=Overflow error; x=Nothing

And the test was(pseudo-code) with XE2 in 32-bit mode:

number := High(TNumber);
number := number + 1;
Conspire answered 25/7, 2012 at 21:22 Comment(4)
@DavidHeffernan, read the docs, just tested the code and it does generate range-check errors. You must read after the , namely where it states that all assignments to scalars and subrange variables are checked.Conspire
Presumably it would be overflow error if the data type was integerHarville
@DavidHeffernan If the calculation is performed on Integer types, sure, but i := i + Int64(1); should (untested) cause a range error when i is an Integer with a value of MaxInt.Hoyos
Nice table! To sum up, type < 32 bytes -> range error, type => 32 bytes -> overflow error. Pretty weird..Vietcong
F
0

I have seen too many Delphi programmers writing quite large programs without ever activating Range, Overflow and Assertion checking. Of course, you can do that if you want, but your code will be more buggy.

So, if you allow me let me insert a parallel (bug still relevant) and to your question, in the hope to convince more programmers to enable these 3 checking right now. A word of warning also included at the end.


Overflow checking

This will check certain integer arithmetic operations (+, -, *, Abs, Sqr, Succ, Pred, Inc, and Dec) for overflow. For example, after a + (addition) operation the compiler will insert additional binary code that verifies that the result of the operation is within the supported range.

An "integer overflow" occurs when an operation on an integer variable produces a result that is outside the range of that variable. For example, if an integer variable is declared as a 16-bit signed integer, its value can range from -32768 to 32767. If an operation on this variable produces a result greater than 32767 or less than -32768, an integer overflow has occurred.

When an integer overflow occurs, the result of the operation is undefined and can lead to undefined behavior in the program: • Wrap-around The result might result in a wrapped-around value. This means that the number 32768 will be actually stored as 1 since it is 1 unit higher than the highest value we can store (32767). • Truncation The result may be truncated or otherwise modified to fit within the range of the integer type. For example, the number 32768 will be actually stored as 32767 since that is the highest value we can store.

Undefined program behavior is one of the worst kind of errors, because it is not an error easy to reproduce. Therefore, it is difficult to track and repair.

There is a small price to pay if you activate this: the speed of the program will decrease slightly.

IO checking

Checks the result of an I/O operation. If an I/O operation fails, an exception is raised. If this switch is off, we must check for I/O errors manually. There is a minor price to pay if you activate this: the speed of the program will decrease, but insignificantly because the few microseconds introduced by this check is nothing compared with the millisecond-range time required by the I/O operation itself (the hard drives are slow).

Range Checking

The Delphi Geek calls this “The most important Delphi setting” and I totally agree. It checks if all array and string indexing expressions are within the defined bounds. It also checks that all assignments to scalar and subrange variables are within range.

Here is an example of code that would ruin our life if Range Checking would not be available:

Type 
    Pasword= array [1..10] of byte; // we define an array of 10 elements
…
x:= Pasword[20];       // Range Checking will prevent the program from accessing element 20 (ERangecheckError exception is raised). Security breach avoided. Error log automatically sent to the programmer. Bruce Willis saves everyone.

Enabling Runtime Error Checking

To activate the Runtime Error Checking, go to Project Options and check these three boxes:

Enabling the Runtime Error Checking in ‘Project Options’ enter image description here

Assertions

A good programmer MUST use assertions in its code to increase the quality and stability of the program. Seriously man! You really need to use them.

Assertions are used to check for conditions that should always be true at a certain point in the program, and to raise an exception if the condition is not met. The Assert procedure, which is defined in the SysUtils unit, is typically used to perform assertions.

You can think of assertions as poor man’s unit testing. I strongly advise you to look deeper into assertions. They are very useful and do not require as much work as unit testing.

Typical example:

SysUtils.Assert(Input <> Nil, ‘The input should not be nil!’);

But for the program to check our assertions, we need to active this feature in the Project Settings -> Compiler Options, otherwise they will simply be ignored, like they are not there in our code. Make sure that you understand the implications of what I just said! For example, we screw up badly if we call routines that have side effects in the Assert. In the example below, during Debugging when the assertions are on, the Test() function will be executed and 'This was executed' will appear in the Memo. However, during Release more, that text will not appear in the memo because now Assert is simply ignored. Congratulations we just made the program to behave differently in debug/release mode ☹.

function TMainForm.Test: Boolean;
begin
 Result:= FALSE;
 mmo.Lines.Add('This was executed');
end;

procedure TMainForm.Start;
VAR x: Integer;
begin
 x:= 0;
 if x= 0
 then Assert(Test(), 'nope');
end;

Here are a few examples of how it can be used:

1 To check if an input parameter is within the 0..100 range:

procedure DoSomething(value: Integer);
begin
  Assert((value >= 0) and (value <= 100), 'Value out of range');
  …
end;

2 To check if a pointer is not nil before using it:

Var p: Pointer;
Begin
  p := GetPointer;
  Assert(Assigned(p), 'Pointer is nil');
   …
End;

3 To check if a variable has a certain value before proceeding:

var i: Integer;
begin
   i := GetValue;
   Assert(i = 42, 'Incorrect response to “What is the answer to life”!');
  …
end;

Assertions can also be disabled by defining the NDEBUG symbol in the project options or by using the {$D-} compiler directives.

We can also use the Assert as a more elegant way of handling errors and exceptions in some cases, as it can be more readable and it also includes a custom message, that would help the developer understand what went wrong.

Personally, I use it a lot at the top of my routines to check if the parameters are valid.

Activating this feature will (naturally) make your program slower because… well, there is one extra line of code to execute.

Nothing comes for free

Everything nice comes with a price (fortunately a small price in our case): enabling Runtime error checking and Assertions slows down our program and makes it somewhat larger.

Computers today have lots of RAM so the slight increase in size is irrelevant, so, let’s put that aside. But let’s look at the speed, because that is not something we can easily ignore:

Type                 Disabled   Enabled
Range checking       73ms         120ms
Overflow checking    580ms        680ms
I/O checking         Not tested   Not tested

As we can see the program's speed is strongly impacted by these runtime checking. If we have a program where speed is critical, we better activate “Runtime error checking” during debugging only. What I do, I also leave it active in the first release and wait a few weeks. If no bugs are reported, then I release an update in which “Runtime error checking” is off.

Personally, I leave the “IO checking” always active. The performance hit because of this check is microscopic.


Big warning:
If you have an existing project that was not so nicely written, and you activate any of the Runtime Error checking below, your program may will crash more often than usual. No, the Runtime Error checking routines did not break your program. It was always broken – you just didn’t know. The Runtime Checking routines are now finding all those places where the code is fishy and shitty and smelly. The sole purpose of Runtime Checking is to find bugs in your program.

Folkestone answered 27/2, 2023 at 9:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.