Feel.Builtin (feel v0.1.0)

Copy Markdown View Source

The built-in functions of FEEL (DMN 1.6, section 10.3.4).

This module holds their names, and finds and calls the ones that are written. The parser needs the names: many are several words (string length, date and time, years and months duration), and which tokens form a name depends on the names in scope, so the built-ins are part of the outermost scope (Feel.Analysis.ScopeStack).

The names are canonical (see Feel.Name).

How a function is called (D-56)

A function is a Feel.Builtin.Function that its area module lists with functions/0. A name that is in names/0 and in no list is not written yet (implemented?/1), and the analyzer says so.

The analyzer turns a call of a function into the IR node {:call, meta, {:builtin, meta, name}, arguments} (Feel.Analysis.Calls), with the arguments put in the order of the parameters: a call with named arguments is for the first signature that has them all (of those, the first that has them in the order they are written, D-79), and a parameter it leaves out is null. Both backends evaluate the arguments, in order, and then call/3 runs the function on the values and gives an outcome, which they settle at the span of the call. That is the whole contract, and generated code makes the same call. A function whose result can be larger than its arguments (string join, replace, flatten...) works out the size it is going to make and, when the limits refuse it, ends the run as settle/3 would for the finished value (Feel.Builtin.Size): the call does not return.

A call whose arguments no signature accepts (too many or too few, or a name no parameter has) is a null with an :arity_mismatch error, at run time; the analyzer sees it and warns, as it does for an operation that can only be a type mismatch.

Areas

The functions themselves are grouped by area of the specification, one module each, listed by areas/0. Every name belongs to exactly one of them, and a test keeps their lists and this module's list equal (D-56).

ModuleSectionFunctions
Feel.Builtin.Conversion10.3.4.18
Feel.Builtin.Boolean10.3.4.21
Feel.Builtin.String10.3.4.313
Feel.Builtin.List10.3.4.4, .924
Feel.Builtin.Number10.3.4.514
Feel.Builtin.Temporal10.3.4.6, .8, .117
Feel.Builtin.Range10.3.4.714
Feel.Builtin.Context10.3.4.105

Generated code may call them, so they are part of the ABI of Feel.Runtime.

Summary

Functions

Returns the modules that hold the built-in functions, one per area of the specification. Each has a names/0 with the names of its functions.

The diagnostic for a call of a function with a number of arguments that none of its signatures takes.

Returns true when name is the canonical name of a built-in function.

Calls a function on the values of its arguments, in the environment of a run, and returns the outcome (Feel.Runtime.Env.outcome/0) for the caller to settle.

call/4 settled at the span of the call, as the backends make it (Feel.Runtime.Env.settle/3), with the implicit conversions of the arguments reported first (D-46): a list of one element given where the parameter takes the element, and a value given where it takes a list, are each an :info :implicit_conversion at the span.

Forgets the table of functions that function/1 keeps (D-56), so that the next call builds it again from the areas.

Finds a built-in function that is written, by its canonical name.

Finds a function with the extensions that are on (Feel.Extension, D-81): a function of DMN 1.6 with the overloads that the extensions add to it (Feel.Builtin.Function.with_overloads/2), or a function that an extension adds. A function of DMN 1.6 that is not written is not found, whatever the extensions overload.

Returns true when the function is written. All 86 names are (D-87 wrote the last, sort), so it is false only for a name that is no built-in's; the path for a name that is registered and not written is kept for the day one is.

Returns the canonical names of the built-in functions, sorted.

A built-in function as a value (D-82): abs where an expression names it and does not call it. It is a Feel.Value.Function of the kind :builtin with the signatures of the function (and of the overloads that the extensions add to it), which a call finds the function by its name from. The type of a parameter or of the result is its type if the signature takes only one, and Any if it takes more. The signatures that differ only in types are one signature here, as they are for a call by name (Feel.Builtin.Function.merge_variants/1): abs is function<Any> -> Any, though a call of it is typed by the kind of its argument. The signatures that were made one are kept beside it, each as it was (D-171): what a call of the value does for a number, or for a duration, is what that signature says (Feel.Value.Function.refinements/1). They are not function types of the value, which is an instance of the function types of the merged signature, as the specification has it.

Functions

areas()

@spec areas() :: [module()]

Returns the modules that hold the built-in functions, one per area of the specification. Each has a names/0 with the names of its functions.

iex> Feel.Builtin.String in Feel.Builtin.areas()
true

arity_mismatch(function, arity)

The diagnostic for a call of a function with a number of arguments that none of its signatures takes.

iex> {:ok, function} = Feel.Builtin.function("date")
iex> Feel.Builtin.arity_mismatch(function, 2).message
"date takes 1 or 3 arguments, not 2"

builtin?(name)

@spec builtin?(Feel.Name.t()) :: boolean()

Returns true when name is the canonical name of a built-in function.

iex> Feel.Builtin.builtin?("date and time")
true
iex> Feel.Builtin.builtin?("price")
false

call(env, name, arguments, signature \\ nil)

Calls a function on the values of its arguments, in the environment of a run, and returns the outcome (Feel.Runtime.Env.outcome/0) for the caller to settle.

A function that is refused for the size of its result, or whose regular expression passes its budget, does not return: it ends the run (Feel.Runtime.Env.run/2 is where that is caught). A number of arguments that no signature accepts is {:error, diagnostic} with the code :arity_mismatch. A function that is not written raises RuntimeError, as the unwritten parts of the runtime do, and a name that is no built-in's is an :unknown_function error. The function is found with the extensions of the environment (function/2).

