Skip to content

Function overload resolution

Evan Machusak edited this page Mar 8, 2024 · 10 revisions

This article describes how all aspects of how function overload resolution works in CQL.

Intro

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.

Algorithm

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 cost

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

Coercion cost algorithm

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.

  1. Check if from's type is an exact match with from. If true, return ExactMatch, else continue.

Exact match compares type specifiers according to §4.3 Type Testing. In particular, it performs an Identity check.

  1. If to is an Interval whose point type is not valid, return Incompatible, 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.

  1. Check if from's type is a subtype of to. If true, return Subtype, else continue.

  2. Check if from's type is Any. If true, return Compatible, 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)
  1. Check if from's type can be cast to to. If true, return Cast, 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 a Choice type, then there must be a pair (typeInFrom, typeInTo) where typeInFrom can be cast to typeInTo
  • Else, there must be a pair (typeInFrom, to) where typeInFrom can be cast to to.

Otherwise, if to is a Choice type, then there must be a pair (from, typeInTo) where from can be cast to typeInTo.

  1. Check if from can be implicitly cast to to. If true, then if to is a simple type, return ImplicitToSimpleType, els return ImplicitToClassType, 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.

Clone this wiki locally