Feel.IR (feel v0.1.0)

Copy Markdown View Source

The resolved form of an expression: what Feel.prepare/2 builds from the syntax tree, and what the backends run.

The syntax tree (Feel.AST) describes the source as written. The analyzer turns it into this: names are resolved to variables, locals, or built-in functions; dates and times in @"..." literals are decoded; calls with named arguments to a known function are reordered into positional ones; the entries of a context are put in the order they can be evaluated; and every node carries its inferred type.

Every node is a tuple {kind, meta, ...}, as in the tree. meta is a map with the node's :span in the source (so a diagnostic raised at run time can point at the text) and its :type, a Feel.Type.Union.t/0 that is [:any, :null] when it cannot be known without running. The type of a node that cannot run (a branch of an if that is never taken) is Feel.Type.Union.never/0, the union of no value (unreachable/1, D-173).

Nodes

NodeMeaning
{:const, meta, value}a constant, including decoded dates and times
{:var, meta, name}a name bound in the bindings
{:local, meta, name}a name bound by an enclosing for, some, every, function, or context entry; meta.structure is what the analyzer knows of the list or the context it is: the shapes of what is in it (D-193), also when it is bound to a path or a descendant of a node that has one (D-193); meta.id is the depth of the context that binds it, which a fact about it (Feel.Analysis.Narrowing) is kept with (D-193); meta.callees is the functions its value says it can be, for a call to find (Feel.Analysis.Callees, D-193): of a key of a context, a parameter that a call gives an if, an item or a key of functions, and the iterator of a loop over a list of them (D-193); a shape of meta.structure has callees for the item or the entry in the same way
{:item, meta}the element in the brackets of a filter (item); meta.depth is the depth of the filter (D-71)
{:item_field, meta, name}an entry of that element, known to be there (every element has it); meta.depth as above
{:dynamic_name, meta, name, fallback}a name that some of the elements of the filters around may have: the entry of the first element, of the filters in meta.frames (depths, innermost first), that has it, and else fallback, the node of what the name is without them
{:builtin, meta, name}a built-in function: the callee of a call of one, and its value where its name stands alone (D-82), which is a Feel.Value.Function of the kind :builtin, made with the extensions of the run; meta.type is the function type of each of its signatures. meta.from_binding is true when a call through a local binding is resolved to this builtin; its written name may resolve differently without that binding
{:negate, meta, expr}-expr
{:arithmetic, meta, op, left, right}+ - * / **
{:compare, meta, op, left, right}= != < <= > >=. = and != of two operands that are the same read, and cannot report, are not this node but the constant (D-170)
{:logic, meta, :and | :or, left, right}three-valued and, or
{:between, meta, value, low, high}value between low and high: value >= low and value <= high, with the value evaluated once (D-69)
{:in, meta, subject, tests}subject in test, subject in (a, b): the tests are expressions of the input ?, which is the value of subject, and the node is the three-valued OR of their values (D-61)
{:prefix_test, meta, op, endpoint}< 5, >= x, = 3, != 3 as a value: the Feel.Value.Range it says, null with an error for an endpoint of a kind with no order (D-61). In a test it is the comparison of ?
{:range, meta, start_included, from, to, end_included}[1..5) as a value: a Feel.Value.Range, or null with an error when the endpoints are of kinds that cannot make one (D-61)
{:instance_of, meta, expr, type}expr instance of type
{:if, meta, condition, then, otherwise}if … then … else …. meta.boxed (also of :filter and :quantified) is the strict reading of a boxed expression: null with an error for a condition that is a value other than a boolean and null (D-212, Feel.Runtime.Boxed)
{:for, meta, iterators, body}for x in xs return body; meta.partial is how the body reads partial: false (not at all), true (as a value, the list of the results so far) or :view (only as partial[index] and count(partial), which are the two nodes below, D-86)
{:partial_at, meta, index}partial[index] in the body of a for whose meta.partial is :view: the result at the index, as a filter with that index takes the element of the list of the results (the first for 1, the last for -1, null past the ends and for 0, null with an error for an index that is not a whole number). meta is that of the filter
{:partial_count, meta}count(partial) in such a body: the number of results so far. meta is that of the call
{:quantified, meta, :some | :every, bindings, test}some x in xs satisfies test
{:filter, meta, mode, source, brackets}source[brackets]; meta.depth is 1 for a filter that is in no other, and one more for each that it is in
{:path, meta, source, key}source.name, key the canonical name (D-65)
{:descendant, meta, source, key}source...name, key as for a path (D-65)
{:call, meta, callee, arguments}callee(…). A built-in function is {:builtin, meta, name}, with the arguments in the order of its parameters as a list. Anything else is a value: the callee is any node, and the arguments are {:positional, [node]} or {:named, [{name, node}]}, matched to the parameters when the call runs (D-82). meta.error carries a fixed structural error, such as invalid arity, that the call reports instead of calling. Static argument-type mismatches retain runtime value validation; the inferred types appear in preparation warnings, not runtime diagnostics. meta.extension is true on a call of a built-in function that a signature of an extension may answer (D-81): it is folded with the extensions of the expression (D-45), and not as a call of DMN 1.6. meta.signature is the signature that the names of a call by name chose (D-56): the function is given the values and not the signature, so the run checks the values against it first (Feel.Builtin.call/4); a call by position and a call that an extension may answer have none
{:function, meta, parameters, body}function(a, b) body: parameters are {name, type}, the type being :any if none was declared; the parameters are {:local, meta, name} in the body. The node is a closure of the bindings it is evaluated in, so a function value (Feel.Value.Function, kind :feel) is made when the node is evaluated (D-82); meta.closure numbers it in the analysis, which a call of a callee that has the node finds the place it was written in by (D-193)
{:list, meta, items}[a, b]
{:context, meta, entries}{a: 1, "b": 2}, entries in evaluation order; meta.order has the keys in the order they are written in (D-135), when that is another
{:unary_tests, meta, kind, tests}the root of a list of unary tests: each test is an expression of the input ?, and the node is true or false
{:decision_table, meta, policy, aggregation, inputs, rules, outputs}a decision table, as in Feel.AST: inputs are {expression, input_values}, rules are {id, entries, results} and outputs are {name, order, default}, each part an IR node or nil. The entries are :unary_tests nodes whose ? is the value of the input of their column; the parts run only when the policy asks for them (Feel.Runtime.DecisionTable)
{:match, meta, uses_input?, input, expr}a test whose meaning depends on the value of expr: a list, a range, or a value
{:member, meta, uses_input?, input, expr}the same for a test of in (D-61): a list is list contains, an element of another kind is not equal to the input and not an error
{:conform_bindings, meta, mode, conforms, body}body run with the bound values of the names in conforms ([{name, span, type}], D-138) converted to the declared type and verified to conform to it: a value that can be made to is converted, null stays null, and one that cannot is null in body, with an error at span, the first read of the name With the mode {:report, severity} (D-138), nothing is converted and a value that does not conform as it is stays, with a :type_mismatch of that severity.
{:verify_bindings, meta, mode, verifications, body}body run with the bound values of the names in verifications ([{name, span, tests}], D-138; {name, span, tests, marks} for a name that is declared ! (:non_null, D-48) or as a list whose elements the tests are for (:each, D-51), or for the member of the value at the end of a path ({:path, steps}, D-161: a key of a context, :each for every element of a list; there are several entries for a name that has constraints for several members)) verified first: a value that is not null and that the unary tests do not accept is null in body, with an error at span, the first read of the name With the mode {:report, severity} (D-138), the value stays and only a :value_not_allowed of that severity is given. For a name declared ! the value is not null but the run ends with an error of kind :input. With :each, the tests are run on each element that is not null, and the first that they do not accept makes the value not allowed; with a path, on what is there, and a member that is not there or is null is exempt.
{:allowed, meta, subject, expr, tests}the result of an expression with allowed values (D-138), subject being :result (or a name): expr unless it is not null and the unary tests do not accept it, then null with an error. With meta.each the result is a list and the tests are for its elements (D-51): the first that they do not accept makes the result null, with the error that says which element. With meta.path (a list of keys of contexts and :each) the tests are for the member of the result at the end of the path, and with each too for the elements of that member; what is null or not there is exempt (a component constraint of a declared value)
{:conform, meta, expr, type}the result of expr converted to the declared type (D-138): as it is, or converted so that it conforms, or null with an error
{:quiet, meta, expr}expr read without reporting (D-100): what it reports when it is run, an error, a warning or an info, is dropped, and does not end a mode: :strict run. It is the argument of a built-in function whose parameter is quiet (Feel.Builtin.Function.parameter/3), and has the type of expr
{:type, meta, type}the root of a type expression

