Menu ▾ ▴

#41 6.4.2 `round`, `format` and a derived default clamped a float outside the integer range

CLIPS
open
nobody
None
5
2 days ago
2 days ago
Batch Job 1
No

round, format and a derived default clamped a float outside the integer range

What happened. Three places cast a float to a CLIPS integer without asking whether there was one. All three returned the nearest end of the integer range instead.

(printout t "round: " (round 1e20) crlf)
(printout t "format: " (format nil "%d" 1e20) crlf)
(deftemplate ranged (slot amount (type NUMBER) (range 1e20 1e21)))
(assert (ranged))
(facts)

printed

round: 9223372036854775807
format: 9223372036854775807
f-1     (ranged (amount 9223372036854775807))

The slot in that fact declares a range of 1e20 to 1e21. 9223372036854775807 is about 9.2e18, which is below the minimum, and writing it into that slot explicitly is rejected:

[CSTRNCHK1] A literal slot value found in the 'assert' command does not fall
in the allowed range 1e+20 to 1e+21 for slot 'amount'.

So CLIPS refused the value when a program asked for it and supplied the same value itself.

Why it is a fix and not a preference. Section 12.5.22 of the Basic Programming Guide says round "rounds its only argument ... toward the closest integer" and that "the return value of this function is an integer". The closest integer to 1e20 is 100000000000000000000, and 9223372036854775807 is not it.

Section 12.4.6 says of format that the d conversion "displays a long long integer"; o, x and u display the same value in other bases. 1e20 is not a long long and the digits printed were a different number.

Section 11.5 lists the rules for deriving a default value. Rule 1 chooses the type, and INTEGER comes before FLOAT; rule 3 says that "if the default type is INTEGER or FLOAT and the range attribute is specified, then the minimum range value is used as the default value if it is not ?VARIABLE, otherwise, the maximum range value is used"; rule 4 gives the fallback, "0 for type INTEGER". 9223372036854775807 is not the minimum range value, not the maximum, and not the rule 4 fallback.

Where it came from. Three casts. RoundFunction in emathfun.c:

   if (CVIsType(returnValue,FLOAT_BIT))
     { returnValue->integerValue = CreateInteger(theEnv,(long long) round(returnValue->floatValue->contents)); }

PrintFormatFlag in iofun.c, on the d, o, x and u branch:

        if (theResult.header->type == FLOAT_TYPE)
          { gensprintf(printBuffer,formatString,(long long) theResult.floatValue->contents); }

FindDefaultValue in default.c, twice — once for the minimum range bound and once for the maximum:

      else if (theConstraints->minValue->type == FLOAT_TYPE)
        { return(CreateInteger(theEnv,(long long) theConstraints->minValue->floatValue->contents)); }

A float-to-integer conversion in C has no result when the integral part does not fit the destination. What came back was whatever the host produced, which here was the end of the range.

The change. All four casts ask FloatFitsInInteger first. That function is in evaluatn.c and came with the fix in the sibling directory for integer and div, which guards CVCoerceToInteger with it; these three places cast without going through that macro, so each needed the test added at its own site.

round and format report through ArgumentOverUnderflowErrorMessage and set the evaluation error, as div and mod do for an integer result they cannot represent. round tests the rounded value, because that is what gets cast. format reports before building the conversion string and returns NULL, which is how PrintFormatFlag already signals a bad argument to FormatFunction.

FindDefaultValue has no call to report against — it is deriving a slot default, not evaluating a user's expression — so a bound with no integer simply stops being a candidate and the next one is tried. For (range -1e20 100) that means the integer maximum 100 supplies the default, where the cast of the minimum used to give −9223372036854775808. For (range 1e20 1e21) neither bound has an integer, the candidates run out, and standardDefault applies: rule 4's 0 for type INTEGER.

0 is outside that slot's declared range, as 9223372036854775807 was. The difference is that 0 is the value section 11.5 names for an integer slot with no usable bound, and 9223372036854775807 is named nowhere. A range whose every value needs a float cannot be satisfied by an integer default at all, and that follows from rule 1's type precedence rather than from any of these casts.

What was checked. For round: (round 1e20), its negative, 2⁶³ exactly — one step past the largest long long — and an infinity. All four report, none prints a value, and a call after them still evaluates. Then the arguments that must still round: the example the guide gives, a half rounding away from zero in both directions, an integer argument returned unchanged, 9223372036854774784 — the largest double inside the range — and −9223372036854775808 as a double, which is exact.

For format: one call for each of %d, %x, %o and %u with a float that has no integer. All four report and none prints digits. Then %d on an integer, %d and %x on floats that truncate, %d on the largest double in range, and %f on 1e20, which takes no integer cast and still prints 100000000000000000000.000000.

For the derived default: four templates with (type NUMBER), so INTEGER is the derived type in each. (range -1e20 100) takes the maximum, (range 1e20 1e21) reaches rule 4, (range 2.5 100.5) and (range 5 100) have a bound that converts and are unchanged at 2 and 5. Neither clamped value appears in any fact. The same file then asserts 9223372036854775807 into the (range 1e20 1e21) slot explicitly, which CSTRNCHK1 rejects, so the fact count stays at four.

The reason to care about this one: the derived default is the case a program never writes down, so the value that reached the slot was one CLIPS would have refused from the program in the same breath.

Present in 7.0? Not checked.

5 Attachments

Discussion

Anonymous
Anonymous

Add attachments
Cancel