The guarded fast path of the BEAM backend, for the expressions that only do scalar work on declared names (D-164).
The general emitter calls shared runtime operations. This specialization removes dispatch and repeated kind checks where declared scalar types establish the required assumptions. Numeric and boolean operations no longer carry a step counter. String construction still checks individual value-size limits.
The subset
An expression is in it when its nodes are all of these, and there are at most 32 of them:
- constants: a number, a string,
trueorfalse; - a name that is read from the bindings, whose kind is
number,stringorboolean(the type that the scope declares for it, or, if the expression was not given declarations, the one the analyzer found); + - * /of two numbers,+of two strings, and-xof a number;=and!=of two numbers, two strings or two booleans, and<,<=,>and>=of two numbers;and,orandnot(x)of booleans, andbetweenof three numbers;in, with tests that are such expressions of booleans, which is what a range or a comparison written as a unary test becomes (age in [18..65));if then elseof a boolean, where it is the value of the expression or the value of a branch of one that is, so that what follows a branch is the end of the run (if a then 1 else if b then 2 else 3); anifthat is an operand is left to the emitter.
Everything else (contexts, lists, loops, functions, calls of built-in functions other than not, paths, dates and
durations, the unary tests themselves, allowed values, a declared result) is left to the emitter, and so is an
expression that has no name to read, which the optimizer has folded.
What it does
It reads the names it needs from the bindings in one pattern, with a guard for each that says it is a number (a
Decimal that is finite, of at most 34 digits and an exponent from -6176 to 6111: a guard cannot count the digits of a
coefficient, so a number with a larger exponent, 1E+6144, takes the clause that does not match and the fallback;
D-214), a string or a boolean, and a string that is declared is asked whether it is
valid UTF-8 (Feel.Runtime.Kind.string?/1), as the check of the declared types asks. A value that is not one, a name
that has no binding, or a binding that is spelled differently, is a clause that does not match, and so is a null
where the name is read by an operation that reports it (below). The declared names that the
expression does not read are checked as well, since the declared types are checked for all of them. With the values
in hand, the operations are done in a straight line without an environment and without an outcome: a case on
'Elixir.Feel.Runtime.Number':multiply(A, B) for an operation that gives a number, a comparison of the order that
Feel.Runtime.Number.compare/2 gives, the operators of the language for strings and booleans. The largest constructed
string is checked against max_string_length at the end of each path, including intermediate
strings when the final result is boolean. Successful paths return the original environment.
run(Bindings, E0) ->
case Bindings of
#{<<"price">> := #{'__struct__' := 'Elixir.Decimal', coef := C1, exp := X1} = P1,
<<"quantity">> := #{'__struct__' := 'Elixir.Decimal', coef := C2, exp := X2} = P2,
<<"tax">> := #{'__struct__' := 'Elixir.Decimal', coef := C3, exp := X3} = P3}
when is_integer(C1), C1 < 10^34, is_integer(X1), X1 >= -6176, X1 =< 6111,
...the same for C2, X2 and C3, X3 ->
case 'Elixir.Feel.Runtime.Number':multiply(P1, P2) of
{ok, V1} ->
case 'Elixir.Feel.Runtime.Number':add(Decimal1, P3) of
{ok, V2} ->
case 'Elixir.Feel.Runtime.Number':multiply(V1, V2) of
{ok, V3} -> {V3, E0};
_ -> fallback(Bindings, E0)
end;
_ -> fallback(Bindings, E0)
end;
_ -> fallback(Bindings, E0)
end;
_ -> fallback(Bindings, E0)
end.
fallback(Bindings, E0) ->
'Elixir.Feel.Runtime.Interpret':run(IR, Bindings, E0).A null
Most operations report a null operand (Feel.Runtime.NullOperand, D-103), and the path leaves those to the
interpreter. Some answer for it and report nothing: < and > are null, = and != are a boolean (null = null
is true, a number is not equal to null), and, or and not are the three-valued logic (false and null is
false), and the condition of an if that is not true takes the else. The arithmetic, <=, >=, between,
negation, the join of strings and the subject of an in report it. Every value that the path makes knows the names
that make it null (none: it is a value of its kind), and an operation that answers for one has code for it, which a
case of nil is: the same value and diagnostics as the interpreter gives.
A name may be null in the guard that reads it only if every operation it is an operand of answers for it. The first
that does not says which names it was given, they are made values (strict), and the code is made again: there is one
more strict name at each attempt, and an attempt is a walk of at most 32 nodes. A name declared ! is strict from the
start, since a null of it ends the run. A name that is not strict is read by a guard that says null or a value of
its kind, which costs a little more than the pattern of a value, and a declared string that is not strict is asked to
be valid only if it is not null.
What it does not do
It does not report anything, and it does not end a run. Anything that the interpreter would report or end the run for,
the path takes as a sign that it is not the one to answer: a null that is reported, a name that has no value, a value that has to be
converted to the declared type (a list of one number, a date for a date and time) or cannot be, an operation that gives
an error (an overflow, a division by zero, a number of more than 34 digits), and a constructed string longer than its limit.
It has changed nothing at that point, since it only makes values
until it returns, so the function fallback/2 runs the whole expression from the environment it was given with the
interpreter's walk (Feel.Runtime.Interpret.run/3), whose answer is what the interpreter would have given: the
diagnostics, the place where a limit ends the run, and the strict mode's error. The path is exact for the runs it
answers because each of its steps is one the runtime takes, with the arguments the runtime would give it, and for the
others because it does not answer.
Only the taken branch of an if constructs values, as in the interpreter. Each branch checks
its own largest string, so an untaken branch cannot cause a size failure.
The dialect of the expression is not read: nothing in the runtime
reads it for an operation that gives a value, and the operations that would (the ones that give an error) are not the
path's.
The IR that fallback/2 runs is a literal of the module, with the metadata of each node cut down to the span (and the
error and the signature of a call, and boxed of a boxed if), which is all that the interpreter reads of the nodes
of the subset: a module that
has the whole IR is 100 to 200 µs longer to compile, and one that has it as a binary takes 1 to 3 µs to read it for each
run that is not answered.
The compile
The function is nested one case deep for each operation that gives a number, with the names read at the start, so
every name is alive until it is used, and the Erlang compiler takes more than linear time for a function of many
of those (D-113): for arithmetic alone it takes 1.2 to 1.5 times as long as for the emitter's code for 4 to 32 nodes
(a chain of + of 16 names is the worst), for comparisons 0.7 to 0.9 times, and for an if chain or a mix of
operations about as long. The subset is therefore bounded, and an expression of more nodes than that keeps the code of
the emitter, which Feel.Backend.Beam.Chunks cuts.
Summary
Functions
@spec emit(Feel.IR.t()) :: {:ok, [:erl_parse.abstract_form()]} | :error
Returns the forms of run/2 and fallback/2 for an IR in the subset, or :error for one that is not.
iex> ir = {:conform_bindings, %{span: {0, 5}}, :enforce, [{"x", {0, 1}, :number}],
...> {:arithmetic, %{span: {0, 5}}, :add, {:var, %{span: {0, 1}, type: [:number, :null]}, "x"},
...> {:const, %{span: {4, 5}, type: [:number]}, Decimal.new(1)}}}
iex> {:ok, [{:function, _, :run, 2, _}, {:function, _, :fallback, 2, _}]} = Feel.Backend.Beam.Scalar.emit(ir)
iex> Feel.Backend.Beam.Scalar.emit({:const, %{span: {0, 1}, type: [:number]}, Decimal.new(1)})
:error