What is type coercion and promotion in method calls?
← Advanced Type System · Ref: Q258
Type coercion in EK9 is the automatic widening of a value from one type to a compatible wider type. This happens through the promote operator (#^) and is tightly integrated with the cost-based method resolution system.
HOW PROMOTION WORKS
When you pass an Integer where a Float is expected, the compiler checks if Integer has a #^ operator that returns Float. It does, so the compiler automatically inserts the promotion call. The promoted value is then passed to the function.
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
PROMOTION IN METHOD RESOLUTION
Promotion carries a COERCION_COST of 0.5 in the method matching system. This means:
Exact match (0.0) always wins over a promoted match (0.5) Superclass match (0.05) wins over promotion Trait match (0.10) wins over promotion Promotion (0.5) wins over Any fallback (20.0)
Example: if a function has overloads for Float and Any, passing an Integer selects the Float overload (cost 0.5) over the Any overload (cost 20.0).
SINGLE PROMOTION ONLY
The compiler attempts exactly ONE promotion step. It does NOT chain promotions. If Integer promotes to Float and Float promoted to something else, the compiler would NOT go Integer to Float to that other type. It tries one step and stops.
This is a deliberate safety measure. Chained promotions in other languages are a major source of bugs:
C++: user-defined conversions chain with standard conversions JavaScript: == operator coerces through multiple steps Scala 2: implicit conversions could chain unpredictably
USER-DEFINED PROMOTION
Define #^ on your own types for custom promotion:
Measurement
operator #^ as pure
<- rtn as String: formatted text
A type can have only ONE #^ operator.
EXPLICIT PROMOTION
You can explicitly invoke promotion:
intVal <- 42 floatVal <- #^ intVal
This calls Integer's #^ operator directly.
See Q24 for type conversion overview. See Q25 for the promote operator in detail. See Q250 for fixing type mismatch errors. See Q255 for the full method resolution algorithm.
Example
defines module qa.advancedtypes.typecoercion defines function <?- Accepts a Float parameter. Integer arguments are automatically promoted. -?> processFloat() as pure -> mainValue as Float <- rtn as String: `Float: ${mainValue}` <?- Accepts a String parameter. Character arguments are automatically promoted. -?> processString() as pure -> mainValue as String <- rtn as String: `String: ${mainValue}` defines class Temperature degrees <- Float() Temperature() -> initialDegrees as Float degrees :=: initialDegrees // User-defined promotion: Temperature to Float operator #^ as pure <- rtn as Float: Float(degrees) operator $ as pure <- rtn as String: `${degrees}C` default operator ? defines program TypeCoercionDemo() stdout <- Stdout() // === BUILT-IN PROMOTION: Integer to Float === // Integer promoted to Float automatically (cost 0.5) floatResult <- processFloat(42) stdout.println(floatResult) // Float passed directly (cost 0.0, exact match) exactResult <- processFloat(3.14) stdout.println(exactResult) // === BUILT-IN PROMOTION: Character to String === charResult <- processString('Z') stdout.println(charResult) directResult <- processString("hello") stdout.println(directResult) // === USER-DEFINED PROMOTION IN CALCULATION === // Temperature promotes to Float via #^ for arithmetic boiling <- Temperature(100.0) freezing <- Temperature(0.0) // Automatic promotion: Temperature to Float in function call boilingResult <- processFloat(boiling) stdout.println(boilingResult) // Explicit promotion with #^ boilingFloat <- #^ boiling freezingFloat <- #^ freezing range <- boilingFloat - freezingFloat stdout.println(`Temperature range: ${range}`)
Common mistakes
E06270 — Renaming 'operator #^' to method 'asFloat' removes the promote operator from Temperature. The call processFloat(boiling) relied on automatic promotion from Temperature to Float via #^. Without the operator, the compiler finds processFloat(Float) but cannot match the Temperature argument, triggering E06270 parameter mismatch. For automatic type promotion to work, you MUST define 'operator #^', not a regular method. See ek9 -h E06270 for details.
Incorrect:
asFloat() as pure
Correct:
operator #^ as pure
Other ways to ask this
- How does automatic type promotion work in EK9 method calls?
- What happens when I pass an Integer where a Float is expected?
- How does the #^ operator affect method resolution?
- Why does EK9 only allow one level of type promotion?
Coming from another language?
Java: autoboxing + widening, Integer == Long compares identity not value, ternary widens unexpectedly. C++: implicit conversion sequences can chain standard + user-defined, explicit keyword to prevent. JavaScript: == coerces through multiple steps, === exists because == is unreliable. Python: mostly explicit, but __add__/NotImplemented/__radd__ chains exist. Rust: no implicit conversion at all, From/Into traits require explicit calls. Go: no implicit conversion, all conversions explicit. Kotlin: no implicit widening, explicit .toFloat() required. EK9: single-level promotion via #^ operator, COERCION_COST (0.5) in method resolution, no chaining, predictable and safe.
Keywords: migrate, cost, resolution, automatic, single, widening, promotion, implicit, type-system, advanced, method, chain, promote, coercion, type, conversion