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.

Clone this wiki locally