A literal function's meta.type keeps its observable runtime signature bound separate from its more precise body result: the former is an arrow, the latter a call refinement in an overload where needed (Feel.Type.Arrows). Signature tests read only the arrows. Literal closures retain this distinction in lists and contexts; known-callee and shape metadata support additional reanalysis. Its meta.returns is the public result type of that signature, captured by kind-only analysis in its written environment and used by both backends. Refinement, call specialization and optimization preserve it even when they narrow the body's inferred type. An enforced recursive result stays exact in runtime metadata (result_spec/1); its observable analysis result is widened to Any | Null, with structural body bounds used only for calls.

A filter has a mode, decided at analysis from the type of the brackets (Feel.Analysis.Filters.mode/2): :predicate when no number can come out of them (keep the elements for which they are true), :index when only a number can and they do not read the element (take that element; they are run once, outside the filter), and :dynamic when it cannot be known and the evaluator must decide from the value (Feel.Runtime.Filter.dynamic/5). The brackets are run for each element with the element bound under the depth of the filter, so that a filter inside the brackets of another can read both.

Not every node is produced yet

The set is bigger than what the analyzer makes of the syntax tree, and the nodes it makes are the ones both backends run. :type is not produced by any entry point (a type is read on its own by Feel.Type.parse/2). :builtin is only ever the callee of a :call.