A call by name has chosen a signature (Feel.Analysis.Calls.arrange/2), and the function is given the values and not that signature, so with signature the values are checked against it first (D-56): one that its parameter does not take makes the result null with an :argument_out_of_domain error, and the function is not called. Without one the function decides, by the kinds of the values, among the signatures it has.

iex> env = Feel.Runtime.Env.new()
iex> Feel.Builtin.call(env, "date", ["2012-12-25"])
{:ok, ~D[2012-12-25]}
iex> {:error, diagnostic} = Feel.Builtin.call(env, "date", [])
iex> diagnostic.code
:arity_mismatch
iex> {:ok, put} = Feel.Builtin.function("context put")
iex> [key_signature, _keys_signature] = put.signatures
iex> arguments = [%{"y" => %{"a" => Decimal.new(0)}}, ["y", "a"], Decimal.new(2)]
iex> {:ok, _context} = Feel.Builtin.call(env, "context put", arguments)
iex> {:error, diagnostic} = Feel.Builtin.call(env, "context put", arguments, key_signature)
iex> diagnostic.code
:argument_out_of_domain

call_at(env, name, arguments, signature, span)

call/4 settled at the span of the call, as the backends make it (Feel.Runtime.Env.settle/3), with the implicit conversions of the arguments reported first (D-46): a list of one element given where the parameter takes the element, and a value given where it takes a list, are each an :info :implicit_conversion at the span.

A call whose arguments are taken as they are, or by no signature (that is the error of the call), reports nothing, and neither does one that is null with an error: the signatures say what the specification converts, and a function that does not (date([2020], 1, 2)) ends with its error, which says it did not. call/4 does not report, so that a caller who wants the outcome of the function alone has it.

iex> env = Feel.Runtime.Env.new()
iex> {value, env} = Feel.Builtin.call_at(env, "contains", [["foobar"], "of"], nil, {0, 30})
iex> {value, Enum.map(Feel.Runtime.Env.diagnostics(env), &{&1.severity, &1.code, &1.span})}
{false, [{:info, :implicit_conversion, {0, 30}}]}
iex> {value, env} = Feel.Builtin.call_at(Feel.Runtime.Env.new(), "contains", ["foobar", "of"], nil, {0, 30})
iex> {value, Feel.Runtime.Env.diagnostics(env)}
{false, []}

clear_cache()

@spec clear_cache() :: :ok

Forgets the table of functions that function/1 keeps (D-56), so that the next call builds it again from the areas.

The table is built from functions/0 of each area on the first call and kept for the life of the VM, since an area makes its list of functions anew at each call (a lookup took 1.5 to 3 µs, and every call of a function in a run made one). A VM in which the code of an area is loaded again (a shell that recompiles, a test that does) calls this after it.

iex> :ok = Feel.Builtin.clear_cache()
iex> {:ok, function} = Feel.Builtin.function("abs")
iex> function.name
"abs"

function(name)

@spec function(Feel.Name.t()) :: {:ok, Feel.Builtin.Function.t()} | :error

Finds a built-in function that is written, by its canonical name.

iex> {:ok, function} = Feel.Builtin.function("date and time")
iex> function.name
"date and time"
iex> Feel.Builtin.function("price")
:error

function(name, extensions)

@spec function(Feel.Name.t(), Feel.Extensions.t()) ::
  {:ok, Feel.Builtin.Function.t()} | :error

Finds a function with the extensions that are on (Feel.Extension, D-81): a function of DMN 1.6 with the overloads that the extensions add to it (Feel.Builtin.Function.with_overloads/2), or a function that an extension adds. A function of DMN 1.6 that is not written is not found, whatever the extensions overload.

iex> {:ok, number} = Feel.Builtin.function("number", Feel.Extensions.none())
iex> Feel.Builtin.Function.arities(number)
[1, 3]
iex> Feel.Builtin.function("shout", Feel.Extensions.none())
:error

implemented?(name)

@spec implemented?(Feel.Name.t()) :: boolean()

Returns true when the function is written. All 86 names are (D-87 wrote the last, sort), so it is false only for a name that is no built-in's; the path for a name that is registered and not written is kept for the day one is.

iex> Feel.Builtin.implemented?("now")
true
iex> Feel.Builtin.implemented?("sort")
true
iex> Feel.Builtin.implemented?("price")
false

names()

@spec names() :: [Feel.Name.t()]

Returns the canonical names of the built-in functions, sorted.

iex> "string length" in Feel.Builtin.names()
true

value(name, extensions \\ Extensions.none())

@spec value(Feel.Name.t(), Feel.Extensions.t()) ::
  {:ok, Feel.Value.Function.t()} | :error

A built-in function as a value (D-82): abs where an expression names it and does not call it. It is a Feel.Value.Function of the kind :builtin with the signatures of the function (and of the overloads that the extensions add to it), which a call finds the function by its name from. The type of a parameter or of the result is its type if the signature takes only one, and Any if it takes more. The signatures that differ only in types are one signature here, as they are for a call by name (Feel.Builtin.Function.merge_variants/1): abs is function<Any> -> Any, though a call of it is typed by the kind of its argument. The signatures that were made one are kept beside it, each as it was (D-171): what a call of the value does for a number, or for a duration, is what that signature says (Feel.Value.Function.refinements/1). They are not function types of the value, which is an instance of the function types of the merged signature, as the specification has it.

iex> {:ok, length} = Feel.Builtin.value("string length")
iex> {length.kind, length.name, Feel.Value.Function.type(length)}
{:builtin, "string length", {:function, [:string], :number}}
iex> {:ok, abs} = Feel.Builtin.value("abs")
iex> Feel.Value.Function.type(abs)
{:function, [:any], :any}
iex> Feel.Value.Function.refinements(abs) |> length()
3
iex> Feel.Builtin.value("price")
:error