9. Operations

  1. 9.1. VARIETY and overflow
  2. 9.2. ERROR_TREATMENT
  3. 9.3. Division and remainder
  4. 9.4. change_variety
  5. 9.5. and, or, not, xor
  6. 9.6. Floating-point operations, ROUNDING_MODE
  7. 9.7. change_bitfield_to_int, change_int_to_bitfield
  8. 9.8. make_compound, make_nof, n_copies

Most of the arithmetic operations of TDF have familiar analogues in standard languages and processors. They differ principally in how error conditions (e.g. numeric overflow) are handled. There is a wide diversity in error handling in both languages and processors, so TDF tries to reduce it to the simplest primitive level compatible with their desired operation in languages and their implementation on processors. Before delving into the details of error handling, it is worthwhile revisiting the SHAPEs and ranges in arithmetic VARIETYs.

9.1. VARIETY and overflow

An INTEGER VARIETY, for example, is defined by some range of signed natural numbers. A translator will fit this range into some possibly larger range which is convenient for the processor in question. For example, the integers with variety(1,10) would probably be represented as unsigned characters with range (0..255), a convenient representation for both storage and arithmetic.

The question then arises of what is meant by overflow in an operation which is meant to deliver an integer of this VARIETY - is it when the integer result is outside the range (1..10) or outside the range (0..255)? For purely pragmatic reasons, TDF chooses the latter - the result is overflowed when it is outside its representational range (0..255). If the program insists that it must be within (1..10), then it can always test for it. If the program uses the error handling mechanism and the result is outside (1..10) but still within the representational limits, then, in order for the program to be portable, then the error handling actions must in some sense be "continuous" with the normal action. This would not be the case if, for example, the value was used to index an array with bounds (1..10), but will usually be the case where the value is used in further arithmetic operations which have similar error handling. The arithmetic will continue to give the mathematically correct result provided the representational bounds are not exceeded.

The limits in a VARIETY are there to provide a guide to its representation, and not to give hard limits to its possible values. This choice is consistent with the general TDF philosophy of how exceptions are to be treated. If, for example, one wishes to do array-bound checking, then it must be done by explicit tests on the indices and jumping to some exception action if they fail. Similarly, explicit tests can be made on an integer value, provided its representational limits are not exceeded. It is unlikely that a translator could produce any more efficient code, in general, if the tests were implicit. The representational limits can be exceeded in arithmetic operations, so facilities are provided to either to ignore it, to allow one to jump to a label, or to obey a TDF exception handler if it happens.

9.2. ERROR_TREATMENT

Taking integer addition as an example, plus has signature:

ov_err: ERROR_TREATMENT
arg1:   EXP INTEGER(v)
arg2:   EXP INTEGER(v)
        -> EXP INTEGER(v)

The result of the addition has the same integer VARIETY as its parameters. If the representational bounds of v are exceeded, then the action taken depends on the ERROR_TREATMENT ov_err.

The ERROR_TREATMENT, impossible, is an assertion by the producer that overflow will not occur; on its head be it if it does.

The ERROR_TREATMENTS continue and wrap give "fixup" values for the result. For continue the fixup value is undefined. For wrap, the the answer will be modulo 2 to the power of the number of bits in the representational variety.Thus, integer arithmetic with byte representational variety is done modulo 256. This just corresponds to what happens in most processors and, incidentally, the definition of C.

The ERROR_TREATMENT that one would use if one wished to jump to a label is error_jump:

lab: LABEL
     -> ERROR_TREATMENT

A branch to lab will occur if the result overflows.

The ERROR_TREATMENT, trap(overflow) will raise a TDF exception(see section 6.3) with ERROR_CODE overflow if overflow occurs.

9.3. Division and remainder

The various constructors in involving integer division (e.g. div1, rem1) have two ERROR_TREATMENT parameters, one for overflow and one for divide-by-zero e.g. div1 is:

div_by_zero_error: ERROR_TREATMENT
ov_err:            ERROR_TREATMENT
arg1:              EXP INTEGER(v)
arg2:              EXP INTEGER(v)
                   -> EXP INTEGER(v)

There are two different kinds of division operators (with corresponding remainder operators) defined. The operators div2 and rem2 are those generally implemented directly by processor instructions giving the sign of the remainder the same as the sign of the quotient. The other pair, div1 and rem1, is less commonly implemented in hardware, but have rather more consistent mathematical properties; here the sign of remainder is the same as the sign of divisor. Thus, div1(x, 2) is the same as shift_right(x, 1) which is only true for div2 if x is positive. The two pairs of operations give the same results if both operands have the same sign. The constructors div0 and rem0 allow the translator to choose whichever of the two forms of division is convenient - the producer is saying that he does not care which is used, as long as they are pairwise consistent. The precise definition of the divide operations is given in ().

9.4. change_variety

Conversions between the various INTEGER varieties are provided for by change_variety:

ov_err: ERROR_TREATMENT
r:      VARIETY
arg1:   EXP INTEGER(v)
        -> EXP INTEGER(r)

If the value arg1 is outside the limits of the representational variety of r, then the ERROR_TREATMENT ov_err will be invoked.

9.5. and, or, not, xor

The standard logical operations, and, not, or and xor are provided for all integer varieties. Since integer varieties are defined to be represented in twos-complement the result of these operations are well defined.

9.6. Floating-point operations, ROUNDING_MODE

All of the floating-point (including complex) operations include ERROR-TREATMENTs. If the result of a floating-point operation cannot be represented in the desired FLOATING_VARIETY, the error treatment is invoked. If the ERROR_TREATMENT is wrap or impossible, the result is undefined; otherwise the jump operates in the same way as for integer operations. Both floating_plus and floating_mult are defined as n-ary operations. In general, floating addition and multiplication are not associative, but a producer may not care about the order in which they are to be performed. Making them appear as though they were associative allows the translator to choose an order which is convenient to the hardware.

Conversions from integer to floating are done by float_int and from floating to integers by round_with_mode . This latter constructor has a parameter of SORT ROUNDING_MODE which effectively gives the IEEE rounding mode to be applied to the float to produce its integer result.

One can extract the real and imaginary parts of a complex FLOATING using real_part and imaginary_part. A complex FLOATING can be constructed using make_complex. Normal complex arithmetic applies to all the other FLOATING constructors except for those explicitly excluded (eg floating_abs, floating_max etc.)

9.7. change_bitfield_to_int, change_int_to_bitfield

There are two bit-field operation, change_bitfield_to_int and change_int_to_bitfield to transform between bit-fields and integers. If the varieties do not fit the result is undefined; the producer can always get it right.

9.8. make_compound, make_nof, n_copies

There is one operation to make values of COMPOUND SHAPE, make_compound:

arg1: EXP OFFSET(base, y)
arg2: LIST(EXP)
      -> EXP COMPOUND(sz)

The OFFSET arg1 is evaluated as a translate-time constant to give sz, the size of the compound object. The EXPs of arg2 are alternately OFFSETs (also translate-time constants) and values which will be placed at those offsets. This constructor is used to construct values given by structure displays; in C, these only occur with constant val[i] in global definitions. It is also used to provide union injectors; here sz would be the size of the union and the list would probably two elements with the first being an offset_zero.

Constant sized array values may be constructed using make_nof, make_nof_int, and n_copies. Again, they only occur in C as constants in global definitions.