Why is 'Ratio as Float constrain as >= 1' rejected with E08265?

← Advanced Type System · Ref: Q1391

A constraint's literal must match the parameter type of the operator it is compared with EXACTLY. A constraint is never converted - there is no promotion inside one.

THE RULE

  Ratio as Float constrain as
    >= 1 and <= 100          // E08265: Float's '>=' takes a Float, not an Integer
  Ratio as Float constrain as
    >= 1.0 and <= 100.0      // correct

The same holds for every comparison operator and every promotable pair: a Character does not become a String either, so a String constraint writes <> "x", not <> 'x'.

WHY A DECLARATION IS EXACT

In an ordinary expression, ratio >= 1 promotes the Integer to a Float and works. A constraint is different: it is a DECLARATION of the type's admissible values, and the check the compiler generates compares the value with the literal exactly as written. Before E08265 existed, '>= 1' on a Float compiled and then called a Float comparison taking an Integer that does not exist - it failed at the FIRST construction, on the JVM and natively. Rather than hide a conversion inside a declaration, EK9 makes the declaration say exactly what it means.

TWO REMEDIES

1. Built-in base (Float, String, ...): you cannot add operators to a built-in type, so write the literal in the operator's type - 1 becomes 1.0, 'x' becomes "x". The error message gives the respelling.
2. User-defined base: you have a choice. Either write the literal in the type the existing operator takes, or ADD the operator that takes the literal's type. The example below gives Price both 'operator >= -> threshold as Float' and 'operator >= -> threshold as Integer', so its constraint can use whole-number bounds directly.

WHAT IS NOT AFFECTED

Comparing a constrained VALUE with a base value at run time (age >= limit) is an ordinary expression and follows the ordinary rules - see Q717. Only the literal written inside 'constrain as' must match exactly.

See Q257 for constrained types and Q1376 for constraints that no value can satisfy. Use 'ek9 -h E08265' for the full error explanation.

Example

defines module qa.constraint.literal.exact

  defines record

    //A user-defined base. It declares '>=' and '<=' for Integer as well as Float, so a constraint
    //written with whole-number bounds finds an operator taking exactly that type.
    Price
      amount as Float?

      Price()
        amount: Float()

      Price()
        -> initial as Float
        amount: initial

      Price()
        -> other as Price
        amount: other.amount

      operator >= as pure
        -> threshold as Float
        <- rtn as Boolean: amount >= threshold

      operator >= as pure
        -> threshold as Integer
        <- rtn as Boolean: amount >= #^threshold

      operator <= as pure
        -> threshold as Float
        <- rtn as Boolean: amount <= threshold

      operator <= as pure
        -> threshold as Integer
        <- rtn as Boolean: amount <= #^threshold

      override operator ? as pure
        <- rtn as Boolean: amount?

      operator :=:
        -> other as Price
        amount :=: other.amount

  defines type

    //Built-in base: the bounds are written as Floats because Float's operators take a Float.
    Ratio as Float constrain as
      >= 1.0 and <= 100.0

    //Built-in base: a String's '<>' takes a String, so the bound is "x", not the Character 'x'.
    NotX as String constrain as
      <> "x"

    //User-defined base: Integer bounds work because Price declares the Integer operators.
    Bounded as Price constrain as
      >= 1 and <= 100

  defines program

    ConstraintLiteralExactType()
      stdout <- Stdout()

      stdout.println(`ratio 50.5 accepted: ${Ratio().of(50.5)?}`)
      stdout.println(`ratio 0.5 accepted: ${Ratio().of(0.5)?}`)

      stdout.println(`"y" accepted: ${NotX().of("y")?}`)
      stdout.println(`"x" accepted: ${NotX().of("x")?}`)

      stdout.println(`price 20.0 accepted: ${Bounded().of(Price(20.0))?}`)
      stdout.println(`price 250.0 accepted: ${Bounded().of(Price(250.0))?}`)

Common mistakes

E08265 — Float's comparison operators take a Float. A constraint never promotes its literal, so an Integer bound is rejected; write it as a Float.

Incorrect:

    Ratio as Float constrain as
      >= 1 and <= 100

Correct:

    Ratio as Float constrain as
      >= 1.0 and <= 100.0

E08265 — 'x' is a Character and String's '<>' takes a String. A Character is not converted inside a constraint; write the bound as a String literal.

Incorrect:

    NotX as String constrain as
      <> 'x'

Correct:

    NotX as String constrain as
      <> "x"

E08265 — For a user-defined base you may add the operator that takes the literal's type - or write the bound as a Float to use the existing operator.

Incorrect:

      //no 'operator >= -> threshold as Integer' - so Bounded's '>= 1' only reaches the Float operator by promotion

Correct:

      operator >= as pure
        -> threshold as Integer
        <- rtn as Boolean: amount >= #^threshold
Other ways to ask this
  • Why must a Float constraint use 1.0 instead of 1?
  • Does EK9 promote an Integer literal inside a constraint?
  • How do I constrain a user-defined type with whole-number bounds?
  • Why can't a String constraint compare with a Character like 'x'?

Coming from another language?

Java Bean Validation annotations such as @Min(1) and @DecimalMin("1") accept a bound in whatever form the annotation takes and convert it silently; a hand-written check widens int to double invisibly. Kotlin and C# likewise widen numeric literals in comparisons. EK9 applies ordinary promotion in expressions, but a constrained type's declaration must state its bounds in the compared type, so the declared constraint and the checked constraint are the same thing.

Keywords: promotion, E08265, coercion, exact, constrain, float, literal, operator, constraint, integer