What is the promote operator?

← Getting Started · Ref: Q25

The promote operator (#^) enables controlled, single-level type widening. It is central to how EK9 resolves method signatures and assignments when types do not match exactly.

HOW IT WORKS

When you assign a value to a variable of a different type, or pass an argument to a function expecting a different type, the compiler checks if the source type has a #^ operator that returns the target type:

  intValue <- 42
  floatResult as Float: intValue

The compiler sees Integer assigned to Float, finds Integer's #^ operator returns Float, and automatically inserts the promotion call. The same applies to function calls:

  acceptsFloat(21)

The Integer literal 21 is automatically promoted to Float because acceptsFloat expects a Float parameter.

BUILT-IN PROMOTIONS

  Integer to Float     Safe numeric widening
  Character to String  Single character to text
  Date to DateTime     Date to full timestamp
  Millisecond to Duration  Time unit widening

USER-DEFINED PROMOTIONS

You can define #^ on your own types:

  Measurement
    operator #^ as pure
      <- rtn as String: some formatted text

A type can have only ONE #^ operator. This is by design.

SINGLE PROMOTION ONLY (Critical Rule)
The compiler attempts exactly ONE promotion. It does NOT chain promotions. If Integer promotes to Float and Float promotes to something else, the compiler will NOT automatically go Integer to Float to that other type. It tries one step and stops.

This is enforced in the method matching mechanism. After promoting a type, the compiler checks if the promoted type is directly assignable to the target using uncoerced matching only. No further promotion is attempted.

COST-BASED METHOD MATCHING

When resolving which method overload to call, the compiler assigns costs:

  Exact type match:      0.0  (perfect, always preferred)
  Superclass match:      0.05 (inheritance)
  Trait match:           0.10 (interface implementation)
  Promotion match:       0.5  (via #^ operator)
  Any type match:        20.0 (universal fallback)
  No match:             -1.0  (invalid)

The method with the lowest total cost wins. Exact matches are always preferred over promoted matches. If two methods score within 0.001 of each other, the compiler reports an ambiguity error rather than guessing.

WHY OTHER LANGUAGES GET THIS WRONG

Implicit conversion chains are one of the most prolific sources of subtle bugs across programming languages.

C++: User-defined conversions can chain with standard conversions. A converting constructor plus an implicit operator can create paths the developer never intended. The 'explicit' keyword was added specifically to stop this, but developers must remember to use it.

JavaScript: The == operator performs type coercion through multiple steps: [] == false is true, '0' == false is true, but [] == '0' is false. The entire '===' operator exists solely because == coercion is so unreliable.

Scala: Implicit conversions (implicit def) can compose into chains. Scala 2's implicit resolution was so complex that even experienced developers could not predict which conversion path the compiler would choose. Scala 3 replaced this with 'given' and 'using' specifically because chained implicits were too dangerous.

Java: Autoboxing combined with widening produces surprises. Integer == Long compiles but compares object identity, not values. Ternary expressions with mixed numeric types silently widen in unexpected directions.

Python: While mostly explicit, __add__ returning NotImplemented triggers __radd__ on the other operand, which can create surprising dispatch chains with mixed types.

EK9's single-promotion rule eliminates all of these problems. One step, predictable, cost-ranked, compiler-verified. If you need more complex conversion, use an explicit constructor call. The compiler will never silently chain conversions behind your back.

See also Q24 (How do I convert between types?) for the full type conversion story including dispatcher and constructor conversion.

See Q24 for type conversion. See Q39 for integer and float. See Q242 for all conversion and introspection operators. See Q258 for type coercion and promotion in method calls. See Q552 for Date-to-DateTime promotion.

Example

defines module qa.promote.operator

  defines function

    acceptsFloat() as pure
      -> mainValue as Float
      <- rtn as Float: mainValue * 2.0

  defines class

    Measurement
      reading <- Float()

      Measurement()
        -> reading as Float
        this.reading :=: reading

      reading() as pure
        <- rtn as Float: reading

      operator #^ as pure
        <- rtn as String: `${reading}units`

      default operator ?

  defines program
    PromoteDemo()
      stdout <- Stdout()

      // Automatic promotion: Integer to Float in assignment
      intValue <- 42
      floatResult as Float: intValue
      stdout.println(`Promoted int to float: ${floatResult}`)

      // Automatic promotion: Integer to Float in function call
      doubled <- acceptsFloat(21)
      stdout.println(`Promoted in call: ${doubled}`)

      // Character to String promotion
      letter <- 'X'
      textValue as String: letter
      stdout.println(`Promoted char to string: ${textValue}`)

      // User-defined promotion
      sensor <- Measurement(98.6)
      displayText as String: sensor
      stdout.println(`Custom promote: ${displayText}`)

Common mistakes

E08180 — Class fields must be initialised inline. An uninitialised field triggers E08180. Use a constructor call like Float() or a literal value. See ek9 -h E08180 for details.

Incorrect:

reading as Float

Correct:

reading <- Float()

E07520 — The ? operator is inherited from the base type and requires 'default' or 'override'. Declaring bare 'operator ?' triggers E07520 because operator semantics require a Boolean return. See ek9 -h E07520 for details.

Incorrect:

operator ?

Correct:

default operator ?
Other ways to ask this
  • How does automatic type promotion work in EK9?
  • How does EK9 handle implicit type conversion?
  • Why does EK9 only allow one level of promotion?
  • How does the #^ operator work?

Coming from another language?

C++: implicit conversions chain (constructor + operator), 'explicit' keyword added to fix. JavaScript: == coercion chains create bizarre equality. Scala 2: implicit def chains unpredictable, replaced in Scala 3. Java: autoboxing + widening surprises (Integer == Long). Python: __add__/NotImplemented/__radd__ chains. EK9: single #^ promotion only, cost-based matching (0.0 exact, 0.05 super, 0.10 trait, 0.5 promotion), no chaining. Predictable, verifiable, safe.

Keywords: beginner, coerce, start, single, implicit, resolution, convert, promote, intro, operator, overload, first, signature, matching, migrate, chain, promotion, widening, coercion, cost