Functions that call one another, each prepared on its own (D-188).
A function of FEEL reads the functions it calls as names: fib(n - 1) reads fib from the bindings, as n is read
from the parameters. Where the host binds fib to a function that was made before, nothing is special. A function
that calls itself, or two that call each other, have no such value to bind: the value of fib is a closure that
holds the bindings of its own body, fib among them, and an immutable value cannot hold itself. FEEL has no node for
it (fib in {fib: function(n) fib(n - 1)} is the name outside the context, D-58), and none is added: what
an expression means does not change.
This module makes the value that a name stands for by a link. The host gives the expressions that are to call
one another, each with the bindings of its other names, and says which of its names are the others (link/1). A
name that stands for another member is bound to a function that has the parameters of the member and, when it is
called, makes the member's closure from its expression on the spot, with its own names bound the same way, and
calls it. Nothing is made until a call is made, so there is no end to what is linked, and the call is a call of FEEL
in every other respect: its arguments are converted to the declared types of the parameters, it is one level
deeper than the call it is made in, and it counts as a call, so Feel.Limits ends a recursion that does not
(max_call_depth, max_calls) in either mode, as it ends any other.
iex> odd = Feel.Scope.new(%{"odd" => {:function, [:number], :boolean}})
iex> even = Feel.Scope.new(%{"even" => {:function, [:number], :boolean}})
iex> {:ok, is_even} = Feel.prepare("function(n: number) if n = 0 then true else odd(n - 1)", scope: odd, backend: :interpreter)
iex> {:ok, is_odd} = Feel.prepare("function(n: number) if n = 0 then false else even(n - 1)", scope: even, backend: :interpreter)
iex> linked =
...> Feel.Recursion.link(%{
...> even: %{expression: is_even, bindings: %{}, peers: [{"odd", :odd}]},
...> odd: %{expression: is_odd, bindings: %{}, peers: [{"even", :even}]}
...> })
iex> {:ok, function, []} = Feel.Recursion.run(linked, :even, [])
iex> f = Feel.Scope.new(%{"f" => {:function, [:number], :boolean}})
iex> {:ok, ten} = Feel.prepare("f(10)", scope: f, backend: :interpreter)
iex> {:ok, seven} = Feel.prepare("f(7)", scope: f, backend: :interpreter)
iex> {Feel.run(ten, %{"f" => function}), Feel.run(seven, %{"f" => function})}
{{:ok, true, []}, {:ok, false, []}}What a host gets, and what it must not expect
The value of a member is its closure with the links bound in it. It is a function value like any other: a decision
that reads it calls it, and its result is the same as if the names were bound to values made by hand. A name that
stands for a member that has no function (its expression gave something else, or nothing) is bound to null, as a name
with no value is.
The expressions are run by the interpreter only, as the call of a link makes a closure inside the run it is
called in, which is what keeps the limits and the diagnostics of that run. link/1 raises ArgumentError for an
expression of another backend, and for expressions that were prepared with other dialects or extensions: they run
in one environment.
The text of a member is run inside the text of the one that calls it, so what the body of a member reports has a span of
the member's text and not of the caller's. The function of a member, and a call through a link to it, report with the origin
that is the key of the member (Feel.Diagnostic, :origin, D-201), and the host puts the span in the text of the member it
names.
The closure of a member is made at the first call of a link to it in a run, and kept in the environment of the run
(Feel.Runtime.Env, :links): the nodes that make it (the conversion of its bindings to their declared types, and the
function node) and the map of its links are paid for once, and the calls after it are the calls of a function that
was written out. The environment is the one thing that a run threads, so nothing outlives the run: a function value
that a run gives back, and that is called in another, makes its closure again there.
Summary
Types
What identifies a member: whatever the host likes, a term that compares by value.
A member: its prepared expression, which gives a function, the values it reads that are not links, and the names that stand for other members, each with the key of the member.
Functions
Links the members: each expression is to be run with its bindings, and with each name in peers bound to the function
of the member whose key it has (null for a key that is not a member, or a member that gives no function).
Runs the expression of a member of a link, as Feel.run/3 does, with its links bound: the result is the function value
of the member when its expression is a function. A function that the run made without an origin is given the key of
the member as its origin (D-201), and one that it only gives back (it has one already) keeps its own.
Types
@type key() :: term()
What identifies a member: whatever the host likes, a term that compares by value.
@type member() :: %{ expression: Feel.Expression.t(), bindings: Feel.bindings(), peers: [{Feel.Name.t(), key()}] }
A member: its prepared expression, which gives a function, the values it reads that are not links, and the names that stand for other members, each with the key of the member.
@opaque t()
Functions
@spec link(%{optional(key()) => member()}, [Feel.run_option()]) :: t()
Links the members: each expression is to be run with its bindings, and with each name in peers bound to the function
of the member whose key it has (null for a key that is not a member, or a member that gives no function).
The expressions are probed once, with null for their peers, for the parameters and the result type of the function
they make, which the function of a link has. Raises ArgumentError for an expression that is not run by the
interpreter, and for members of different dialects or extensions. Preparation and signature probes accept
the run options of Feel.run/3, including sandbox: false for trusted callers. The default keeps
containment enabled, and opting out cannot leave an enclosing worker.
Before probing, members are prepared again from their saved AST and preparation options, with internal evidence that only their peers are single-signature FEEL functions (or null). This keeps recursive calls precise without treating ordinary host function declarations as exhaustive. The original prepared expressions are not changed. Expressions already prepared for those peers, as DMN compilation does, are reused without reanalysis.
@spec run(t(), key(), [Feel.run_option()]) :: Feel.result(Feel.Value.t())
Runs the expression of a member of a link, as Feel.run/3 does, with its links bound: the result is the function value
of the member when its expression is a function. A function that the run made without an origin is given the key of
the member as its origin (D-201), and one that it only gives back (it has one already) keeps its own.
Raises KeyError for a key that is not a member.