A new kind of node needs a clause in Feel.Backend.Interpreter.Walk (its value_of/3) and one in Feel.Backend.Beam.Emitter.

Summary

Types

The arguments of a call. A call to a built-in function has them as a list, in the order of its parameters (Feel.Analysis.Calls); a call to a function only known at run time keeps them as they were written, by position or with the names (D-82).

What a node that verifies the bound values does with one that does not hold: :enforce makes it null (converted first, for a type), and {:report, severity} leaves it and reports it (D-138). The analyzer decides it, once, from the :declarations option (D-138).

What a for iterator ranges over: a list, or from..to.

What a path or descendant selects: a canonical name, or tokens resolved at run time. The analyzer makes a path's key a canonical name always (D-65); the other form is for a name that only the keys of the value can end (D-27).

A piece of the IR that names are looked for in (local_names/1): a node, the domain of an iterator, or a list of them.

A byte range in the source: from inclusive, to exclusive.

t()

Functions

Returns the type of a node as a reader that compares and calls functions sees it: plain_type/1 with the overloads kept (Feel.Type.Union.positive_keeping_overloads/1, D-171). A built-in function with a signature for each kind it takes is all of its function types, and what is passed to a parameter of a function type, or called, has to be asked as that.

Returns the byte range of the domain of an iterator (D-86): the node, or the two ends of from..to.

Returns the kind of a node, the first element of its tuple.

Returns the names that the :local nodes in a piece of the IR read (D-86): the iterators, parameters and keys of contexts that it names, wherever they are bound. A name that a construct inside the piece binds again is in it too, so these are the names the piece may read from outside, and perhaps more.

Returns the type of a node as a reader that looks into it sees it: the exact type, with the kinds that are a kind less some of it (Feel.Type.Except, D-152) as the whole kind (Feel.Type.Union.positive/1). It holds every value that type/1 holds. What only passes a type on (the branches of an if, the entries of a context) reads type/1, and what asks what the type is (is it a list, which entries has it) reads this one.

The result type exposed by a closure's runtime signature. An enforced recursive result declaration retains its exact graph; an analysis approximation cannot replace that evidence. Checks around the result preserve it, while another conversion establishes a new boundary. The analyzer stores this selection in the function node's meta.returns before optimization; backends use the stored type rather than selecting again from a rewritten body.

Returns the byte range of a node's source text.

Returns the inferred type of a node, a Feel.Type.Union.

Returns a piece of the IR as it is when it cannot run (D-173): the type of every node in it is Feel.Type.Union.never/0, the union of no value. The analyzer does it to a branch of an if that the type of the condition rules out, or in which a read finds that the facts of the conditions around it leave its name no value, so that nothing in the branch keeps a type that is true of no run (a name that the facts empty would be the type it had before the conditions).

Types

arguments()

@type arguments() :: {:positional, [t()]} | {:named, [{Feel.Name.t(), t()}]}

The arguments of a call. A call to a built-in function has them as a list, in the order of its parameters (Feel.Analysis.Calls); a call to a function only known at run time keeps them as they were written, by position or with the names (D-82).

arithmetic_operator()

@type arithmetic_operator() :: :add | :subtract | :multiply | :divide | :power

binding_mode()

@type binding_mode() :: :enforce | {:report, :error | :warning}

