Feel.Backend.Interpreter (feel v0.1.0)

Copy Markdown View Source

The backend that walks the resolved form of an expression.

It generates no code. Its executable is the IR itself, and run/3 evaluates it node by node, calling Feel.Runtime for every operation. Nothing is loaded into the VM and no atoms are created, so it is the backend to use for expressions that come from sources that are not trusted. It is also the reference that the BEAM backend is tested against.

Each operation returns an outcome (Feel.Runtime.Env.outcome/0), and the interpreter passes it to Feel.Runtime.Env.settle/3 with the span of the node, so a diagnostic points at the text it came from and strict mode ends the run there.

Bindings

A :var node is looked up with Feel.Runtime.Bindings.lookup/4 (D-37), the same function the generated code of the BEAM backend calls, so both find the same value.

Operands

Both operands of a binary operator are always evaluated, and and or included: the specification gives them no order, and a false decides a conjunction whatever the other operand is, an error included (D-53). So an error in the operand that does not matter is still reported, and in strict mode it ends the run.

A conditional is the exception, as the specification has it: it evaluates the condition and then only the branch it takes, the then branch for a condition that is true and the else branch for any other value. So nothing in the other branch is reported, and in strict mode it does not end the run.

Lists and contexts

The elements of a list are evaluated in order. The entries of a context are evaluated in the order the node has them, which is the order they depend on each other in, and the value of each is bound to its key for the ones after it: a :local node reads it. The bindings of the run hold them under {:local, name}, which no name of the bindings can be, and the context's own copy of the bindings is what the entries after see, so a key that an inner context has again shadows the outer one only inside it.

Loops

for, some and every are run by Feel.Runtime.Iteration, which takes a function for each domain and one for the body: the walk of the nodes, with the iterators bound as {:local, name} in the bindings (D-86). A domain with more than 16 iterators before it is given and binds only the ones its code names (Feel.IR.local_names/1), where binding all of them for each domain made a loop of n iterators n * n / 2 bindings (D-86).

Nodes

run/3 evaluates every node that the analyzer produces (see Feel.IR, which also says which nodes of its set are not produced yet). A node that it has no clause for raises ArgumentError: that is a mistake of the analyzer and not of the expression, which is why it is not a diagnostic.

The walk and the monitor

The walk of the nodes is in Feel.Backend.Interpreter.Walk, one text that is two modules: Feel.Backend.Interpreter.Plain for run/3, and Feel.Backend.Interpreter.Monitored for run_monitored/4, which tells a function the value of every node (Feel.Paranoid, D-159). A run does not ask at each node whether there is a monitor: the question cost 3 to 14% of a run of a few operations on names.

Summary

Types

What a monitored run tells of each node: the node, and the value it made.

t()

Functions

Wraps the IR in an executable, with the dialect and the extensions it was prepared with (D-49, D-81). The interpreter has nothing to compile, so the other options are only checked. The profile :explain is not implemented yet and raises ArgumentError (Feel.Backend.validate_profile!/1).

Evaluates an IR on a set of bindings and an environment, as run/3 does, and returns {value, env} without catching the end of a run (Feel.Runtime.Env.run/2 is what does).

Evaluates an executable on a set of bindings.

Evaluates an executable and tells monitor, a function of a node and its value, the value of every node as it is made (Feel.Paranoid). The run is the run of run/3.

Types

monitor()

@type monitor() :: (Feel.IR.t(), Feel.Value.t() -> term())

What a monitored run tells of each node: the node, and the value it made.

t()

@type t() :: %Feel.Backend.Interpreter{
  binding_names: MapSet.t(Feel.Name.t()) | nil,
  dialect: Feel.Dialect.t(),
  env: Feel.Runtime.Env.t() | nil,
  extensions: Feel.Extensions.t(),
  ir: Feel.Backend.ir()
}

Functions

compile(ir, options)

@spec compile(Feel.Backend.ir(), [Feel.Backend.compile_option()]) :: {:ok, t()}

Wraps the IR in an executable, with the dialect and the extensions it was prepared with (D-49, D-81). The interpreter has nothing to compile, so the other options are only checked. The profile :explain is not implemented yet and raises ArgumentError (Feel.Backend.validate_profile!/1).

iex> {:ok, executable} = Feel.Backend.Interpreter.compile(:an_ir, [])
iex> executable.ir
:an_ir

evaluate(ir, bindings, env)

Evaluates an IR on a set of bindings and an environment, as run/3 does, and returns {value, env} without catching the end of a run (Feel.Runtime.Env.run/2 is what does).

It is the walk itself, for what runs the interpreter in the middle of a run: the guarded fast path of the BEAM backend (Feel.Backend.Beam.Scalar) runs an expression this way from the environment it was given when it finds that it cannot answer, through Feel.Runtime.Interpret (D-164).

iex> ir = {:arithmetic, %{span: {0, 5}}, :add, {:const, %{span: {0, 1}}, Decimal.new(1)}, {:var, %{span: {4, 5}}, "x"}}
iex> {value, _env} = Feel.Backend.Interpreter.evaluate(ir, %{"x" => Decimal.new(2)}, Feel.Runtime.Env.new())
iex> value
Decimal.new(3)

run(executable, bindings, options)

Evaluates an executable on a set of bindings.

Takes the run options of Feel.run/3 and raises ArgumentError for any other.

iex> ir = {:const, %{span: {0, 1}, type: :number}, Decimal.new(7)}
iex> {:ok, executable} = Feel.Backend.Interpreter.compile(ir, [])
iex> Feel.Backend.Interpreter.run(executable, %{}, [])
{:ok, Decimal.new(7), []}

iex> ir = {:var, %{span: {0, 1}, type: :any}, "x"}
iex> {:ok, executable} = Feel.Backend.Interpreter.compile(ir, [])
iex> Feel.Backend.Interpreter.run(executable, %{"x" => "a"}, [])
{:ok, "a", []}

run_monitored(interpreter, bindings, options, monitor)

@spec run_monitored(t(), Feel.bindings(), [Feel.run_option()], monitor()) ::
  Feel.result(Feel.Value.t())

Evaluates an executable and tells monitor, a function of a node and its value, the value of every node as it is made (Feel.Paranoid). The run is the run of run/3.

iex> ir = {:const, %{span: {0, 1}, type: :number}, Decimal.new(7)}
iex> {:ok, executable} = Feel.Backend.Interpreter.compile(ir, [])
iex> test = self()
iex> Feel.Backend.Interpreter.run_monitored(executable, %{}, [], fn _node, value -> send(test, {:value, value}) end)
{:ok, Decimal.new(7), []}
iex> receive do {:value, value} -> value end
Decimal.new(7)