Turbo Pascal's 64-bit comp Type

Saturday, July 29, 2006
By: Jason Doucette

Back in the day, the comp data type in Turbo Pascal, a 16-bit compiler (which compiled 8-bit code by default), allowed you to store 64-bit signed integer values.  But it was a very strange type.  It was not considered an integer.  It was considered a 'real' (floating point) type by the language:

You could not increment or decrement it with the normal integer increment decrement functions, Inc() and Dec() (the equivalent to the postfix increment and decrement operators in C / C++: ++ and --).  You had to deal with comp as a floating point value in computations.  Thus, the following is required to increment / decrement it: x = x + 1.0; and  x = x - 1.0;  If you printed the value to the screen with WriteLn() it would be displayed as a floating point value.  It also had a curious range of -(2^63)+1 to +(2^63)-1 (-9,223,372,036,854,775,807 to 9,223,372,036,854,775,807) such that the magnitude of the lower and upper bounds are equal.  We would expect an 8-byte signed integer to have the range of -2^63 to +2^63-1 (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807), just as a 2-byte signed integer would store -32,768 to 32,767 (there is one less possible positive value than there are negative values because zero is stored in the 'positive half').  Who concocted such a strange, mongrel data type?


Why Is comp Considered A Floating Point?

The key lies in how floating point values are stored.  A floating point number stores three components: a sign, an exponent, and a mantissa.  The value that the floating point bits represent is as follows:

value = sign * mantissa * 2 ^ exponent

What this implies, but may not be immediately obvious, is that many integers can be stored with full accuracy in a floating point representation.  Any integer that does not require more precision than what the mantissa can store can be represented exactly.  The IEEE 80-bit floating point type stores 64-bits, plus the implied 1 bit, for a total of 65-bits for the mantissa.  Thus, it could can all possible 64-bit integer values.  So, was the comp type actually an 80-bit floating point type?

This would explain the strange range limits of -(2^63)+1 to +(2^63)-1 (-9,223,372,036,854,775,807 to 9,223,372,036,854,775,807), since floating point values store the sign bit separately from the magnitude data.  Thus, the smallest possible negative value always has the same magnitude as the largest possible positive value.


How Large Is comp?

But, if it were an 80-bit floating point type, it would take up 10 bytes of memory, instead of only 8.  I have Turbo Pascal kicking around on my hard drive, so let's see if it does.  I made the following program:

program comp_type;
  x: comp;
  a: array[1..1000] of comp;

  x := 1.0;
  writeln('x = ',x);
  writeln('sizeof(x) = ',sizeof(x),' bytes.');
  writeln('sizeof(a) = ',sizeof(a),' bytes.');

This produces the following output:

x =  1.00000000000000E+0000
sizeof(x) = 8 bytes.
sizeof(a) = 8000 bytes.

Hmm... so it is only stored in 8 bytes, not 10.  Well, this makes sense.  If we are only ever storing 64-bits of information, we should only use 64 bits to store it in, not 80 bits.  When the time is needed to perform computations on this data, we can convert it into the 80-bit floating point type.  So, using comp means it would be slower than expected due to this.  But, I guess this should come at no surprise, considering you cannot use normal integer operations on the type.  After all, the comp type is defined as a real (i.e. floating point type) in the Turbo Pascal manuals.


What Integers Can 80-bit Floating Point Store?

The largest fully represented integer (such that every bit of the integer is accounted for by the mantissa) the 80-bit floating point type can store has the following attributes:  The mantissa, in binary, is composed entirely with 1's.  Recalling that there is no implied 1 bit for the 80-bit floating point, this dictates 64 1's, not 65.  And the exponent is just high enough such that the last bit of the mantissa represents the least significant bit of the integer (the 1's place) we are trying to represent.  This state occurs when the exponent = 63.  So, we have:

1.111111111111111111111111111111111111111111111111111111111111111b * 2 ^ 63 = 18,446,744,073,709,551,615

There should be exactly 64 1's in the first number above.  The postfix "b" means the number is binary.  Without the "b", the number is decimal.  The result is shown in decimal.  You can type this equation, exactly as-is (everything before the equals sign) into Microsoft's PowerToy Calculator.  Make sure you have the precision set high enough so the program will maintain enough digits of accuracy, as "Low Precision (32)" is not enough.  You can change it by going into the View menu -> Advanced Options.  I recommend "High Precision (128)", since "Medium Precision (64)" is just barely at the limit of the accuracy we need.


Why Can't comp Store -2^63?

The result above, 18,446,744,073,709,551,615, is equivalent to 2^64-1.  Thus, the 80-bit floating point format can store integers in full accuracy from -2^64-1 to +2^64-1.  This is more than enough to store the standard range of a normal signed 64-bit integer (-2^63 to +2^63-1).  So, why does comp only store from -(2^63)+1 to 2^63-1?  What is going on here?

I have no idea.  Let's try and force it to be stored with a Turbo Pascal program.  The following program sets a comp data type to be 2^63-1, which is the maximum signed 64-bit integer which comp can store.  It negates it, and stores -2^63+1, which is supposedly the lowest value comp can store, but it one higher than the smallest value a signed 64-bit integer can store.  We then subtract one from this to get -2^63, and see what happens...

program comp_type_2;
  x: comp;
  e: extended;
  i: integer;

procedure showbits(x: comp);
  pb: ^byte;
  i,j: integer;
  pb := @x;
  for i := 7 downto 0 do
    for j := 7 downto 0 do
      write(ord(pb^ and (1 shl j) <> 0));

  e := 1.0;
  for i := 1 to 63 do
    e := e * 2.0;
  e := e - 1.0; {e = 2^63 - 1 = maximum signed 64-bit integer}
  x := e; {2^63-1}
  x := -e; {-2^63+1}
  x := x - 1.0; {-2^63}
  {x := x - 1.0; {uncomment to cause: Error 207: Invalid floating point operation}

The output is as follows, which clearly indicates that the values are stored in two's complement format, as are almost all integers:

0111111111111111111111111111111111111111111111111111111111111111 (+2^63-1)
1000000000000000000000000000000000000000000000000000000000000001 (-2^63+1)
1000000000000000000000000000000000000000000000000000000000000000 (-2^63)

comp could store -2^63, despite the restriction mentioned in the Turbo Pascal help file.  To ensure this is the case, if you uncomment out the second subtraction, it will reveal an error: "Error 207: Invalid floating point operation"

I think the original design of comp was made knowing that floating point values can store both negative and positive values of the same magnitude, and it was thought this would be a restriction in the comp storage.  It was unknown at this time that the 80-bit floating point type could easily store all possible signed 64-bit integer values, and this mistake continued forward into the Turbo Pascal help file.  It is just a simple error.


Additional Information



About the Author: I am Jason Doucette of Xona Games, an award-winning indie game studio that I founded with my twin brother. We make intensified arcade-style retro games. Our business, our games, our technology, and we as competitive gamers have won prestigious awards and received worldwide press. Our business has won $190,000 in contests. Our games have ranked from #1 in Canada to #1 in Japan, have become #1 best sellers in multiple countries, have won game contests, and have held 3 of the top 5 rated spots in Japan of all Xbox LIVE indie games. Our game engines have been awarded for technical excellence. And we, the developers, have placed #1 in competitive gaming competitions -- relating to the games we make. Read about our story, our awards, our games, and view our blog.