What a node that verifies the bound values does with one that does not hold: :enforce makes it null (converted first, for a type), and {:report, severity} leaves it and reports it (D-138). The analyzer decides it, once, from the :declarations option (D-138).

comparison_operator()

@type comparison_operator() :: :eq | :neq | :lt | :lte | :gt | :gte

domain()

@type domain() :: t() | {:through, t(), t()}

What a for iterator ranges over: a list, or from..to.

filter_mode()

@type filter_mode() :: :predicate | :index | :dynamic

iterator()

@type iterator() :: {Feel.Name.t(), domain()}

key()

@type key() :: Feel.Name.t() | {:dynamic, Feel.AST.tokens()}

What a path or descendant selects: a canonical name, or tokens resolved at run time. The analyzer makes a path's key a canonical name always (D-65); the other form is for a name that only the keys of the value can end (D-27).

meta()

@type meta() :: %{
  :span => span(),
  :type => Feel.Type.Union.t() | Feel.Type.Union.never(),
  optional(atom()) => term()
}

piece()

@type piece() :: domain() | [piece()]

A piece of the IR that names are looked for in (local_names/1): a node, the domain of an iterator, or a list of them.

span()

@type span() :: Feel.Diagnostic.span()

A byte range in the source: from inclusive, to exclusive.

t()

@type t() ::
  {:const, meta(), Feel.Value.t()}
  | {:var, meta(), Feel.Name.t()}
  | {:local, meta(), Feel.Name.t()}
  | {:item, meta()}
  | {:item_field, meta(), Feel.Name.t()}
  | {:dynamic_name, meta(), Feel.Name.t(), t()}
  | {:builtin, meta(), Feel.Name.t()}
  | {:negate, meta(), t()}
  | {:arithmetic, meta(), arithmetic_operator(), t(), t()}
  | {:compare, meta(), comparison_operator(), t(), t()}
  | {:logic, meta(), :and | :or, t(), t()}
  | {:between, meta(), t(), t(), t()}
  | {:in, meta(), t(), [t()]}
  | {:prefix_test, meta(), comparison_operator(), t()}
  | {:range, meta(), boolean(), t(), t(), boolean()}
  | {:instance_of, meta(), t(), Feel.Type.t()}
  | {:if, meta(), t(), t(), t()}
  | {:for, meta(), [iterator(), ...], t()}
  | {:partial_at, meta(), t()}
  | {:partial_count, meta()}
  | {:quantified, meta(), :some | :every, [{Feel.Name.t(), t()}, ...], t()}
  | {:filter, meta(), filter_mode(), t(), t()}
  | {:path, meta(), t(), key()}
  | {:descendant, meta(), t(), key()}
  | {:call, meta(), t(), arguments() | [t()]}
  | {:function, meta(), [{Feel.Name.t(), Feel.Type.t()}], t()}
  | {:list, meta(), [t()]}
  | {:context, meta(), [{String.t(), t()}]}
  | {:unary_tests, meta(), :any | :positive | :negated, [t()]}
  | {:decision_table, meta(), Feel.AST.decision_policy(),
     Feel.AST.decision_aggregation(), [{t(), t() | nil}],
     [{String.t(), [t() | nil], [t()]}],
     [{String.t() | nil, t() | nil, t() | nil}]}
  | {:match, meta(), boolean(), t(), t()}
  | {:member, meta(), boolean(), t(), t()}
  | {:conform_bindings, meta(), binding_mode(),
     [{Feel.Name.t(), span(), Feel.Type.t()}], t()}
  | {:verify_bindings, meta(), binding_mode(),
     [
       {Feel.Name.t(), span(), t()}
       | {Feel.Name.t(), span(), t(),
          [:non_null | :each | {:path, [String.t() | :each]}]}
     ], t()}
  | {:allowed, meta(), Feel.Name.t() | :result, t(), t()}
  | {:conform, meta(), t(), Feel.Type.t()}
  | {:quiet, meta(), t()}
  | {:type, meta(), Feel.Type.t()}

Functions

callable_type(node)

@spec callable_type(t()) :: Feel.Type.Union.t()

Returns the type of a node as a reader that compares and calls functions sees it: plain_type/1 with the overloads kept (Feel.Type.Union.positive_keeping_overloads/1, D-171). A built-in function with a signature for each kind it takes is all of its function types, and what is passed to a parameter of a function type, or called, has to be asked as that.

