The step between the analyzer and the backends: it removes from the IR what the analysis has already settled (D-45).
Feel.prepare/2 parses an expression, analyzes it, optimizes the IR that the analyzer
produced, and has a backend compile the result. The optimizer is a function from IR to IR,
and both backends run what it gives. It changes nothing a run can observe: the value, the
diagnostics and where they point, and the point at which a strict run ends are those of the IR
it was given. A test runs the same expressions both ways, on both backends, to check it.
What it does
- Folds closed subtrees. An operation whose operands are constants is evaluated, and
the constant that comes out takes its place:
1 + 2is3,1 < 2istrue, and1 + nullisnull. A list or a context of constants is the list or the context, so[1, 2 + 3]is the list[1, 5]. So are a path or a descendant of a constant, a range,between,instance ofand a filter whose brackets read nothing but its elements, and the call of a built-in function that depends on its arguments alone (Feel.Builtin.Function,pure?), on constants:abs(-3)is3, butnow()reads the clock and stays. Subtrees are taken from the bottom, sox + (1 + 2)becomesx + 3. The evaluation is the runtime's own (Feel.Backend.Interpreter), with the limits of the expression. It is only used when the run reports nothing:1 / 0isnullwith an error, and a strict run ends there, so it stays an operation. So does a result that is past a limit (a string that is too long). - Settles a value compared with itself (D-170).
=of two operands that are the same read and cannot report istrue, and!=isfalse, for every value,nullincluded (Feel.Analysis.Identity). The analyzer does it for the operands it sees to be the same; here it is done again after the operands are folded, which can make two operands the same:[i, -1] = [i, 0 - 1], or(if 1 < 2 then i else 0) = i. A name of the bindings is not settled, since the read of it reports a name that is not bound (D-37). - Takes the branch of a conditional whose condition is a constant.
if true then a else bisa, and so isif false then b else a,nulland a value that is not a boolean being asfalse. The choice reports nothing and the other branch never runs, so the branch that is left is the conditional, with its own node and so its own spans. A branch is optimized only if it is the one that is left. A condition that is not a constant is left as it is, even when its type settles it: reading a name reports a name that is not bound. - Drops a conversion that is not needed. The
:conformnode of the declared type of the result (D-138) goes when the type of the expression is already within the declared one, as it is for1 + 2andnumber. A value of that type is not changed by the conversion. - Drops the verification of the result's allowed values (D-138) when the analysis has shown that
every value the expression can have satisfies them, and that the tests report nothing
while they do (an
ifamong them reports nothing if its condition and the branches it can take do not). For a result that is3and the tests[0..10]that is so. It also goes when the result isnullfor sure, which is exempt, so the tests are never run.
What it leaves
Optimizing a function body preserves its captured meta.returns (D-219). Narrowing or folding the
body cannot change runtime signature inspection or make a previously rejected callback acceptable.
Body call refinements can remain more precise than this observable signature. A result check stays
whenever the inferred possible values do not establish that the conversion or verification is redundant.
It does not look at what the types say for an expression with a name in it: the read of a name
reports a name that is not bound (D-37), and the order of evaluation is observable (D-53). A
result that certainly fails its allowed values keeps its verification, which reports the error; the
analyzer is where that is warned about. The verifications and conversions of the names at the start of
a run (D-138) stay. A call that is a certain error is left for the run to report, a call that reports
something is left too (the spelling of a text that an extension reads, an :info of every run, D-81), a call of
a function that is not pure is left (uuid()), and the call of a function value is never folded, since the callee
is only known when the run begins (D-82).
The fold is evaluated with the :dialect and the :extensions of the expression (D-45), as its run will be, so a
call of a function that an extension adds, or one that an extension gives a signature, is folded as a call of
DMN 1.6 is: trim(" a ") is "a" where :camunda is on.
Nothing in the IR changes in kind: a constant has the span of the operation it replaces and the exact type of its value.
iex> {:ok, ast} = Feel.parse("1 + 2 * 3")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> Feel.IR.kind(ir)
:arithmetic
iex> {:const, _meta, value} = Feel.Optimizer.optimize(ir)
iex> Decimal.equal?(value, 7)
true
Summary
Functions
Optimizes an IR. The options are the ones that a fold is evaluated with: :limits, a Feel.Limits.t/0 that
a fold is evaluated within (a result past a limit is not folded), and the :dialect and the :extensions of the
expression (D-81), so that a call of a function that an extension adds, or overloads, is the one the run will make.
Types
@type optimize_option() :: {:limits, Feel.Limits.t()} | {:dialect, Feel.Dialect.t()} | {:extensions, Feel.Extensions.t() | [atom()]}
Functions
@spec optimize(Feel.IR.t(), [optimize_option()]) :: Feel.IR.t()
Optimizes an IR. The options are the ones that a fold is evaluated with: :limits, a Feel.Limits.t/0 that
a fold is evaluated within (a result past a limit is not folded), and the :dialect and the :extensions of the
expression (D-81), so that a call of a function that an extension adds, or overloads, is the one the run will make.