What a condition says about the names it reads (D-194).
if c then a else b runs a only when c is true, and b for anything else, null included. So
in a a name that c compares has a value for which c can be true, and in b one for which it
can be something else: after x > 5 is true, x is a number above 5 and not null; after x = null
is not true, x is not null; after y != 0 is true, y is not 0. facts/2 reads that off the
resolved condition, for one of the two branches, and narrow/2 applies it to the type of a name.
A fact is a function from the type of a name to the alternatives that are left. It only ever takes alternatives away, or makes the set of values of one smaller, so a type that is narrowed is still a type of every value the name can have in the branch. Only the cases that can be read safely are read:
- a name that is the whole condition (
if flag), and - a comparison
= != < <= > >=of a name with any expression, whichever side the name is on, - joined by
andandor, which meet and join the facts as the logic does.
A condition of anything else says nothing.
Paths
What a condition says about a path p.age (D-65) is said in the same way, and holds for the same
reason: a value is bound once, so p.age is the same in every read of it. The subject of such a fact
is {:path, "p", ["age"]}, the name and the keys, and only a path that starts at a variable has one:
a key of a context literal or a name that something else binds can be shadowed (D-26) and a fact about
it would be about another value. path_subject/1 finds it; the analyzer narrows the path nodes that
have one.
Locals
A local, a parameter of a function or an entry of a context, is narrowed in the same way (D-193). It is told by the
depth of the context that binds it and its name, {:local, id, name}, and {:local, meta, name} has the id in its
meta: a local of the same name that is bound again deeper, in the branch of the condition, is another value and
takes no fact. The id is a number and not a reference, since the IR is hashed to name the module of the BEAM backend.
The comparison
The value of a name is compared with an expression of a type. When the comparison is true the
value is of a kind that the type can be compared with, and in the set that Feel.Analysis.TestSets
gives: x > 5 leaves the numbers above 5, and a string, a boolean, a list and null are gone, for
they give null. When it is not true the value is one that makes it false or null, which is
every value, except where the comparison is true for sure: x != null is true for every value that
is not null, so when it is not true the name is null, and x = 5 is true only for 5 when the
other side is exactly 5, so the name is anything but 5. The sets of the values that make it false
are used only when null cannot be the outcome, which is when the other side is of one kind and not
null (x > y for a number y that is never null), and every value is left otherwise.
null = x is false for a value that is not null and null != x is true, so a name that can be
null stays null in the branch of x = null and goes from the other; that is the guard a decision
most often makes.
The strings of a type are a set (Feel.Type.StringSet, D-39), and what a comparison leaves of them is
a set too, and exactly: after s < "b" an s of {"a", "b", "c"} is {"a"}, and when it is not true
it is {"b", "c"}; after s != "a" a bare string is string except "a", and so is it when s = "a"
is not true; against a set of strings it is some string of it. The sets are those of TestSets, and
the set of the name is cut by them, ties included (the run says "a" <= "a�" is false). An other
side that is unknown has nothing to leave out, and does not narrow.
Operations
A comparison of an arithmetic operation (x + 1 > 5, n * 2 = 5, -n > 3) is not a comparison of a name, but says
two things of the names it reads (D-194, D-122). When it is true the operation is not null, so its operands are not,
and they are of kinds that give a value of a kind the other side has (x * 2 = 5 is not true of a duration, which a
number multiplies to a duration). And the number it gives is one that the comparison is true for (> 5: the numbers
above 5), so the numbers of its operands are those that give such a number with some number of the other operand
(Feel.Analysis.Preimage): x + 1 > 5 leaves x above 4, n * 2 = 5 leaves 2.5, and n + m > 150 for m in
[0..100] leaves n above 50. An operand that is an operation is read in turn (n * 2 + 1 > 51 leaves n above
25), and so are the tests of an in (n + 1 in [2..5]). It is read only in the branch where the comparison is true,
for the other is also taken when the operation is null for lack of range, a zero divisor, or a result too small to
write; and only if the other operand of the operation is a kind that the operation is read for, since a name of a
type that is not known can be anything.
A call of a number built-in is read in the same way (D-161, step C): abs(x) < 100 leaves x in (-100..100), floor(x) = 3
leaves [3..4), sqrt(x) < 10 leaves [0..100), round half up(x, 1) = 2.5 leaves [2.45..2.55), and exp(x) > 1 the
numbers above 0 (Feel.Analysis.Images.Number.Preimage has the numbers of each). The call is not null when the comparison
is true, so its arguments are numbers (a list of one is the number it has, and stays), and it is read in the branch where the
comparison is true only. A call that is an operand of an operation, or has an operation or another call as its argument, is
read in turn (abs(x + 1) < 5, floor(abs(x)) = 3). A scale of a rounding function has to be one number, or the call
says nothing of its argument.
A call of any other built-in function is read for the kinds of its arguments (D-170): when string length(s) > 3, day of week(d) = "Monday" or sum(xs) > 5 is true the call is not null, so each argument is one of the kinds for which the call gives a value
of the kind of the other side (Feel.Analysis.Images says which), and a null is one of them only for a call that takes one
(is(x, null) = true). The length of a string says more: string length(s) > 0 is not true of the empty string, and
string length(s) = 0 is true of nothing else.
The operation can be one on dates and durations (D-122): the images of its value are the days of a date, the months
of a years and months duration and the seconds of a days and time duration, and a comparison leaves the images of the
kind of the other side (Feel.Analysis.Operands). d + u > @"2020-06-30" leaves the dates from the 21st of June
for a u of at most ten days, e + @"P1M" = @"2021-02-28" the 28th to the 31st of January, and n * u > @"P5D" the
numbers above a half; a number times a duration is a duration, so it is read for the kind that the other side is.
And and or
A conjunction is true when both are, so in then the facts of both hold (the narrowings follow each
other); it is not true when either is not, so in else a name is narrowed only if both sides narrow
it, to the alternatives that either leaves. A disjunction is the same the other way round.
iex> {:ok, ast} = Feel.parse("x > 5")
iex> {:ok, %{ir: condition}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> %{"x" => fact} = Feel.Analysis.Narrowing.facts(condition, :then)
iex> Feel.Analysis.Narrowing.narrow([:any, :null], fact) |> Feel.Type.Union.format()
"number in (5..max]"
iex> %{"x" => fact} = Feel.Analysis.Narrowing.facts(condition, :else)
iex> Feel.Analysis.Narrowing.narrow([:number, :null], fact) |> Feel.Type.Union.format()
"number in [-max..5] | Null"
Summary
Types
What a condition said, as the branch of a conditional is told: :then is where it was true, :else where it was not
(false, null, or a value that is no boolean, since if takes the else branch for them all, Table 50), and the
two outcomes that :else is made of: :false where it was false, and :null where it was null or no boolean
(D-194). not(c) is true where c is false, so it needs the facts of the outcomes of c.
What is known about a name: the alternatives of its type that are left. It is empty when the fact leaves none, which says that the branch cannot be taken.
The facts about the names and the paths a condition reads, for a branch.
The kinds of subject some facts are about (:name for a variable, and the tag of the others, as subject/0 has them),
and the subjects that are paths from each name and each local (Feel.Analysis.Narrowing.Kinds).
A key of the process dictionary that narrow_sources/3 keeps its answers under: nil keeps none.
What the caller knows of how a subject is read: given the subject and a fact about it that was made of others, the types the
fact will be applied to where it is read, each with what the fact leaves of it ([] if it does not know any), and whether
the reads are :answered by those or the fact has to be :applied to some type (one that it has no answer for). The fact
is made to answer those at once (combine/5).
What a name that is bound to the value of a condition says of the names the condition read (D-194): the
node of the condition, whose facts are made where the name is read, once (cache: where they are kept, in the process, for
the time of the analysis), or, for a parameter whose argument was written in another place, the facts of each branch as they
were made there, with the subjects that the condition has in an operation.
The origin of the local of a depth and a name, or nil if it has none.
Facts and the kinds of subject they are about, which kinds/1 would say of them.
What a fact is about: a variable, or the entry or property of one that a path selects (p.age, then
{:path, "p", ["age"]}); a local, which is told by the depth of the context that binds it and its name
({:local, 2, "a"}, and {:local_path, 2, "a", ["age"]} for a.age, D-193); or, in the brackets of a filter, the
element of the filter of a depth or an entry of it ({:item_path, 1, []} for item, {:item_path, 1, ["x"]}
for x and for item.x).
Functions
The subjects that the condition compares through a call of a number function that is a hull (floor(x) = 2.5, exp(x) = 3,
round half up(x, 1) > 0.25): the names, locals, paths and elements of a filter that are read inside such a call, on
either side of a comparison or in an operation or a call of abs, sqrt or modulo on a side of it (floor(x) + 1 = 3.5,
abs(floor(x)) = 2.5), as the value of a between or its bounds or as the subject of an in, and in the operands of an and
or an or. The rounding functions, exp and log are the hulls (@hull_calls).
The facts about the paths of a chain of selections node.k1.k2…, by the number of links they are at: for the keys
["a", "b"] of p.a.b, the fact about p.a is at the first link and the one about p.a.b at the second. A fact about
p.x, or about p.a.c, is at none.
Returns the facts of left and right or left or right in a branch from the facts of the two operands in it, which
is what facts/2 makes of the node. A caller that has the facts of the left operand already (the analyzer, in a chain
of and or of or, where it has been made from the one before) needs no walk of the operands to have them.
Returns what the resolved condition says about its names in a branch of the conditional: in the
:then branch the condition was true, and in the :else branch it was not. The two outcomes that :else is made
of are asked by :false (the condition was false) and :null (it was null, or a value that is no boolean).
facts/2 for a condition that reads names that are bound to conditions: origins says, for the local of a depth and a name,
what it is bound to. A name that was bound to x > 5 and is read as a condition (if ok) says what x > 5 does in the branch,
besides what it says of ok: ok is the value that x > 5 had, and x is the value that it had.
Takes away what origin_of/2 kept under the key.
The subject of the element of a filter, or of an entry of it, that a name in the brackets reads
({:item_path, depth, keys}, with no keys for the element itself). Anything else has none.
The kinds of subject the facts are about, and the subjects that are paths from each name.
Returns the facts that hold when two sets of facts both do: a name that both narrow is narrowed by the first and then by the second.
Narrows the type of a name by a fact. A fact that leaves nothing says the branch cannot run, and
then any type will do, so the type is returned as it is (narrowed/2 tells the two apart). So is a type that the fact
leaves as it was; the type is in normal form (Feel.Type.Union), as the types of the analysis are.
Narrows the type of the source of a path by a fact about the path (D-194): a name p is read in a branch where the
condition said something of p.age, and p has to be a value that has that.
Narrows the type of a source by the facts about several of its paths: narrow_source/3 for each, in an order that keeps
the most (D-194). The facts are about the properties of a date, and a set of days is kept in at most 16 pieces: the
year leaves few, the month some more, and the day of the month and the weekday many, so d.month = 2 and d.day = 29
is the one day of the leap years, where the day first is a piece for each month and the one that holds them all. The
facts of the other paths follow, in the order they are given.
The type that narrow/2 gives, or :none when the fact leaves nothing of it: no value of the type is one that the
branch can have, so the place the type is read at cannot be reached (D-152).
The subjects that the condition has in an operation, or in an instance of: the names, locals, paths and elements of a filter
that are read inside an arithmetic operation, a negation, or the operand of an instance of. What a comparison of one says of them
is that the operation is not null (D-194), and for some sets of values it is (1 / x for the set 0, x ** 1000 for the very
large numbers) and for others it is not; what x instance of T says when it is not true depends on the type that x is as a whole.
Such a fact leaves a set as a whole, or none of it, and not the part of it that is in the set of the fact, which every other fact that
this module makes does. Where the analyzer answers a fact about a path from the part of a set that is in another, it does not
for a subject in an operation. A subject that is not in one has facts of the kind that it can, whatever the other
conditions of the chain are.
operation_subjects/1 for a condition that reads names bound to conditions (facts/3): the subjects that those conditions have
in an operation are among them, since what they say of the subject is what the facts of the condition say.
The origin a name has that is bound to the value of node: the node itself, if it is a condition or the name of a value
that says something when it is true, and nil for anything else (a number, a call, a list), which says nothing.
The types that a fact about the path keys from a source of the type root is applied to when the path is read, with what
the fact leaves of each: narrow_sources/2 applies it to the entry of each alternative of the source, and the path
node to the type that is selected from the source it leaves. The first are found by doing it, with a fact that notes what it
is given, and so is the type that is selected from the source as it was, which is the same at every term of a chain.
The facts that narrow a name or a local that is read, as {keys, fact}: the ones about the paths from it. For a read that is the
source of a chain of selections p.a.b (keys given), the facts at the links of the chain come first, and the others that
are about a path from p, up to 16 of them, follow. For any other read (keys is nil) it is the others.
The subject of a path that starts at a variable: {:path, name, keys}. A path that starts at
anything else has none.
The facts with the kinds of subject they are about. The analyzer asks of the facts around a name whether any is about a
path from it, and of the facts a condition leaves whether the right operand reads any: a look at all of them each time,
which in a chain of a thousand conditions is a million looks. The kinds of the facts of a chain are made from those of
the part of it before (combine/5).
Whether a read of any of the names is narrowed by the facts: one of them is about a name, or about a path from it. A fact about a local or an element of a filter is taken as read, since those are not names.
The values of a type that one unary test accepts, read as the condition of the input ? that it is (D-161). A constraint of
DMN, an allowedValues or a typeConstraint, is a unary test, so it says what a condition says: the same facts are read,
with the operations (? * 2 < 100), the properties of a date, a duration and a time (?.year >= 2020), the tests of an in
and the rest of what narrows a name.
Types
@type branch() :: :then | :else | false | :null
What a condition said, as the branch of a conditional is told: :then is where it was true, :else where it was not
(false, null, or a value that is no boolean, since if takes the else branch for them all, Table 50), and the
two outcomes that :else is made of: :false where it was false, and :null where it was null or no boolean
(D-194). not(c) is true where c is false, so it needs the facts of the outcomes of c.
@type fact() :: (Feel.Type.Union.t() -> [Feel.Type.Union.member()])
What is known about a name: the alternatives of its type that are left. It is empty when the fact leaves none, which says that the branch cannot be taken.
The facts about the names and the paths a condition reads, for a branch.
@type kinds() :: Feel.Analysis.Narrowing.Kinds.t()
The kinds of subject some facts are about (:name for a variable, and the tag of the others, as subject/0 has them),
and the subjects that are paths from each name and each local (Feel.Analysis.Narrowing.Kinds).
A key of the process dictionary that narrow_sources/3 keeps its answers under: nil keeps none.
@type memos() :: (subject(), fact() -> {[{Feel.Type.Union.t(), [Feel.Type.Union.member()]}], :answered | :applied})
What the caller knows of how a subject is read: given the subject and a fact about it that was made of others, the types the
fact will be applied to where it is read, each with what the fact leaves of it ([] if it does not know any), and whether
the reads are :answered by those or the fact has to be :applied to some type (one that it has no answer for). The fact
is made to answer those at once (combine/5).
@type origin() :: {:node, Feel.IR.t(), cache :: nil | {memo_key(), reference()}} | {:facts, %{required(branch()) => facts()}, MapSet.t(subject())}
What a name that is bound to the value of a condition says of the names the condition read (D-194): the
node of the condition, whose facts are made where the name is read, once (cache: where they are kept, in the process, for
the time of the analysis), or, for a parameter whose argument was written in another place, the facts of each branch as they
were made there, with the subjects that the condition has in an operation.
@type origins() :: (pos_integer(), Feel.Name.t() -> origin() | nil)
The origin of the local of a depth and a name, or nil if it has none.
Facts and the kinds of subject they are about, which kinds/1 would say of them.
@type subject() :: Feel.Name.t() | {:path, Feel.Name.t(), [Feel.Name.t(), ...]} | {:local, pos_integer(), Feel.Name.t()} | {:local_path, pos_integer(), Feel.Name.t(), [Feel.Name.t(), ...]} | {:item_path, pos_integer(), [Feel.Name.t()]} | {:item_entry, pos_integer(), Feel.Name.t(), Feel.Type.Union.t()}
What a fact is about: a variable, or the entry or property of one that a path selects (p.age, then
{:path, "p", ["age"]}); a local, which is told by the depth of the context that binds it and its name
({:local, 2, "a"}, and {:local_path, 2, "a", ["age"]} for a.age, D-193); or, in the brackets of a filter, the
element of the filter of a depth or an entry of it ({:item_path, 1, []} for item, {:item_path, 1, ["x"]}
for x and for item.x).
Functions
The subjects that the condition compares through a call of a number function that is a hull (floor(x) = 2.5, exp(x) = 3,
round half up(x, 1) > 0.25): the names, locals, paths and elements of a filter that are read inside such a call, on
either side of a comparison or in an operation or a call of abs, sqrt or modulo on a side of it (floor(x) + 1 = 3.5,
abs(floor(x)) = 2.5), as the value of a between or its bounds or as the subject of an in, and in the operands of an and
or an or. The rounding functions, exp and log are the hulls (@hull_calls).
It is where a fact says more of a name than the type of the condition does. The type of a comparison is decided from the sets of
its two sides (Feel.Analysis.Inference.compare/3), and the set of the result of such a call is a hull: floor(x) is
number, or an interval of the numbers, and floor(x) = 2.5 is undecided for it, though no number gives a result that is not
whole. The fact is the numbers that give a result in the set the comparison leaves (Feel.Analysis.Images.Number.Preimage), and it
leaves none. The comparison of a name, of an operation on names, or of a call that gives an interval for an interval, is
decided by the sets as the facts are, and a fact there leaves nothing only where the type of the condition has said so already
(D-194 has the measure of that).
iex> {:ok, ast} = Feel.parse("floor(x) = 2.5 and y > 1 and exp(z) < 3")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> Feel.Analysis.Narrowing.call_subjects(ir)
["x", "z"]
iex> {:ok, ast} = Feel.parse("abs(floor(x)) = 2.5 or x + 1 = 3 or abs(y) < 2 or count(s) = 1")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> Feel.Analysis.Narrowing.call_subjects(ir)
["x"]
@spec chain_facts(Feel.IR.t(), facts(), [Feel.Name.t(), ...]) :: %{ optional(pos_integer()) => fact() }
The facts about the paths of a chain of selections node.k1.k2…, by the number of links they are at: for the keys
["a", "b"] of p.a.b, the fact about p.a is at the first link and the one about p.a.b at the second. A fact about
p.x, or about p.a.c, is at none.
The analyzer asks it once for a chain and gives each link the fact that is at it (and path_sources/3 gives the source
the same, first). A source that has a condition on each of n different paths from it has n facts, and a chain is
asked of them without looking at all of them (D-154): one look-up for each link, in the facts. A chain that is longer
than there are facts to look at is the other way round, the facts are looked at (a look-up of each link makes the key of
each, which is n for the n-th).
iex> {:ok, ast} = Feel.parse("p.age > 5")
iex> {:ok, %{ir: {:compare, _, :gt, {:path, _, source, "age"}, _} = condition}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> facts = Feel.Analysis.Narrowing.facts(condition, :then)
iex> %{1 => fact} = Feel.Analysis.Narrowing.chain_facts(source, facts, ["age"])
iex> Feel.Analysis.Narrowing.narrow([:any, :null], fact) |> Feel.Type.Union.format()
"number in (5..max]"
iex> Feel.Analysis.Narrowing.chain_facts(source, facts, ["name"])
%{}
Returns the facts of left and right or left or right in a branch from the facts of the two operands in it, which
is what facts/2 makes of the node. A caller that has the facts of the left operand already (the analyzer, in a chain
of and or of or, where it has been made from the one before) needs no walk of the operands to have them.
A conjunction is true when both are, so there the facts of both hold; it is not when either is not, so a name is
narrowed only if both narrow it. A disjunction is the other way round.
memos says, of a subject that a conjunction has two facts about, the types it is read at and what the fact of the two
leaves of each. The fact is then made to answer those types with that, and any other as it would have been narrowed without.
A chain of n conditions about a name is then n steps, where a read of the name in the n-th applied n facts (D-194).
iex> fact = fn type -> Enum.reject(type, &(&1 == :null)) end
iex> left = Feel.Analysis.Narrowing.profile(%{"x" => fact})
iex> right = Feel.Analysis.Narrowing.profile(%{"y" => fact})
iex> {{facts, kinds}, applied} = Feel.Analysis.Narrowing.combine(:and, :then, left, right)
iex> {Map.keys(facts), Feel.Analysis.Narrowing.Kinds.to_list(kinds), applied}
{["x", "y"], [:name], []}
iex> {{facts, _kinds}, []} = Feel.Analysis.Narrowing.combine(:and, :else, left, right)
iex> facts
%{}The subjects that two facts were made one of, and that memos did not answer (:applied), are given with the facts: a
read of one applies as many facts as it was made of, and the analyzer counts them (max_narrowing_facts, D-154).
Returns what the resolved condition says about its names in a branch of the conditional: in the
:then branch the condition was true, and in the :else branch it was not. The two outcomes that :else is made
of are asked by :false (the condition was false) and :null (it was null, or a value that is no boolean).
Every set of facts is a superset of the states that have its outcome, so a shape that is not read says nothing, and
the facts of :false and :null are never narrower than the run has them. Where a shape has no rule of its own for
them they are the facts of :else, which holds both.
iex> {:ok, ast} = Feel.parse("not(x = null)")
iex> {:ok, %{ir: condition}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> %{"x" => fact} = Feel.Analysis.Narrowing.facts(condition, :then)
iex> Feel.Analysis.Narrowing.narrow([:number, :null], fact) |> Feel.Type.Union.format()
"number"
facts/2 for a condition that reads names that are bound to conditions: origins says, for the local of a depth and a name,
what it is bound to. A name that was bound to x > 5 and is read as a condition (if ok) says what x > 5 does in the branch,
besides what it says of ok: ok is the value that x > 5 had, and x is the value that it had.
iex> {:ok, ast} = Feel.parse("{ok: x > 5, r: if ok then x else 0}")
iex> {:ok, %{ir: {:context, _, entries}}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> %{"ok" => ok, "r" => {:if, _, {:local, %{id: id}, "ok"} = read, _, _}} = Map.new(entries)
iex> origins = fn ^id, "ok" -> Feel.Analysis.Narrowing.origin_of(ok, nil) end
iex> %{"x" => fact} = Feel.Analysis.Narrowing.facts(read, :then, origins)
iex> Feel.Analysis.Narrowing.narrow([:number, :null], fact) |> Feel.Type.Union.format()
"number in (5..max]"
@spec forget(memo_key()) :: :ok
Takes away what origin_of/2 kept under the key.
The subject of the element of a filter, or of an entry of it, that a name in the brackets reads
({:item_path, depth, keys}, with no keys for the element itself). Anything else has none.
iex> Feel.Analysis.Narrowing.item_subject({:item, %{span: {0, 4}, type: [:number], depth: 1}})
{:ok, {:item_path, 1, []}}
iex> Feel.Analysis.Narrowing.item_subject({:item_field, %{span: {0, 1}, type: [:number], depth: 2}, "a"})
{:ok, {:item_path, 2, ["a"]}}
iex> Feel.Analysis.Narrowing.item_subject({:const, %{span: {0, 1}, type: [:null]}, nil})
:error
The kinds of subject the facts are about, and the subjects that are paths from each name.
Returns the facts that hold when two sets of facts both do: a name that both narrow is narrowed by the first and then by the second.
iex> fact = fn type -> Enum.reject(type, &(&1 == :null)) end
iex> facts = Feel.Analysis.Narrowing.meet(%{"x" => fact}, %{"x" => fact, "y" => fact})
iex> Map.keys(facts)
["x", "y"]
@spec narrow(Feel.Type.Union.t(), fact()) :: Feel.Type.Union.t()
Narrows the type of a name by a fact. A fact that leaves nothing says the branch cannot run, and
then any type will do, so the type is returned as it is (narrowed/2 tells the two apart). So is a type that the fact
leaves as it was; the type is in normal form (Feel.Type.Union), as the types of the analysis are.
iex> fact = fn type -> Enum.reject(type, &(&1 == :null)) end
iex> Feel.Analysis.Narrowing.narrow([:number, :null], fact)
[:number]
iex> Feel.Analysis.Narrowing.narrow([:null], fact)
[:null]
@spec narrow_source(Feel.Type.Union.t(), [Feel.Name.t(), ...], fact()) :: Feel.Type.Union.t()
Narrows the type of the source of a path by a fact about the path (D-194): a name p is read in a branch where the
condition said something of p.age, and p has to be a value that has that.
An alternative of the source stays if the selection from it can be what the fact leaves, and goes if it cannot: a
Null and a number select Null, so p.age > 5 leaves neither. A context type that lists the entry has it narrowed;
one that does not list it gets it listed, when what the fact leaves is not null, for then the entry is there (an
entry that is not there is selected as null). Any is taken kind by kind where the fact leaves no null, since no
value of a function has an entry. A path of several keys narrows the entries in turn. Like narrow/2 it returns
the type as it is when nothing is left, for the branch cannot run.
iex> source = [{:context, %{"age" => [:number, :null]}}, :null]
iex> {:ok, ast} = Feel.parse("p.age > 5")
iex> {:ok, %{ir: condition}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> %{{:path, "p", ["age"]} => fact} = Feel.Analysis.Narrowing.facts(condition, :then)
iex> Feel.Analysis.Narrowing.narrow_source(source, ["age"], fact) |> Feel.Type.Union.format()
"context<age: number in (5..max]>"
@spec narrow_sources( Feel.Type.Union.t(), [{[Feel.Name.t(), ...], fact()}], memo_key() ) :: Feel.Type.Union.t()
Narrows the type of a source by the facts about several of its paths: narrow_source/3 for each, in an order that keeps
the most (D-194). The facts are about the properties of a date, and a set of days is kept in at most 16 pieces: the
year leaves few, the month some more, and the day of the month and the weekday many, so d.month = 2 and d.day = 29
is the one day of the leap years, where the day first is a piece for each month and the one that holds them all. The
facts of the other paths follow, in the order they are given.
memo is a key under which the answers are kept in the process (nil, the default, keeps none): a fact is a function of
its argument, and the reads of a name in a chain of conditions are narrowed by nearly the same facts in the same order
(D-154). Whoever gives a key takes the answers away with Process.delete/1 when it is done with them; the analyzer does,
at the end of analyze/2.
iex> {:ok, ast} = Feel.parse("d.day = 29 and d.month = 2")
iex> {:ok, %{ir: condition}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> facts = Feel.Analysis.Narrowing.facts(condition, :then)
iex> sources = for {{:path, "d", keys}, fact} <- facts, do: {keys, fact}
iex> days = Feel.Type.NumberSet.interval(Decimal.new(Date.to_gregorian_days(~D[2019-01-01])), Decimal.new(Date.to_gregorian_days(~D[2022-12-31])))
iex> Feel.Analysis.Narrowing.narrow_sources([{:date, days}], sources) |> Feel.Type.Union.format()
"date = 2020-02-29"
@spec narrowed(Feel.Type.Union.t(), fact()) :: {:ok, Feel.Type.Union.t()} | :none
The type that narrow/2 gives, or :none when the fact leaves nothing of it: no value of the type is one that the
branch can have, so the place the type is read at cannot be reached (D-152).
iex> fact = fn type -> Enum.reject(type, &(&1 == :null)) end
iex> Feel.Analysis.Narrowing.narrowed([:number, :null], fact)
{:ok, [:number]}
iex> Feel.Analysis.Narrowing.narrowed([:null], fact)
:none
The subjects that the condition has in an operation, or in an instance of: the names, locals, paths and elements of a filter
that are read inside an arithmetic operation, a negation, or the operand of an instance of. What a comparison of one says of them
is that the operation is not null (D-194), and for some sets of values it is (1 / x for the set 0, x ** 1000 for the very
large numbers) and for others it is not; what x instance of T says when it is not true depends on the type that x is as a whole.
Such a fact leaves a set as a whole, or none of it, and not the part of it that is in the set of the fact, which every other fact that
this module makes does. Where the analyzer answers a fact about a path from the part of a set that is in another, it does not
for a subject in an operation. A subject that is not in one has facts of the kind that it can, whatever the other
conditions of the chain are.
iex> {:ok, ast} = Feel.parse("1 / x > 0 and y > 1")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> Feel.Analysis.Narrowing.operation_subjects(ir)
MapSet.new(["x"])
iex> {:ok, ast} = Feel.parse("p.age + 1 > 0 and not(p.age instance of number) and y = 1")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> Feel.Analysis.Narrowing.operation_subjects(ir) |> Enum.sort()
[{:path, "p", ["age"]}, "p"]
operation_subjects/1 for a condition that reads names bound to conditions (facts/3): the subjects that those conditions have
in an operation are among them, since what they say of the subject is what the facts of the condition say.
The origin a name has that is bound to the value of node: the node itself, if it is a condition or the name of a value
that says something when it is true, and nil for anything else (a number, a call, a list), which says nothing.
The facts of the node are made when the name is read as a condition, and are kept under memo (a key of the process, which
whoever gives it takes away with forget/1 when it is done with it; nil keeps nothing): a name that is bound to a condition
that reads a name that is bound to one ({a1: x > 1, a2: a1 and x > 2, …}) would otherwise make the facts of the chain again
at each read, and apply them one after the other.
@spec path_reads(Feel.Type.Union.t(), [Feel.Name.t(), ...], fact(), boolean()) :: {[{Feel.Type.Union.t(), [Feel.Type.Union.member()]}], :answered | :applied}
The types that a fact about the path keys from a source of the type root is applied to when the path is read, with what
the fact leaves of each: narrow_sources/2 applies it to the entry of each alternative of the source, and the path
node to the type that is selected from the source it leaves. The first are found by doing it, with a fact that notes what it
is given, and so is the type that is selected from the source as it was, which is the same at every term of a chain.
The type selected from the source that is left is the one that changes with each term, and it is answered without applying the fact to it (D-194), in the cases that follow, and in no other:
- it is what is selected from the source as it was;
- it is within that, and it and what the fact leaves of that have only sets in them (numbers, strings, booleans, the
amounts and days of a duration or a date) and
Null: a fact that leaves of a set what is in a set of its own leaves of a part of it the part that is in that set (restrict/3), so it leaves of it what it leaves of that, in it; - every alternative of the source that is left is
Nullor a context that has the entry, so what is selected is what the fact left of the entries, and a fact applied to what it left of a type leaves that.
The fact is applied to it as to any other type if none of them is so, and when claims? is false: a condition on an operation
(1 / x > 0) leaves of a set what depends on the set, as a whole, and not the part of it that is in another (operation_subjects/1).
That is the second thing that is given with the reads, :applied; they are :answered when the fact was not applied to anything that
changes with each term.
iex> fact = fn type -> Enum.reject(type, &(&1 == :null)) end
iex> root = [{:context, %{"age" => [:number, :null]}}, :null]
iex> {reads, :answered} = Feel.Analysis.Narrowing.path_reads(root, ["age"], fact)
iex> for {input, output} <- reads, do: {Feel.Type.Union.format(input), Feel.Type.Union.format(output)}
[{"number | Null", "number"}, {"Null", "Never"}, {"number", "number"}]
@spec path_sources(Feel.IR.t(), facts(), [Feel.Name.t(), ...] | nil, kinds() | nil) :: [ {[Feel.Name.t(), ...], fact()} ]
The facts that narrow a name or a local that is read, as {keys, fact}: the ones about the paths from it. For a read that is the
source of a chain of selections p.a.b (keys given), the facts at the links of the chain come first, and the others that
are about a path from p, up to 16 of them, follow. For any other read (keys is nil) it is the others.
The others are taken from the facts as they come, until there are enough, so a name with few facts has them all (and is narrowed by all of them, as it is by all there are), and a name with many has some of them.
With the kinds of the facts (kinds/1, or the ones a profile has) a name that no fact is about a path from is read
without a look at the facts, and a name that some are about is read with a look at those: the work of a read is that of
the facts about the name, and not of all the facts, which in a chain of a condition on a path from a different name each
was a look at the facts of every name before it, for each read. The others are the same facts, in the same order, as
without the kinds.
iex> {:ok, ast} = Feel.parse("p.age > 5 and p.size < 3")
iex> {:ok, %{ir: {:logic, _, :and, _, _} = ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> {:logic, _, :and, left, right} = ir
iex> facts = Feel.Analysis.Narrowing.facts(left, :then)
iex> {:compare, _, :lt, {:path, _, source, "size"}, _} = right
iex> [{["age"], _fact}] = Feel.Analysis.Narrowing.path_sources(source, facts, ["size"])
iex> Feel.Analysis.Narrowing.path_sources(source, facts, nil) |> length()
1
iex> kinds = Feel.Analysis.Narrowing.kinds(facts)
iex> [{["age"], _fact}] = Feel.Analysis.Narrowing.path_sources(source, facts, ["size"], kinds)
iex> Feel.Analysis.Narrowing.path_sources({:var, %{}, "q"}, facts, nil, kinds)
[]
The subject of a path that starts at a variable: {:path, name, keys}. A path that starts at
anything else has none.
iex> {:ok, ast} = Feel.parse("p.address.city")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> Feel.Analysis.Narrowing.path_subject(ir)
{:ok, {:path, "p", ["address", "city"]}}
iex> {:ok, ast} = Feel.parse("{p: 1}.p")
iex> {:ok, %{ir: ir}} = Feel.Analysis.Analyzer.analyze(ast, [])
iex> Feel.Analysis.Narrowing.path_subject(ir)
:error
The facts with the kinds of subject they are about. The analyzer asks of the facts around a name whether any is about a
path from it, and of the facts a condition leaves whether the right operand reads any: a look at all of them each time,
which in a chain of a thousand conditions is a million looks. The kinds of the facts of a chain are made from those of
the part of it before (combine/5).
iex> fact = fn type -> type end
iex> {_facts, kinds} = Feel.Analysis.Narrowing.profile(%{"x" => fact, {:path, "p", ["age"]} => fact})
iex> Feel.Analysis.Narrowing.Kinds.to_list(kinds)
[:name, :path]
@spec reads_a_fact?(profiled(), MapSet.t(Feel.Name.t())) :: boolean()
Whether a read of any of the names is narrowed by the facts: one of them is about a name, or about a path from it. A fact about a local or an element of a filter is taken as read, since those are not names.
It looks at the facts or at the names, whichever there are fewer of, and in the names at a look-up in the facts and in the kinds: the right operand of a chain of conditions reads a few names, and the facts around it are of all that went before.
iex> fact = fn type -> type end
iex> profiled = Feel.Analysis.Narrowing.profile(%{{:path, "p", ["age"]} => fact})
iex> Feel.Analysis.Narrowing.reads_a_fact?(profiled, MapSet.new(["p", "q"]))
true
iex> Feel.Analysis.Narrowing.reads_a_fact?(profiled, MapSet.new(["q"]))
false
@spec restrict_by_test(Feel.Type.Union.t(), Feel.IR.t(), branch()) :: Feel.Type.Union.t()
The values of a type that one unary test accepts, read as the condition of the input ? that it is (D-161). A constraint of
DMN, an allowedValues or a typeConstraint, is a unary test, so it says what a condition says: the same facts are read,
with the operations (? * 2 < 100), the properties of a date, a duration and a time (?.year >= 2020), the tests of an in
and the rest of what narrows a name.
The type is that of the input, which has no null (the tests do not run for it), and the result has none. :then is the
values for which the test is true, :else those for which it is not true (false or null), a superset of where it is
false, which not(...) needs. A test that says nothing of ? accepts every value, and the type is returned as it is. One
that no value passes gives [].
iex> {:ok, ast} = Feel.parse("? * 2 < 100", entry: :unary_tests)
iex> {:ok, %{ir: {:unary_tests, _, _, [test]}}} = Feel.Analysis.Analyzer.analyze(ast, input: :number, declarations: :ignore)
iex> Feel.Analysis.Narrowing.restrict_by_test([:number], test, :then) |> Feel.Type.Union.format()
"number in [-max..50)"