-
Notifications
You must be signed in to change notification settings - Fork 21
Function overload resolution
This article describes how all aspects of how function overload resolution works in CQL.
Function overload resolution is the process by which a set of arguments are bound to a function's operands. When two functions exist which have the same name but differ only by its list of operands, also known as its signature, we must determine which of these functions is the best possible match.
For example, consider these two overloads:
define function Foo(x Integer)
define function Foo(x String)
define "Call with Integer": Foo(1)
define "Call with String": Foo('hello')
In the above example, x
is the operand of Foo
. In the first overload, the type of the operand is Integer; in the second, it is String.
The value 1
as it appears in Foo(1)
is an argument. This is the value that will be bound to the operand x
in Foo
.
These terms will be used throughout this article and it is critical to keep them straight. These are also the terms we use in the SDK codebase.
There are three main aspects to overload resolution:
- Computing the cost of a coercion
- Inferring the value of generic arguments
- Comparing compatible overloads to pick the best one
We will discuss each one of these in detail.
Coercion is the process of binding an argument of one type to an operand of another type. This allows usage like this:
define function TakesDecimal(x Decimal)
define CallWithInteger: TakesDecimal(1)
If CQL didn't implement automatic coercion during overload resolution, then CallWithInteger
would be an error. The author would have been required to write this instead:
define CallWithInteger: TakesDecimal(ToDecimal(1))
This would make CQL unwieldy to use. Virtually every modern language that any programmer will ever use implements argument-to-operand type coercion, so this concept should be familiar to you.
Consider this scenario:
define function Foo(x Decimal): 'decimal'
define function Foo(x Integer): 'integer'
define CallWithInteger: Foo(1)
In this example, your programming instincts will tell you that CallWithInteger
will return integer
because it will call the overload that takes an Integer
- the language will not decide to convert 1
to Decimal
implicitly, like it does in the prior example, because a "better" overload exists.
The reason Foo(x Integer)
is a better signature than Foo(x Decimal)
is because the coercion is cheaper.
CQL defines the relative cost of the coercion it supports in §4.9. Conversion Precedence.
We implement this functionality through a CoercionProvider
class, which does two things:
- Computes the cost of a conversion, returning a
CoercionCost
enum value - Creates the coercion by changing the ELM of the argument
This function has a signature of:
CoercionCost GetCoercionCost(Expression from, TypeSpecifier to)
'from' is as an ELM Expression
for the argument which we are coercing. to
is the type of the operand to which we are coercing the argument.
For example, in this ELM:
define function TakesDecimal(x Decimal)
define CallWithInteger: TakesDecimal(1)
We would be passing a Literal
expression whose value is 1 as from
, and a NamedTypeSpecifier
for the system Decimal
type as to
.
This function returns a CoercionCost
enumeration whose values are as follows:
ExactMatch = 1
Subtype = 2
Compatible = 3
Cast = 4
ImplicitToSimpleType = 5
ImplicitToClassType = 6
IntervalPromotion = 7
ListDemotion = 8
IntervalDemotion = 9
ListPromotion = 10
Incompatible = 1000
This order corresponds to the language of the specification.
The function follows these steps in order.
- Check if
from
's type is an exact match withfrom
. Iftrue
, returnExactMatch
, else continue.
Exact match compares type specifiers according to §4.3 Type Testing. In particular, it performs an Identity
check.
- If
to
is anInterval
whose point type is not valid, returnIncompatible
, else continue.
This is an undocumented rule that does not appear in the specification language, but is established through tests. Without this check, a number of the published specification tests will result in ambiguities.
It comes in this order because if from
is Interval<Any>
and to
is Interval<Any>
, this should constitute an exact match. If from
is an Interval<T>
, then the subtype check that comes immediately after this one will return true
because Interval<T>
is always a subtype of Interval<Any>
for all T
.
-
Check if
from
's type is a subtype ofto
. Iftrue
, returnSubtype
, else continue. -
Check if
from
's type isAny
. Iftrue
, returnCompatible
, else continue.
The language from the specification for Compatible
says this:
Compatible – If the invocation type is compatible with the declared type of the argument (e.g., the invocation type is Any)
The specification here uses invocation type to mean the type of the argument.
The purpose of this is primarily to allow this kind of usage:
define function TakesInteger(x Integer): x
define "Call with null": TakesInteger(null)
- Check if
from
's type can be cast toto
. Iftrue
, returnCast
, else continue.
Specifically, we check whether to
is a subtype of from
. For example, the conversion from Any
to Integer
requires a cast.
We also consider list element types and interval point types here, allowing List<Any>
to be cast to List<Integer>
and Interval<Any>
to be cast to Interval<Integer>
.
We also check that when from
is a Choice
type:
- If
to
is also aChoice
type, then there must be a pair(typeInFrom, typeInTo)
wheretypeInFrom
can be cast totypeInTo
- Else, there must be a pair
(typeInFrom, to)
wheretypeInFrom
can be cast toto
.
Otherwise, if to
is a Choice
type, then there must be a pair (from, typeInTo)
where from
can be cast to typeInTo
.
- Check if
from
can be implicitly cast toto
. Iftrue
, then ifto
is a simple type, returnImplicitToSimpleType
, els returnImplicitToClassType
, else continue.
Implicit conversions are defined in §4.6.3. The determination of whether a type is a simple type or a class type is based on how it is declared in the model information.
The system types that are class types are:
- Quantity
- Ratio
- Code
- Concept
- Vocabulary
- ValueSet
- CodeSystem
All other types are simple types.
- If
EnableIntervalPromotion
is configured,to
is anInterval
type, andfrom
can be promoted, returnIntervalPromotion
, else continue.
An interval can be promoted if the cost of converting from
to the point type of to
is not Incompatible
.
- If
EnableListDemotion
is configured,from
is anList
type and can be demoted, returnListDemotion
, else continue.
A list can be demoted if the cost of converting its element type to to
is not Incompatible
.
- If
EnableIntervalDemotion
is configured,from
is anInterval
type, andfrom
can be demoted, returnIntervalDemotion
, else continue.
An interval can be demoted if the cost of converting its point type to to
is not Incompatible
.
- If
EnableListPromotion
is configured,to
is aList
type, andfrom
can be promoted, returnListPromotion
, else continue.
A type can be promoted to a list if the cost of converting from
to the element type of to
is not Incompatible
.
- Return
Incompatible
.
If none of the options for coercing from
to to
result in success, then from
cannot be coerced to to
.