Feel.Backend behaviour (feel v0.1.0)

Copy Markdown View Source

What turns a resolved expression into something that runs.

Feel.prepare/2 parses an expression, resolves its names, and checks it. The result is the resolved form, the IR. A backend takes the IR and produces an executable, and Feel.run/3 later runs that executable on a set of bindings. Splitting the two lets an expression be prepared once and run many times.

There are two backends, selected with the :backend option of Feel.prepare/2:

  • Feel.Backend.Interpreter (:interpreter, the default) walks the IR. It generates no code, so it loads nothing into the VM and mints no atoms, which suits expressions from sources that are not trusted. It is the reference: the other backend must give the same results.
  • Feel.Backend.Beam (:beam) emits Erlang abstract format, compiles it, and loads the module. It can reduce repeated execution cost, depending on the workload and fallback rate; preparation and release have separate costs. Generated code calls shared Feel.Runtime and built-in operations (Feel.Builtin). Host compilation can be expensive, especially for nested control flow, so an expression that would make more than :max_generated_statements of Feel.Limits is an error of the kind :limit (D-154); the modules and atoms it leaves are not bounded, and public preparation/execution require sandbox: false for trusted expressions.

Both run on Feel.Runtime, so they share one implementation of the semantics.

Both backends implement every node the analyzer produces (listed in Feel.IR). Their contract is the same value and ordered diagnostics (D-38); finite regression tests compare both modes. A new kind of node needs a clause in each. Direct backend calls do not create a sandbox; use the Feel facade to contain untrusted interpreter work.

Summary

Types

What compile/2 returns and run/3 takes. Opaque to everything but the backend.

The resolved form of an expression, as analysis produces it.

Callbacks

Turns the resolved form of an expression into an executable.

Frees whatever an executable holds. A backend that loads code into the VM needs this to unload it; the interpreter holds nothing and need not implement it.

Runs an executable on a set of bindings, with the run options of Feel.run/3.

Functions

Looks up the module of a backend by the name used in the :backend option.

Checks the :profile option of compile/2, which both backends and the emitter of the BEAM backend take.

Types

compile_option()

@type compile_option() ::
  {:profile, :fast | :explain}
  | {:limits, Feel.Limits.t()}
  | {:dialect, Feel.Dialect.t()}
  | {:extensions, Feel.Extensions.t() | [atom()]}

Options for compile/2.

  • :profile - :fast (default) or :explain. The explain profile is to report which rules and branches were taken, for audit trails. It is not implemented yet: the option is here so that it has a place, and :explain raises ArgumentError in both backends (validate_profile!/1).
  • :limits - a Feel.Limits.t/0. The BEAM backend refuses an expression that makes more code than :max_generated_statements allows (D-154); the interpreter makes none.
  • :dialect - the Feel.Dialect.t/0 the expression was prepared in, :feel by default. The executable keeps it, and every run of it has it in its environment (D-49).
  • :extensions - the extensions it was prepared with (Feel.Extensions), none by default. Kept and handed to every run in the same way (D-81).

executable()

@type executable() :: term()

What compile/2 returns and run/3 takes. Opaque to everything but the backend.

ir()

@type ir() :: Feel.IR.t()

The resolved form of an expression, as analysis produces it.

Callbacks

compile(ir, list)

@callback compile(ir(), [compile_option()]) ::
  {:ok, executable()} | {:error, Feel.Error.t()}

Turns the resolved form of an expression into an executable.

release(executable)

(optional)
@callback release(executable()) :: :ok | {:error, :in_use}

Frees whatever an executable holds. A backend that loads code into the VM needs this to unload it; the interpreter holds nothing and need not implement it.

Returns {:error, :in_use} when the executable is still running; the caller may try again later.

run(executable, bindings, list)

Runs an executable on a set of bindings, with the run options of Feel.run/3.

A semantic error in the expression is not an {:error, _}: in lenient mode it is a diagnostic in the result (see Feel.Error).

Functions

fetch(arg1)

@spec fetch(atom()) :: {:ok, module()} | :error

Looks up the module of a backend by the name used in the :backend option.

iex> Feel.Backend.fetch(:interpreter)
{:ok, Feel.Backend.Interpreter}
iex> Feel.Backend.fetch(:beam)
{:ok, Feel.Backend.Beam}
iex> Feel.Backend.fetch(:other)
:error

validate_profile!(other)

@spec validate_profile!(term()) :: :fast

Checks the :profile option of compile/2, which both backends and the emitter of the BEAM backend take.

:fast is the only profile there is. :explain is reserved for a report of the rules and branches that a run took, for audit trails, and is not implemented yet: it raises ArgumentError rather than being accepted and ignored, so a caller who asks for it is not given a run without the report. Anything else raises ArgumentError too.

iex> Feel.Backend.validate_profile!(:fast)
:fast

iex> Feel.Backend.validate_profile!(:explain)
** (ArgumentError) the :explain profile is not implemented yet: it is reserved for a report of the rules and branches that a run took, and only :fast works

iex> Feel.Backend.validate_profile!(:slow)
** (ArgumentError) expected :profile to be :fast or :explain, got: :slow