Narrowing › overloaded functions. The names in the examples are those of section 2.
FEEL has no user-defined overloading: a name has one value. "Overloaded, with separate types" happens in three places, and this page goes through what narrowing does in each:
- A built-in with several signatures:
before(point, point),before(point, range),before(range, point)andbefore(range, range)are four signatures of one function;abs(number),abs(days and time duration)andabs(years and months duration)are three of another. - A built-in with one signature whose parameter takes several types:
day of weektakes a date or a date and time, and gives a string. - A function value whose type is a union of separate function types: an
ifthat picks one of two functions, or a built-in used as a value.
Three facts hold for all of them, and the rest of the page is their consequences:
- The number of arguments, or their names, selects the signatures that fit. The types of the arguments rule out the ones they are certainly not for.
- The type of the result is the union of the results of the signatures that the arguments may be for, with
Nullunless the function is nevernull. Many overloaded functions give the same result kind from every signature.abshas signatures with different result kinds, so a narrowed argument can make its result narrower. - A call that no signature takes is
nullwith anargument_out_of_domainwarning, when the arguments are certainly not for any. Narrowing is what lets that check say "certain". The warning may mention inferred types; the executable call validates actual values and builds its runtime diagnostic from them, independently of the analysis tier.
1. A signature
A built-in is a Feel.Builtin.Function (D-56): a name, its signatures, the function that runs, and flags. A signature is a list of parameters and the types that the result can be:
| Part | Is |
|---|---|
| a parameter | a name, the types it accepts, and whether it is optional, variadic or quiet (is defined, D-100) |
| the result | a list of types: what the call can give, null aside |
extension: true | the signature was added by an extension (D-81), section 9 |
never_null? (of the function) | the result is never null for an argument of a signature (is) |
check_arguments? (of the function) | an argument no signature takes is null with an error before the function is called; false for not, whose own error for a wrong kind is another |
The names of the parameters are what puts a call with named arguments in order; the types describe the function and are what the analyzer types a call by. The function itself decides, when it runs, whether a value is one of them.
2. Which built-ins are overloaded
| Pattern | Functions | A call |
|---|---|---|
| a point or a range, on either side (9) | after, before, coincides, during, finished by, finishes, includes, started by, starts | before(1, [5..10]), includes(r, x) |
| a list, or the values (10) | all, any, max, mean, median, min, mode, product, stddev, sum | sum(xs), sum(x, y) |
| a key, or a list of keys (1) | context put | context put(c, "b", 2), context put(c, ["b", "c"], 2) |
| a position, or a function that matches (1) | list replace | list replace(xs, 1, 5), list replace(xs, function(a, b) …, 5) |
| a kind of number or of duration, with a result of that kind (1) | abs | abs(x), abs(dd), abs(ym) |
| arity only (13) | ceiling, floor, round up, round down, round half up, round half down (a scale is optional); date, time, date and time, number; string join, sublist, substring | date("2024-01-31"), date(2024, 1, 31) |
| one signature, a parameter of several types (5) | day of week, day of year, month of year, week of year (a date or a date and time); years and months duration | day of week(d) |
The first five rows have signatures that differ in the types of a parameter at one arity; the sixth in the number of parameters; the last has a single signature. In 34 of the 35 functions with several signatures, every signature has the same result type, and so does the single signature of the last row; which signature a call is for does not change its type. abs is the one that does: its three signatures take a number, a days and time duration and a years and months duration, and give that kind. So the registry says which result goes with which argument, and the type of a call (section 4) follows that.
3. Which signatures a call fits
By position. The signatures that accept this many arguments (Feel.Builtin.Function.with_arity/2): a variadic parameter takes any number from its place on, and an optional one may be left out. A call with a number of arguments that no signature takes is null, with an arity_mismatch warning when it is prepared and the error when it runs.
By name. The first signature that has a parameter of each name; a parameter the call leaves out is null. Where two signatures have the same names, the order they are written in decides: before(point: x, range: r) is the point-and-range signature, before(range: r, point: x) the range-and-point one. An argument given twice, and a name no signature has, are also an arity_mismatch. Signatures that differ only in types are one choice (Feel.Builtin.Function.merge_variants/1): the names do not say of what kind the arguments are, so abs(n: dd) is for the three signatures of abs, each of which has an n, and not for the first one only.
| Expression | Type | Warnings |
|---|---|---|
date("2024-01-31") | date = 2024-01-31 | — |
date(2024, 1, 31) | date = 2024-01-31 | — |
date(x, 1, 31) | date | Null | — |
date() | Null | arity_mismatch |
substring("abc", 2) | string = "bc" | — |
substring("abc") | Null | arity_mismatch |
sum() | Null | arity_mismatch |
sum(n1: x) | number | Null | — |
abs(n: dd) | days and time duration in [PT0S..max] | Null | — |
abs(n: x) | number in [0..max] | Null | — |
before(point: x, range: r) | boolean | Null | — |
before(range: r, point: x) | boolean | Null | — |
before(range1: r, range2: r) | boolean | Null | — |
4. The type of the result
The result is Feel.Analysis.Calls.result_type/3: the union of the result of every signature that the arguments may be for, and Null unless the function is never null. A signature is left out when an argument is certainly not for it, which is the test of section 5 applied to each signature alone, with one difference: a null that an argument may be does not take it for every signature. A null gives null, which the type has anyway, and the rest of the argument says which signature gives the other results (a parameter that takes null among its types does take it). If the arguments are certainly not for any signature the call is null with its error, and the type is the union of all of them, since the call is Null whatever the type says. A call with constant arguments of a pure function is evaluated (folded), and then has the exact type of its value.
Of the 35 functions with several signatures, 34 give one type from every signature, so the arguments change nothing for them. abs is the case this is for.
| Expression | Type | Warnings |
|---|---|---|
abs(x) | number in [0..max] | Null | — |
abs(dd) | days and time duration in [PT0S..max] | Null | — |
abs(ym) | years and months duration in [P0M..max] | Null | — |
abs(dd + dd) | days and time duration in [PT0S..max] | Null | — |
abs(if b then dd else ym) | years and months duration in [P0M..max] | days and time duration in [PT0S..max] | Null | — |
abs(v) | number in [0..max] | years and months duration in [P0M..max] | days and time duration in [PT0S..max] | Null | — |
abs(s) | Null | — |
if v instance of number then abs(v) else 0 | number in [0..max] | — |
abs(-3) | number = 3 | — |
abs(@"PT1H") | days and time duration = PT1H | — |
max(xs) | number | Null | — |
max(1, 2) | number = 2 | — |
sum(xs) | number | Null | — |
sum(1, 2, 3) | number = 6 | — |
before(x, r) | boolean | Null | — |
list replace(xs, 1, 5) | non-empty list<number | Null> | Null | — |
context put(context: p, key: "zip", value: "x") | context<age: number | Null, name: string | Null, zip: string = "x"> | Null | — |
context put({a: 1}, "b", 2) | context<a: number = 1, b: number = 2> | — |
abs(dd)is a days and time duration, andabs(ym)a years and months duration; previously the type said all three kinds. The argument is the type of an expression, soabs(dd + dd)is as precise as the sum is.- An argument of two kinds gives both:
abs(if b then dd else ym)is either duration, not a number. abs(v)gives all three.visAny, which every signature takes. Narrowingvto a number makesabs(v)a number, as theifshows: a narrowed argument makes the result narrower.abs(s)isNull. No signature takes a string, so the call isnullwhateversis; the signatures alone would list all three kinds, since anullthat is only possible does not make a certain mismatch (abs("a")does, and isNull), and the rule ofabs(Feel.Analysis.Images.Number, D-161) says so. The rule also gives the numbers, from the sets of the argument:abs(x)isnumber in [0..max], and a duration isin [PT0S..max].maxandlist replacefollow the elements of the list (D-151): a signature can say how its result is made from the types of the arguments (result_from), andmax(xs)is the type of the elements ofxs, andlist replace(xs, 1, 5)a list of the elements and of the new item. This applies to 13 functions (below).
A constant gives an exact value: the function runs at prepare time, and the result is number = 3, days and time duration = PT1H, or the closed context that context put made.
A result that follows the types of the arguments (D-151)
A function whose result depends on the elements of an argument, rather than on its kind, says so in its signature (Feel.Builtin.Function.signature/3, result_from), and Calls.result_type/3 makes the type from the types of the arguments:
| Function | The result |
|---|---|
reverse, sort, distinct values, remove, sublist | a list of the elements of the first argument |
append, insert before, list replace | a list of the elements of the first argument and the items that are put in |
concatenate, union | a list of the elements of every argument |
flatten | a list of the elements, the lists among them taken apart |
max, min of a list | one of the elements, or null |
max, min of several arguments | one of the arguments, or null |
| Expression | Type | Warnings |
|---|---|---|
reverse(xs) | list<number | Null> | Null | — |
append(xs, s) | non-empty list<number | string | Null> | Null | — |
concatenate(xs, ys) | list<number | string | Null> | Null | — |
flatten([[1], [2, [3]]]) | non-empty list<number in 1, 2, 3> | — |
max(xs) | number | Null | — |
max(x, s) | Null | — |
reverse(v) | list<Any | Null> | Null | — |
append(xs, k) | non-empty list<number | Null> | Null | — |
append([], k) | non-empty list<number> | — |
max(append([], k)) | number | — |
max(append(xs, k)) | number | Null | — |
max(k, 3) | number | — |
if xs != [] then max(xs) else 0 | number | Null | — |
A value that is not a list is treated as a list of itself, so reverse(x) is a list of numbers; a null argument is not a list and the call is null, so the elements of x (a number that may be null) are numbers, not null. An argument of type Any gives Any elements.
A result is a list that has an element (non-empty list<...>, D-152) when the call puts an item in (append with an item, insert before), or keeps the length of a list that has one (reverse, sort, distinct values, list replace, concatenate and union with one that has), and it is null only for a null that it is given when the function fails for nothing else (append, concatenate, union, reverse, distinct values, flatten; not those that take a position or a function). max and min of a list that has an element, of items of one ordered kind (numbers, strings, dates, durations) and no null, are not null: max(append([], k)) is number, where max(xs) is number | Null for the xs of the tables, which may be empty and may hold a null.
5. Narrowed arguments and the certain mismatch
The type of an argument is used for one thing only, Feel.Analysis.Calls.mismatch/3: whether the arguments are ones that none of the signatures that fit takes (D-56). If so, the call is null with an argument_out_of_domain error, and the analyzer says so as a warning.
It must be certain:
- An argument is taken by a signature if some alternative of its type is compatible with some type of the parameter. A
Null, anAnyand a name of a model type are compatible with every parameter:nullexcuses the pair. - The implicit conversions are allowed (section 10.3.2.9.4): a list of one element is its element where a value is wanted, and a value is a list of itself where a list is wanted; a date is a date and time where one is wanted (D-56). The length of a list is known for a list literal or a constant, and unknown otherwise.
- A parameter whose registry type is
anytakes every argument. When several signatures fit the call, it is a mismatch only if every one of them rejects an argument. - A call that an extension may answer is not looked at (the extension takes the arguments it takes), and a function with
check_arguments?: falseis not checked.
That is why a declared name is not flagged and a narrowed one is: s is string | Null, and null excuses it, but the condition s != null (or s = "a", or s instance of string) removes the Null, and the call becomes certain.
| Expression | Type | Warnings |
|---|---|---|
abs(s) | Null | — |
abs("a") | Null | argument_out_of_domain |
if s != null then abs(s) else 0 | number = 0 | Null | argument_out_of_domain |
if s = "a" then abs(s) else 0 | number = 0 | Null | argument_out_of_domain |
if v instance of string then abs(v) else 0 | number = 0 | Null | argument_out_of_domain |
if v instance of number then abs(v) else 0 | number in [0..max] | — |
if v > 5 then string length(v) else 0 | number = 0 | Null | argument_out_of_domain |
if p.age > 5 then abs(p.name) else 0 | number = 0 | Null | — |
{k: if b then 1 else "a", r: abs(k)}.r | number = 1 | Null | null_possible (an info) |
{k: if b then 1 else "a", r: if k instance of string then abs(k) else 0}.r | number = 0 | Null | argument_out_of_domain |
abs(s)is not flagged:smay benull.abs("a")is flagged, because the constant"a"cannot benull.- A condition on a name (
s != null,s = "a",v instance of string,v > 5) narrows the argument, and narrowing can make a call certain that was only possible before.v > 5makesva number in the call, andstring lengthtakes no number. - A condition about another thing does not help:
p.age > 5narrowsp.age, andp.nameis still a string ornull. - A union of alternatives is excused if any of them is taken:
kis a number or a string andabs(k)takes the number (and is a number, ornullfor the string). A local narrowed byinstance of(k instance of string) has the string only, and the call is certain. The result of a call that is certainly
nullisNull. Inif … then abs(v) else 0thethenisNulland theelseisnumber = 0, so theifisnumber = 0 | Null.
A narrowed argument also selects the signature for the result (section 4): if v instance of number then abs(v) else 0 types abs(v) as a number.
Arguments that have to be of one kind (D-155)
A parameter says what one argument may be, and a point is a number, a string, a date, a time, a date and time or a duration, so each of the two points of before(point1, point2) is taken. They are also compared with each other, and a string and a number cannot be: the run gives null with the same error. A signature says which of its arguments have to agree (same_kind, Feel.Builtin.Function), and Calls.mismatch/3 flags the arguments of a call that cannot agree, as it flags one that no parameter takes: null, with the warning.
| Signature | Arguments that agree |
|---|---|
before, after, coincides of two points | the two points (a list of one element is the point it holds) |
min(list), max(list) | the elements of the list: those that are written out or a constant are each looked at, those of a list that is only typed are when it has an element |
min(c1, ..., cn), max(c1, ..., cn) | the arguments, as items; a list alone is the list of the items |
The kinds that an argument may be of are those of the members of its type that have an order. The same excuses hold as above: null, Any and the name of a model type are of every kind, so one argument of them agrees with everything. before(s, x) is not flagged for a string | Null and a number | Null, and is flagged for an s and an x that are not null.
| Expression | Type | Warnings |
|---|---|---|
before(s, x) | Null | — |
before("a", 2) | Null | argument_out_of_domain |
if s != null and x != null then before(s, x) else 0 | number = 0 | Null | argument_out_of_domain |
if d != null and dt != null then coincides(d, dt) else 0 | number = 0 | Null | argument_out_of_domain |
if s != null then max(s, k) else 0 | number = 0 | Null | argument_out_of_domain |
if s != null and s2 != null then max(s, s2) else 0 | number = 0 | string | — |
max(k, 2) | number | — |
max(["a", 1]) | Null | argument_out_of_domain |
if s != null then min([s, k]) else 0 | number = 0 | Null | argument_out_of_domain |
max(true, false) | Null | argument_out_of_domain |
max([]) | Null | — |
if s != null and r != null then before(s, r) else 0 | boolean | number = 0 | Null | null_possible (an info) |
- A date is not a date and time, and a years and months duration is not a days and time duration:
coincides(d, dt)andafter(ym, dd)are out of the domain, asd = dtis a mismatch. A time and a time with a zone are of one kind: whether they can be compared is decided at run time. - An item that has no order agrees with nothing, a single one included:
max(true)andmax([[1]])are out of the domain, and the elements of a list that may be empty are not looked at (max([])isnullwith no error). - A range is not looked at. The type of a range holds the range that has no endpoint (
(null..null)), which can be related to a point of any kind, sobefore(s, r)is not a certain mismatch for a range of numbers. A range that is a constant is not looked at either. - The result of the call is
Nullfor a call that is certainly out of the domain, as for any other, and the type of a call that is not is unchanged:max(s, s2)of two strings that are notnullis still a string.
6. A built-in as a value
A built-in is a value where its name stands alone and nothing in the scope or in a context has that name (sort(xs, before), {f: abs, r: f(-1)}.r). A function type has a fixed number of parameters, so a built-in has a function type for each number of arguments that a signature takes, with the types of the parameters from that signature; a parameter that takes several types is Any (D-82). A function with a variadic parameter has no list of types, and is Any.
The value is all of them, and its type is an overload (D-171, Feel.Type.Arrows): before is a function of two points and a function of a point and a range, and so on, as before instance of function<number, range<number>> -> boolean says in the run (true: one of its signatures conforms). A reader that does not ask for the overload sees the function types as alternatives, which is the type a built-in had; the questions that compare function types (is it within, is it taken by a parameter, what is left of it after a test) are asked of all of them.
| Expression | Type | Warnings |
|---|---|---|
abs | overload<function<Any | Null> -> Any | Null with function<number | Null> -> number | Null, function<days and time duration | Null> -> days and time duration | Null, function<years and months duration | Null> -> years and months duration | Null> | — |
substring | overload<function<string | Null, number | Null> -> string | Null, function<string | Null, number | Null, number | Null> -> string | Null> | — |
before | overload<function<Any | Null, Any | Null> -> boolean | Null, function<Any | Null, range<Any | Null> | Null> -> boolean | Null, function<range<Any | Null> | Null, Any | Null> -> boolean | Null, function<range<Any | Null> | Null, range<Any | Null> | Null> -> boolean | Null> | — |
string length | function<string | Null> -> number | Null | — |
sum | Any | — |
{f: abs, r: f(-1)}.r | number = 1 | — |
{f: abs, r: f(dd)}.r | days and time duration in [PT0S..max] | Null | — |
sort(xs, before) | list<number | Null> | Null | — |
before instance of function<number, number> -> boolean | boolean = true | — |
substring instance of function<string, number> -> string | boolean = true | — |
substring instance of function<string> -> string | boolean | — |
abs instance of function<Any> -> Any | boolean = true | — |
abs instance of function<number> -> number | boolean | — |
substring has two function types, one for each arity; before four, one for each signature, which all have two parameters and differ in their types. They are the arrows of the overload, and the value is each of them. abs has a signature for each kind, but the specification gives it one parameter that takes all three, and they differ only in types, so as a value they are one: function<Any> -> Any, and that is the one function type of abs (abs instance of function<number> -> number is false in the run, and a name or a parameter declared with that type is given the function of the signature that fits, a conversion, D-197). What a call of abs does with a number or a duration is what the signatures it was made of say: they are the refinements of the overload (after with), true of a call for the arguments in their parameters and silent about the others, and they are not function types of the value. sum is Any: its parameter is variadic.
A call of a key or a parameter that is known to be bound to a built-in ({f: abs, r: f(-1)}.r) is typed and folded as the call of the built-in: the type is number = 1, and the signature rules above apply, so {f: abs, r: f(dd)}.r is a days and time duration where abs as a value says Any. A callee that is an if, an item of a list or a key of a context literal says which functions it can be (D-193, section 8), and the call is the call of each of the built-in functions in it, with the rule of its family. A function that is not read (it came through a call, or an if nested more than four deep, or there are more than 4 of them) is typed by the overload, the arrows that take as many arguments and the refinements:
| Expression | Type | Warnings |
|---|---|---|
(if b then abs else ceiling)(x) | number | Null | — |
(if b then abs else ceiling)(dd) | days and time duration in [PT0S..max] | Null | — |
(if b then abs else ceiling)(v) | number | years and months duration in [P0M..max] | days and time duration in [PT0S..max] | Null | — |
for x in xs return (if b then abs else ceiling)(x) | list<number | Null> | Null | — |
(function(f: function<Any> -> Any, ys: list<number>) for x in ys return f(x))(abs, xs) | list<number in [0..max] | Null> | Null | — |
(function(f: function<Any> -> Any, ys: list<number>) for x in ys return f(x))(if b then abs else ceiling, xs) | list<Any | Null> | Null | — |
(if b then substring else before)("a", 1) | string = "a" | Null | — |
(if b then substring else before)(1) | Null | arity_mismatch |
(function(f: function<Any, Any> -> boolean) f(1, 2))(before) | boolean = true | — |
(function(f: function<number, range<number>> -> boolean, y: number) f(y, [1..5]))(before, k) | boolean | — |
(function(f: function<number> -> number, y: number) f(y))(abs, k) | number | Null | — |
(function(f: function<number> -> number, y) f(y))(abs, dd) | Any | Null | — |
(function(f: function<days and time duration> -> days and time duration, y) f(y))(abs, dd) | days and time duration | Null | — |
The recorded arrows that take as many arguments and whose domains cover every argument bound the result; each applicable call refinement then narrows it. If no recorded arrow covers the call, its starting result is Any | Null, including at a recorded arity: an unrecorded same-arity signature may accept another domain. Arrows may have been omitted by a meet or a cap, so absence does not establish an arity error. A parameter of a function type takes a built-in if one of its signatures is within the type, as the run takes it, and the function is then known: the row with before and f(1, 2) is before(1, 2), a constant. The last three rows are a parameter of function<number> -> number (or of the duration) that abs is not an instance of, and is not passed as it is: the run converts it to the function of the one signature of it that fits (D-197), which takes a number and is null for a duration. The type-only reader cannot establish this conversion's complete signature, so the duration call conservatively has Any | Null. Recursion links and literal functions carry separate callee evidence; ordinary function declarations do not. The rows with a loop show the use of a function that is passed on: the first is a function that is abs or ceiling called for each item; in the second abs is known to the parameter, and in the third the parameter is abs or ceiling, which the node of the argument says (D-193), and the call is each of them. ceiling is a function of a number and not of anything, so the run does not take it for function<Any> -> Any and the call is null then; the body that runs has abs, and the type, which has both, is wider than that.
7. Function values with separate types
A name, a key or an if can hold one of several functions, and then the callee has a union of function types. The call of a value (Feel.Analysis.Invocations, D-82) is typed from what the callee can be: each alternative gives a result, and the call is the union. (Where the node of the callee says which functions they are, the call is each of them called with the arguments, section 8; the types below are what a call has of the type of its callee, and the call has the values that are in both.)
| The alternative | Gives |
|---|---|
function<T1, …, Tn> -> R | R when the arity matches and a recorded arrow covers every argument domain; otherwise start at Any | Null and apply valid call refinements. An arrow declaration does not exclude other signatures, even at the same arity (D-171). |
Any, or a name of a model type | Any | Null |
null | null |
| anything else (a number, a list, …) | null, and a not_a_function warning when the callee cannot be a function |
| Expression | Type | Warnings |
|---|---|---|
(if b then function(a: number) a * 2 else function(a: string) a + "!") | function<number | Null> -> number | Null | function<string | Null> -> string | Null | — |
(if b then function(a: number) a * 2 else function(a: string) a + "!")(1) | number = 2 | string | Null | — |
(if b then function(a: number) a * 2 else function(a: string) a + "!")("x") | number | string = "x!" | Null | — |
(if b then function(a: number) a * 2 else function(a: string) a + "!")(v) | number | string | Null | — |
(if b then function(a: number) a * 2 else function(a: string) a + "!")(1, 2) | Null | arity_mismatch |
{g: if b then function(a: number) a * 2 else function(a: string) a + "!", r: g(1)}.r | number = 2 | string | Null | — |
{g: if b then function(a: number) a * 2 else function(a: string) a + "!", r: if g instance of function<number> -> number then g(1) else null}.r | number = 2 | Null | — |
f(x) | number | Null | — |
f(x, y) | Any | Null | — |
v(1) | Any | Null | — |
x(1) | Null | not_a_function |
- The two functions are separate types, and a call is the union of both, whatever the argument is. The number function given a string, and the string function given a number, are
null; so the result hasNull, and bothnumberandstring. A condition separates them for the arguments it is about.
g instance of function<number> -> numberremovesnull, and what a function that passes it is known to do is the test's type for a number (D-171, function): the string function is a function of a number only by returningnull, so the call with1after it isnumber | Null, where the call of both without the test isnumber | string | Null. For an argument that the test does not take the function is what its own type says.The arity is checked for a function that is known to have the arrows it has: a function written in the expression and called at once or through a key (section 8), a callee whose node says which functions it is (D-193), and a built-in value whose registry signatures are known. A call of one with two arguments, when it takes one, is
nullwith a warning. An overload type alone is not an exhaustive signature list. A function that is known only by its type (a name declaredfunction<number> -> number, a parameter of that type, the alternatives of anif) has an arrow within the type and may have others, so the call isAny | Nullwith no warning (D-171).A named call through a type alone is
Any | Null: function types have no parameter names. The names can select an unrecorded signature of another arity and result kind, even when fewer names are supplied than the recorded arity. Known function values keep their separate argument-binding analysis.
8. A function that is known
For guarded recursive parameters, specialization uses post-conversion bounds and discards operand shape, callee and origin evidence that conversion may invalidate. Recursive result projections can refine calls but cannot establish runtime signature membership; overlapping runtime signatures' possible results stay alternatives. See function narrowing and the guarded-type contract.
When the function is known (written in the expression and called at once, or through a key of a context, or said by the node of the callee, below), the call is not typed from the function's type but from its arguments (D-193): the body is analyzed again, with each parameter at the type of its argument and the facts that hold at the call. So the facts of the call narrow the body, and the body's own conditions narrow its parameters:
| Expression | Type | Warnings |
|---|---|---|
(function(a: number) if a > 5 then a else 0)(7) | number = 7 | — |
(function(a) if a > 5 then a else 0)(3) | number = 0 | — |
(function(a) if a > 5 then a else 0)(v) | number in 0, (5..max] | — |
(function(a: number) if a > 5 then a else 0)(x) | number in 0, (5..max] | — |
{f: function(a: number) if a > 5 then a else 0, r: f(x)}.r | number in 0, (5..max] | — |
(function(a: number) if x > 5 then a + x else a)(1) | number in 1, [6..max] | — |
The first two are constants: the call is evaluated. The third has a as Any in the body, and the condition a > 5 narrows it to a number above 5 in the then. The last two read an outer name x, whose fact holds in the body too. How deep a call is specialized is limited to three calls (@specializations, D-193). Reanalysis also has an aggregate 64 KiB source budget and reads at most four callee alternatives. Once a budget is spent the call uses conservative type information. Full analysis has a separate bounded kind-only signature pass; the two passes keep their own reanalysis budgets (D-219). The shared worker additionally contains the complete public preparation call; reducing repeated analysis within that boundary is still open performance work.
From its first analysis, a literal function keeps its observable runtime arrow separate from its more precise call result. The arrow is captured by kind-only analysis of its written environment, so neither condition facts, call specialization nor optimization can strengthen it (D-219). function(a: number) 1 has a nullable number result signature and a call refinement returning 1; a non-null body result does not make instance of function<number> -> number! true. Calling it still benefits from that precise body result. Non-null parameters remain non-null in public projection; a domain that cannot be projected safely makes the whole function Any.
A function that the body writes is analyzed for the arguments too, but that reanalysis changes its call summary, not its inspectable signature. The runtime signature of function(a) h comes from kind-only analysis in its written environment (h is Any), and that is what instance of and a declared type ask about. So a call that gives a function gives the overload of the arrow as it was written, which is what the algebra sees, with the arrow of this analysis as a refinement, which types a call and nothing else (D-171):
| Expression | Type | Warnings |
|---|---|---|
(function(h) function(a) h)(x) | overload<function<Any | Null> -> Any | Null with function<Any | Null> -> number | Null> | — |
(function(h) function(a) h)(x) instance of function<Any> -> Any | boolean = true | — |
(function(h) function(a) h)(x) instance of function<Any> -> number | boolean | — |
(function(h) function(a) h)(x)(1) | number | Null | — |
(function(x) function(y) x + y)(1)(2) | number = 3 | — |
(function(x) function(y) x + y)(1) instance of function<number> -> number | boolean | — |
The third row was boolean = true to the analysis and false to the run; the refinement is why the fourth and the fifth are still exact.
A callee that says which function it is (D-193)
A function reaches a call through a key of a context literal, an item of a list literal or a branch of an if as a type. A literal FEEL closure keeps its runtime arrow and call refinements inside the container; nested exclusions and function domains are not rewritten by normalization (D-152, D-171). Known-callee metadata can keep additional detail beyond that type, so it is read (Feel.Analysis.Callees): an if is its two branches, list[index] of a list literal is its items, context.key of a context literal is the entry, and a name of a key of a context is its value. A name that is bound to a list or a context keeps the functions in the shapes of its structure (D-193), so fs[1], c.g and a chain of them (c.d.g, fs[1].g) are read from the name, and so is a parameter that a call gives such a node and the iterator of a for, some or every over a list of them. A function literal in it is analyzed again for the arguments, in the place it was written, as one written at the call is, and a built-in function is typed as its call by name would be, with the rule of its family. The call is any of them, and null too where the callee or an argument may not fit, and it has the values that are also in what the type of the callee gives (a condition about the callee, such as g instance of function<number> -> number, narrows its type).
| Expression | Type | Warnings |
|---|---|---|
{g: function(a) a * 2}.g(x) | number | Null | — |
[abs, floor][1](x) | number | Null | — |
(if b then function(a) a * 2 else function(a) a + 1)(3) | number in 4, 6 | — |
(if b then abs else ceiling)(age) | number in [0..150] | Null | — |
{f: if b then abs else ceiling, r: f(age)}.r | number in [0..150] | Null | — |
(if b then function(a, c) a else function(a, c) c)(1) | Null | arity_mismatch |
[function(a) a, 5][1](3) | Any | Null | — |
{fs: [abs, floor], r: fs[1](x)}.r | number | Null | — |
{c: {g: function(a) a * 2}, r: c.g(x)}.r | number | Null | — |
{fs: [{g: function(a) a}, {g: function(a) a + 1}], r: fs[1].g(3)}.r | number in 3, 4 | — |
for f in [abs, floor] return f(x) | list<number | Null> | — |
(function(f: function<Any> -> Any, y) f(y))(if b then abs else ceiling, x) | Any | Null | — |
(function(f) f(3))(if b then function(a) a else function(a) a + 1) | number in 3, 4 | — |
(function(f: list<Any>) f(3))(if b then abs else ceiling) | Null | not_a_function |
{fs: (function() [abs])(), r: fs[1](x)}.r | Any | Null | — |
{g: function(a) function(c) a + c}.g(1)(2) | number = 3 | — |
(if b then function(a) function(c) a + c else function(a) function(c) a - c)(1)(2) | number in -1, 3 | — |
{fs: [abs, floor]}.fs[1](x) | number | Null | — |
[[abs, floor]][1][1](x) | number | Null | — |
(function(fs) fs[1](x))([abs, floor]) | number | Null | — |
(function(c) c.g(3))({g: function(a) a * 2, h: 5}) | number = 6 | — |
{g: function(a) {h: function(c) a + c}}.g(1).h(2) | Any | Null | — |
The function in the rows that are Any | Null is not read: an item that is a number, a list or a context that a call gives (a name has the shape of a list that is written, D-193), and a call that gives a context of functions. A parameter is the function that it is given when the run does not convert the argument: a declared type that is an Any takes it as it is. A declared arrow can convert a built-in to a restricted signature (D-197), so callee identity is kept only when the argument type conforms without conversion. A declared list can wrap it as a list of one; that parameter is typed by its type. There are at most 4 alternatives, and 64 KiB of function source is analyzed again for them in one analysis (the body of each, again for the body of a function in it); a call past either is typed by the type of its callee.
A call that gives a function (D-193). The body of a function that is analyzed again for a call says which functions it gives when its value is a function written in it, an if of them, a name that has them, or a call that gives them (Callees.alternatives/2 of the node of the body); the node of the call keeps them (callees in its metadata, answered to a call that repeats this one with the body's type, D-193), and a call of that call reads them as it reads any callee: g(1)(2) for a g that a key, a list item, an if, a name or a parameter holds. The function is written in the place of the body, with its parameters at the types of the arguments of the first call, so the second call is typed from both. A body that may give a value that is not a function (function(a) if a > 1 then abs else 1) says nothing, and a call that is null for sure gives none. The type of a function that a call of a refinement gives is the arrow as it was written with the arrow of the refinement as a refinement, at every depth (function(a) function(c) function(d) a + c + d called with null and 5 is not a function of null, which the run's function is not an instance of).
A literal that is selected from more than once, and a parameter that is given a list or a context (D-193). A list or a context literal is a container as the shape of a name is: {fs: [abs, floor]}.fs[1], [[abs, floor]][1][1] and (if b then {g: f} else {g: h}).g read the items and the entries of the literals that the node can be, four deep. A parameter that a call gives a list or a context literal, or a name that has the structure of one, with functions in it, has the structure of the argument (D-193), when the run does not convert the argument (it is within the declared type), and so fs[1], c.g and the iterator of a loop over it are read as they are for a name that is bound to the value, and a comparison of it with a list of another length is decided ((function(p) p = [abs, abs])([abs]) is boolean = false). A parameter that is given a list or a context with no function in it has no structure, as it had none.
9. Overloads of an extension
An extension adds signatures to a function DMN 1.6 has (overloads/0, D-81): the :camunda extension gives six functions another signature (D-81). They are marked extension: true. The function of DMN 1.6 runs first; an overload gets its turn only if that reports an error, and may answer :next for arguments that are not for it. A call that an extension may answer is marked in its metadata and:
- is not checked for a certain mismatch (the extension takes the arguments it takes);
- is folded with the extensions of the expression, as a call of DMN 1.6 is (D-45);
- has the types of the extension's signature in its result, not the registry's.
| Expression | Without :camunda | With :camunda |
|---|---|---|
number("1,000.5", ",") | Null | number = 1000.5 |
string join(["a", "b"], ", ", "[", "]") | Null | string = "[a, b]" |
substring("abc", 2, 10) | Null | string = "bc" |
get value({a: {b: 1}}, ["a", "b"]) | Null | number = 1 |
context put({a: 1}, ["b", "c"], 2) | Null | context<a: number = 1, b: context<c: number = 2>> |
Without the extension the first two calls are an arity_mismatch (the signatures do not exist) and the third gives null at run time; with it the overload answers. The overloads: number(from, grouping separator), date and time(date and time, timezone), string join with a prefix and a suffix, get value with a list of keys, context put that makes the path, and substring with a length past the end.
10. What is not done
- No generics. A result that follows the elements of an argument (
max(xs)) is a rule of the registry (D-151), not an arrow; a function type with a type variable is not in the language. A refinement has the result of its signature, not the rule of its family (D-161), where the callee is not read: a built-in function that reaches the call through a call that gives it, an
ifnested more than four deep or more than 4 functions has the overload ofabsandceiling, and its call for anageof[0..150]isnumber | Null(a refinement saysnumber) and notnumber in [0..150] | Null, whichabs(age)by name is. A callee whose node says which functions it is has the rule (section 8), and so does a name, a parameter and an iterator that hold them (D-193).Returned containers do not preserve every callable summary (D-152, D-193):
{fs: (function() [abs])(), r: fs[1](dd)}.risAny | Null. Literal FEEL closures keep their signature/call distinction inside a container; that does not make all built-in family rules or known-callee metadata survive a returned container. Nested closures fall back to their written parameters andAny | Null; equal arrow types in another entry or argument do not establish closure identity. A top-level closure uses a unique unspecialized written arrow when one can be paired, otherwise it keeps the broader unspecialized function alternatives.- No agreement check between the endpoints of a range and a point (
before(s, r),includes(r, s),meets(r, r2)): a range type has the range with no endpoint (section 5), so the kinds of its endpoints are not the kinds it can be related to. todo.md describes what it would take. - No signature has a different result from another signature of its function: in all 34 they share one. If one ever does, the result type will be the union, as it is now for
abs.
See also: function, Any, null, range; Feel.Analysis.Calls, Feel.Analysis.Invocations and Feel.Builtin.Function. The decision record explains the call boundary (D-82), signature contracts (D-171), and propagated summaries (D-193).