Turns the resolved form of an expression into Erlang abstract format.
The result is a list of forms that :compile.forms/2 accepts: the module
attribute, the export of run/2,
and the function that runs the expression. The generated function calls only
Feel.Runtime and the built-in functions; it never calls anything chosen by the
expression.
Abstract format is emitted directly, not Elixir or Erlang source text, so no
string from the expression is ever interpreted as code. Strings and numbers in the
expression become literal terms. Names from the expression are never turned into
atoms: the variables of generated code are numbered (V1, E1, and so on), and the
names of bindings are binaries.
The generated function
run(Bindings, Env0) takes the bindings and the run environment
(Feel.Runtime.Env) and returns {Value, Env}, as the interpreter's evaluation
does. The body is a straight line of matches, one for each operation, in the order
the interpreter evaluates them (left operand, right operand, operation), so the
diagnostics of a run come out in the same order:
run(_Bindings, E0) ->
{V1, E1} = 'Elixir.Feel.Runtime.Env':settle(E0,
'Elixir.Feel.Runtime.Arithmetic':multiply(2, 3), {4, 9}, E0),
{V2, E2} = 'Elixir.Feel.Runtime.Env':settle(E1,
'Elixir.Feel.Runtime.Arithmetic':add(1, V1), {0, 9}, E0),
{V2, E2}.The last argument of settle is the environment from before the operands of the operation, which tells the
diagnostics that the operands reported (the ones that can explain a null operand, D-103) from the ones before them
(D-103). Only the arithmetic operators, the inclusive comparisons, between and the tests of in have it.
A constant is a literal in the call that uses it. The span is the one of the node, so a
diagnostic points at the same text as the interpreter's. A name is a call to
Feel.Runtime.Bindings.lookup/4, with the name as a binary, and the first parameter
is called Bindings only when the expression reads one. Both operands of every binary
operator are evaluated, left then right, with no short cut, as the interpreter does
(D-53).
The constants above are shown as numbers to keep the example short; a number is a
Decimal, a literal map in the generated code.
The fast path
An expression that only does scalar work on declared names is not emitted as above but by
Feel.Backend.Beam.Scalar (D-164): one function that reads the names it needs from the bindings, does the operations
with no environment and no outcome, accounting for constructed strings; a fallback/2 that runs the expression in the
interpreter from the environment it was given is what it does for anything it is not sure of. emit/2 asks it first and
uses its forms if it makes any, so the straight line of this module is what is left: lists, contexts, loops, functions,
calls of built-in functions, paths, dates, unary tests, allowed values and a declared result, an expression that
reads no name, and one of more than 32 nodes.
Nodes
It emits every node that the analyzer produces (see Feel.IR), and raises ArgumentError for any
other. A new kind of node needs a clause here as well as in Feel.Backend.Interpreter.Walk. The
:profile option is accepted and does not change the code yet.
Lists and contexts
The elements of a list are evaluated in order and put in a list expression. The entries of a context
are evaluated in the order the node has them, and the value of each is kept for the ones after it:
a :local node is the variable that holds it, and a context ends in a map expression of its keys
and those variables. A key is a binary in the map and never an atom or a name of a variable, so
nothing from the expression becomes code. Both are passed through Feel.Runtime.Env.settle/3 as a
value, so a list that is too long ends the run as it does in the interpreter.
If every value of a list were a variable that is alive until the list is built, the Erlang compiler would take more
than linear time for the function (a hundred values compile in a fifth of a second, five hundred in four seconds, and
about a thousand are more than a function can hold, which is a :backend error, :compile_failed), and the cuts of
Feel.Backend.Beam.Chunks (below) would find no place at which few variables are alive. So the values of a long list
are put in a variable of their own as they are made, L33 = [V1, …, V16] and then L50 = L33 ++ [V34, …], and the
entries of a context in a map that a group of entries updates at a time (L50 is L33 with the next entries), in
which the entries that later entries read are read (map_get(Key, L50)) and are not kept as variables. The same is done for the
arguments of a call and the tests of in and of unary tests, which are lists as well (D-113). A group ends after the
item at which it has bound 16 variables, wherever that is; a list that binds fewer is the list of its values.
Funs with many parameters
The funs of a function value, of a loop's domains and of its body get their parameters as one list. Up to 16
parameters the head is the list of their variables (fun (E, [P3_0, P3_1])). With more, a pattern with so many
variables, read in the body, costs the Erlang compiler more than linear time (and a for with n iterators has
n * n / 2 of them, since each domain gets the iterators before it), so the fun takes the list as one variable,
its body begins with T = list_to_tuple(Args) and reads the parameter i as element(i, T). A tuple that the body
does not read is not made (D-113). A domain with more than 16 iterators before it is told, in the reads option of
the call of the loop, the places of the ones its code names, and is given the values at them and no others, so its
head is the list of those variables (D-86); the loop would otherwise make the list of all the ones before it for
each domain, which is n * n / 2 for a loop of n iterators.
Long bodies
The Erlang compiler takes more than linear time for one long function, and a body is as long as the expression:
x and x and … of 400 terms took it 4.9 s (D-113). emit/2 therefore gives the function it made to
Feel.Backend.Beam.Chunks, which cuts a body of more than 32 statements, where few variables are alive, into
functions that call one another: run/2 ends in a tail call of run_1/N, which takes the variables that the rest of
the body reads, and so on. The statements are the same and in the same order, so a run reports what it did. The bodies of
the branches of a conditional and of the functions below are cut in the same way.
Conditionals
A conditional runs only the branch it takes, so it is the one node that is not a straight line
of matches: the two branches are the clauses of a case on the condition, each with statements
of its own that end in the value and the environment, and the result of the case is matched
like any other operation. The numbers of the variables go on across the branches, so that no
variable is bound twice, and both branches start from the environment that the condition gave.
For if c then 2 else 3 / z:
run(Bindings, E0) ->
{V1, E1} = 'Elixir.Feel.Runtime.Bindings':lookup(E0, Bindings, <<"c">>, {3, 4}),
{V4, E4} = case V1 of
true ->
{2, E1};
_ ->
{V2, E2} = 'Elixir.Feel.Runtime.Bindings':lookup(E1, Bindings, <<"z">>, {21, 22}),
{V3, E3} = 'Elixir.Feel.Runtime.Env':settle(E2,
'Elixir.Feel.Runtime.Arithmetic':divide(3, V2), {17, 22}, E1),
{V3, E3}
end,
{V4, E4}.Only true takes the first clause: false, null and a value that is not a boolean all take
the second (Table 50). The choice itself requires no runtime call.
Allowed values
A value with allowed values (D-138) is a call to Feel.Runtime.AllowedValues.verify/5, which
gets the value, who it is, where it is, and a function of the environment that runs the tests.
The function is a closure in the generated code: it binds ? in a copy of the bindings
(Feel.Runtime.Bindings.bind_input/2) and runs the tests in a straight line of their own,
with numbers that go on from those of the function around it, since a variable that is bound
inside a function and has the name of one outside it is a match and not a new variable:
run(Bindings, E0) ->
{V1, E1} = 'Elixir.Feel.Runtime.Bindings':lookup(E0, Bindings, <<"age">>, {0, 3}),
{V3, E3} = 'Elixir.Feel.Runtime.AllowedValues':verify(E1, V1, <<"age">>, {0, 3},
fun (E2) ->
B2 = 'Elixir.Feel.Runtime.Bindings':bind_input(Bindings, V1),
...
{'Elixir.Feel.Runtime.UnaryTests':satisfied(positive, [V2]), E2}
end),
{V3, E3}.
Summary
Functions
Emits the forms of a module with a run/2 function for the given IR.
Types
@type forms() :: [:erl_parse.abstract_form()]
A list of forms in Erlang abstract format.
@type option() :: {:module, module()} | {:profile, :fast | :explain}
Options for emit/2.
:module- the name of the module to generate. Required.:profile-:fast(default) or:explain, as inFeel.Backend.compile_option/0.:explainis not implemented yet and raisesArgumentError(Feel.Backend.validate_profile!/1).
Functions
@spec emit(Feel.Backend.ir(), [option()]) :: {:ok, forms()} | {:error, Feel.Error.t()}
Emits the forms of a module with a run/2 function for the given IR.
The generated run/2 takes the bindings and the run environment and returns the
value of the expression and the environment. Raises KeyError without :module,
ArgumentError for an unknown option or for a node that cannot be emitted yet.
iex> ir = {:const, %{span: {0, 1}, type: :number}, Decimal.new(1)}
iex> {:ok, forms} = Feel.Backend.Beam.Emitter.emit(ir, module: Feel.Generated.Doc)
iex> {:attribute, _, :module, module} = hd(forms)
iex> module
Feel.Generated.Doc