Feel.Analysis.Filters (feel v0.1.0)

Copy Markdown View Source

The types of source[brackets] (DMN 1.6, Table 69; D-71).

The analyzer calls this module for the parts of a filter that are about types: what the elements of the source are, whether a name in the brackets can be an entry of an element, which of the three readings of the brackets applies, and what the result is. The run is Feel.Runtime.Filter.

The elements

The source is taken one alternative of its type at a time (parts/1):

The alternativeIts elements
list<E>E
Nullnone: the result is null
Any, a named typeAny | Null, a list or not
anything elseitself: a value that is not a list is a list of one

elements/1 is their union, which is the type of item in the brackets.

The names in the brackets

A name in the brackets is looked up in the elements first: item, and, if the element is a context, its entries (section 10.3.2.11.4). membership/2 says for a name whether every element has it ({:all, type}, an entry that is known to be there), none can (:none, so the name is the one of the scope), or some do ({:some, type}, so it is found at run time and falls back to the scope). A context type of the specification is open, so an entry that it does not list may be there; a context literal and a constant are closed (D-65), and an element that is no context has none. item is the same: it is the element, unless a context element has an entry called item, which wins (0069 #026).

The reading of the brackets

Whether they hold an index or a predicate is circular in the specification (notes 05, E40): it depends on the type of the brackets, which are run for each element. mode/2 decides from the type, which is known as soon as the names are:

  • :predicate when no number can come out of the brackets: a boolean, null, or anything that is not a number keeps no element;
  • :index when only a number (or null) can, and the brackets do not read the element, so that their value is the same outside the filter, where an index is run;
  • :dynamic otherwise, when the value decides (Feel.Runtime.Filter.dynamic/5).

The result

result/3 is the union of what the alternatives of the source give. A predicate gives a list of the elements that it keeps, so a list type whose elements are the type of the elements narrowed by what the predicate says when it is true (narrow/3, D-194): after item > 5, the numbers above 5; after x < 2 for the entry x of a context element, the contexts whose x is below 2, with that x. A predicate that can never be true keeps nothing, and the list is list<Null>, as [] is (Table 43). An index gives an element or null, which is null for a position that is not there; the first and the last element of a list that has one are there (D-152).

iex> parts = Feel.Analysis.Filters.parts([{:list, [:number, :null]}, :null])
iex> Feel.Analysis.Filters.elements(parts)
[:number, :null]
iex> Feel.Analysis.Filters.elements(Feel.Analysis.Filters.parts([:string]))
[:string]
iex> Feel.Analysis.Filters.membership([{:closed_context, %{"a" => [:number]}}], "a")
{:all, [:number]}
iex> Feel.Analysis.Filters.membership([{:context, %{}}], "a")
{:some, [:any, :null]}
iex> Feel.Analysis.Filters.membership([:number], "a")
:none
iex> Feel.Analysis.Filters.mode([:boolean, :null], false)
:predicate
iex> Feel.Analysis.Filters.mode([:number], false)
:index
iex> Feel.Analysis.Filters.mode([:any, :null], false)
:dynamic

Summary

Types

A filter whose brackets are being analyzed: its depth (the number of filters around it, itself included), the type of its elements, that type as a reader that looks into it sees it (positive: read once, for the lookup of every name in the brackets), and the number of contexts of the enclosing context literals that were there when it began, which tells where it comes among them.

One alternative of the type of the source of a filter: a list, a list that has an element, a value that is no list (the list of itself), null, or anything.

Functions

The type of the elements: of item in the brackets.

Whether an element can be a context (or anything at all), and so can have entries that a name in the brackets stands for.

Whether the elements have an entry of this name: all of them do, with the type its values have; none can; or some can, with the type the entry has where it is there.

The reading of the brackets, from their type and whether they read the element. The three readings are in the module documentation.

The elements that are left when the predicate is true, by its facts (Feel.Analysis.Narrowing) about the element of the filter of this depth: the element itself (item > 5) and the entries of a context element (x < 2, item.x < 2, item.a.b = 1). It is [] when none can be.

The alternatives of the type of the source of a filter, by what their elements are. The type is the exact one, in which a list that has an element is a list type less the empty list (Feel.Type.Union.non_empty/1, D-152); the others that are less some of a kind are the kind (Feel.Type.Union.positive/1).

The one index that brackets of this type are, if they are one: the number 1, -1 or 0, and nothing else (a null is no index, and gives null).

Whether the IR reads the element of the filter of this depth: item, an entry of it, or a name that may be one.

The type of source[brackets], from the parts of the source's type, the reading, and narrow: a function from the type of the elements of a part to the type of the elements that the predicate keeps (it gives [] for none).

The order in which the contexts that bind names are looked in, innermost first: the filters (frame/0) and the contexts of the context literals (Feel.Analysis.ScopeStack frames) are in one stack in the specification, and they are kept apart in the analysis, so this puts them together by where each filter began.

Types

frame()

@type frame() :: %{
  depth: pos_integer(),
  elements: Feel.Type.Union.t(),
  positive: Feel.Type.Union.t(),
  locals: non_neg_integer()
}

A filter whose brackets are being analyzed: its depth (the number of filters around it, itself included), the type of its elements, that type as a reader that looks into it sees it (positive: read once, for the lookup of every name in the brackets), and the number of contexts of the enclosing context literals that were there when it began, which tells where it comes among them.

membership()

@type membership() ::
  :none | {:all, Feel.Type.Union.t()} | {:some, Feel.Type.Union.t()}

mode()

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

part()

@type part() ::
  {:list, Feel.Type.Union.t()}
  | {:non_empty, Feel.Type.Union.t()}
  | {:single, Feel.Type.Union.t()}
  | :null
  | :unknown

One alternative of the type of the source of a filter: a list, a list that has an element, a value that is no list (the list of itself), null, or anything.

Functions

elements(parts)

@spec elements([part()]) :: Feel.Type.Union.t()

The type of the elements: of item in the brackets.

entries?(elements)

@spec entries?(Feel.Type.Union.t()) :: boolean()

Whether an element can be a context (or anything at all), and so can have entries that a name in the brackets stands for.

iex> Feel.Analysis.Filters.entries?([{:closed_context, %{}}, :null])
true
iex> Feel.Analysis.Filters.entries?([:any, :null])
true
iex> Feel.Analysis.Filters.entries?([:number, {:list, [:number]}])
false

membership(elements, name)

@spec membership(Feel.Type.Union.t(), Feel.Name.t()) :: membership()

Whether the elements have an entry of this name: all of them do, with the type its values have; none can; or some can, with the type the entry has where it is there.

A list of lists and the like have none. An element that is null has none. Discovery approximates each inspected union locally: an excluded context can still supply the entry. Returned field unions retain their own exclusions.

mode(brackets, reads?)

@spec mode(Feel.Type.Union.t(), boolean()) :: mode()

The reading of the brackets, from their type and whether they read the element. The three readings are in the module documentation.

narrow(elements, facts, depth)

@spec narrow(Feel.Type.Union.t(), map(), pos_integer()) :: Feel.Type.Union.t()

The elements that are left when the predicate is true, by its facts (Feel.Analysis.Narrowing) about the element of the filter of this depth: the element itself (item > 5) and the entries of a context element (x < 2, item.x < 2, item.a.b = 1). It is [] when none can be.

An alternative of a context type is kept with the type of the entry narrowed, and goes if nothing is left of the entry: with [{a: 1}, {a: 2}][a >= 2] the contexts with a 1 go. A selection from what is no context is null, so an element that is null or a scalar goes if the fact cannot hold for null. What can have entries that the type does not tell (an open context, Any) stays.

iex> elements = [{:closed_context, %{"a" => [{:number, Feel.Type.NumberSet.point(Decimal.new(1))}]}},
...>             {:closed_context, %{"a" => [{:number, Feel.Type.NumberSet.point(Decimal.new(2))}]}}]
iex> {:ok, ast} = Feel.parse("[{a: 1}, {a: 2}][a >= 2]")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> {:filter, _meta, :predicate, _source, predicate} = ir
iex> facts = Feel.Analysis.Narrowing.facts(predicate, :then)
iex> Feel.Analysis.Filters.narrow(elements, facts, 1) |> Feel.Type.Union.format()
"context<a: number = 2>"

parts(source)

@spec parts(Feel.Type.Union.t()) :: [part()]

The alternatives of the type of the source of a filter, by what their elements are. The type is the exact one, in which a list that has an element is a list type less the empty list (Feel.Type.Union.non_empty/1, D-152); the others that are less some of a kind are the kind (Feel.Type.Union.positive/1).

position(arg1)

@spec position(Feel.Type.Union.t()) :: 1 | 0 | -1 | nil

The one index that brackets of this type are, if they are one: the number 1, -1 or 0, and nothing else (a null is no index, and gives null).

iex> Feel.Analysis.Filters.position([{:number, Feel.Type.NumberSet.point(Decimal.new(-1))}])
-1
iex> Feel.Analysis.Filters.position([{:number, Feel.Type.NumberSet.point(Decimal.new(0))}])
0
iex> Feel.Analysis.Filters.position([{:number, Feel.Type.NumberSet.point(Decimal.new(2))}])
nil
iex> Feel.Analysis.Filters.position([{:number, Feel.Type.NumberSet.point(Decimal.new(1))}, :null])
nil

reads?(ir, depth)

@spec reads?(Feel.IR.t(), pos_integer()) :: boolean()

Whether the IR reads the element of the filter of this depth: item, an entry of it, or a name that may be one.

iex> Feel.Analysis.Filters.reads?({:item, %{span: {0, 4}, type: [:number], depth: 1}}, 1)
true
iex> Feel.Analysis.Filters.reads?({:item, %{span: {0, 4}, type: [:number], depth: 2}}, 1)
false
iex> Feel.Analysis.Filters.reads?({:const, %{span: {0, 1}, type: [:null]}, nil}, 1)
false

result(parts, mode, narrow, position \\ nil)

@spec result(
  [part()],
  mode(),
  (Feel.Type.Union.t() -> Feel.Type.Union.t()),
  1 | 0 | -1 | nil
) ::
  Feel.Type.Union.t()

The type of source[brackets], from the parts of the source's type, the reading, and narrow: a function from the type of the elements of a part to the type of the elements that the predicate keeps (it gives [] for none).

position is the one index that the brackets can be (position/1), or nil: 1 and -1 are an element of a list that has one, and 1 is the value that is not a list, so there the index is not null; 0 is no position, so it is null for every source (D-170).

iex> parts = Feel.Analysis.Filters.parts(Feel.Type.Union.non_empty({:list, [:number]}))
iex> narrow = fn elements -> elements end
iex> Feel.Type.Union.format(Feel.Analysis.Filters.result(parts, :index, narrow))
"number | Null"
iex> Feel.Type.Union.format(Feel.Analysis.Filters.result(parts, :index, narrow, -1))
"number"
iex> Feel.Type.Union.format(Feel.Analysis.Filters.result(parts, :index, narrow, 1))
"number"
iex> Feel.Type.Union.format(Feel.Analysis.Filters.result(parts, :index, narrow, 0))
"Null"

search_order(filters, scope_stack)

@spec search_order([frame()], Feel.Analysis.ScopeStack.t()) :: [
  filter: frame(),
  local: Feel.Analysis.ScopeStack.frame()
]

The order in which the contexts that bind names are looked in, innermost first: the filters (frame/0) and the contexts of the context literals (Feel.Analysis.ScopeStack frames) are in one stack in the specification, and they are kept apart in the analysis, so this puts them together by where each filter began.