Feel.Optimizer (feel v0.1.0)

Copy Markdown View Source

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 + 2 is 3, 1 < 2 is true, and 1 + null is null. 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 of and 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) is 3, but now() reads the clock and stays. Subtrees are taken from the bottom, so x + (1 + 2) becomes x + 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 / 0 is null with 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 is true, and != is false, for every value, null included (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 b is a, and so is if false then b else a, null and a value that is not a boolean being as false. 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 :conform node 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 for 1 + 2 and number. 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 if among them reports nothing if its condition and the branches it can take do not). For a result that is 3 and the tests [0..10] that is so. It also goes when the result is null for 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

optimize_option()

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

Functions

optimize(ir, options \\ [])

@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.