Feel.Recursion (feel v0.1.0)

Copy Markdown View Source

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.

t()

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

key()

@type key() :: term()

What identifies a member: whatever the host likes, a term that compares by value.

member()

@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.

t()

@opaque t()

Functions

link(members, options \\ [])

@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.

run(recursion, key, options \\ [])

@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.