iex> {:ok, abs} = Feel.Builtin.value("abs")
iex> node = {:const, %{span: {0, 1}, type: Feel.Type.Union.of_value(abs)}, abs}
iex> length(Feel.IR.callable_type(node))
1
iex> length(Feel.IR.plain_type(node))
1
iex> match?([{:overload, _, _}], Feel.IR.callable_type(node))
true

domain_span(node)

@spec domain_span(t() | {:through, t(), t()}) :: span()

Returns the byte range of the domain of an iterator (D-86): the node, or the two ends of from..to.

iex> from = {:const, %{span: {9, 10}, type: [:null]}, nil}
iex> to = {:const, %{span: {12, 14}, type: [:null]}, nil}
iex> Feel.IR.domain_span({:through, from, to})
{9, 14}

kind(node)

@spec kind(t()) :: atom()

Returns the kind of a node, the first element of its tuple.

iex> Feel.IR.kind({:item, %{span: {0, 4}, type: :any}})
:item

local_names(piece)

@spec local_names(piece()) :: MapSet.t(Feel.Name.t())

Returns the names that the :local nodes in a piece of the IR read (D-86): the iterators, parameters and keys of contexts that it names, wherever they are bound. A name that a construct inside the piece binds again is in it too, so these are the names the piece may read from outside, and perhaps more.

The nodes are looked for in tuples and lists. Metadata (a map) is not looked in, and nor is a constant, whose value is data and not code.

iex> local = fn name -> {:local, %{span: {0, 1}, type: [:number]}, name} end
iex> sum = {:arithmetic, %{span: {0, 5}, type: [:number]}, :add, local.("a"), local.("b")}
iex> Feel.IR.local_names({:list, %{span: {0, 9}, type: [:list]}, [sum, local.("a")]})
MapSet.new(["a", "b"])

plain_type(node)

@spec plain_type(t()) :: Feel.Type.Union.t() | Feel.Type.Union.never()

Returns the type of a node as a reader that looks into it sees it: the exact type, with the kinds that are a kind less some of it (Feel.Type.Except, D-152) as the whole kind (Feel.Type.Union.positive/1). It holds every value that type/1 holds. What only passes a type on (the branches of an if, the entries of a context) reads type/1, and what asks what the type is (is it a list, which entries has it) reads this one.

iex> except = {:except, {:list, [:any, :null]}, [{:list, [:number, :null]}]}
iex> node = {:const, %{span: {0, 1}, type: [except, :null]}, nil}
iex> Feel.IR.plain_type(node)
[{:list, [:any, :null]}, :null]

result_spec(node)

@spec result_spec(t()) :: Feel.Type.t()

The result type exposed by a closure's runtime signature. An enforced recursive result declaration retains its exact graph; an analysis approximation cannot replace that evidence. Checks around the result preserve it, while another conversion establishes a new boundary. The analyzer stores this selection in the function node's meta.returns before optimization; backends use the stored type rather than selecting again from a rewritten body.

span(node)

@spec span(t()) :: span()

Returns the byte range of a node's source text.

iex> Feel.IR.span({:item, %{span: {0, 4}, type: :any}})
{0, 4}

type(node)

@spec type(t()) :: Feel.Type.Union.t() | Feel.Type.Union.never()

Returns the inferred type of a node, a Feel.Type.Union.

iex> Feel.IR.type({:const, %{span: {0, 1}, type: [:number]}, Decimal.new(1)})
[:number]

unreachable(node)

@spec unreachable(t()) :: t()

Returns a piece of the IR as it is when it cannot run (D-173): the type of every node in it is Feel.Type.Union.never/0, the union of no value. The analyzer does it to a branch of an if that the type of the condition rules out, or in which a read finds that the facts of the conditions around it leave its name no value, so that nothing in the branch keeps a type that is true of no run (a name that the facts empty would be the type it had before the conditions).

A constant keeps its value, and metadata other than the type stays as it is. A node that has never for its type is taken to have nothing in it that has not (the analyzer makes them only here), and is not looked into. A node of this type that a run reaches is a wrong type, which Feel.Paranoid reports.

iex> const = fn value -> {:const, %{span: {0, 1}, type: Feel.Type.Union.of_value(value)}, value} end
iex> sum = {:arithmetic, %{span: {0, 5}, type: [:number]}, :add, const.(Decimal.new(1)), const.(Decimal.new(2))}
iex> {:arithmetic, meta, :add, {:const, left, _}, _right} = Feel.IR.unreachable(sum)
iex> {meta.type, left.type}
{[], []}