Feel.Analysis.Inference (feel v0.1.0)

Copy Markdown View Source

The static result types of operations, for Feel.Analysis.Analyzer.

A type here is a Feel.Type.Union, a list of alternatives with :null among them when the value may be null (D-39). Given the types of the operands, each function returns {:ok, type} or {:mismatch, type}; the second means that the operation is a type mismatch, so null with an error, whenever no operand is null.

The mirror of this module at run time is Feel.Runtime; a test runs both on the same operands and checks that the value of the one is among the alternatives of the other.

How a union is typed

Each pair of alternatives, one of each operand, is typed alone, and the result is the union of what the pairs give. [:number, :null] + [:number] is the pairs number + number, which is number | null, and null + number, which is null. A pair that is a type mismatch gives null.

The result is {:mismatch, type} when every pair without a null is a type mismatch, and there is one: the operation is an error unless an operand is null. The type is the same union as for {:ok, type}; it is [:null] except for = and !=, which are false and true for a null operand. A union with some pairs that are mismatches is a possible mismatch, which is not reported.

Value sets

An alternative can be a set of values (D-39): {:number, set}, {:string, strings}, or {:boolean, true} or {:boolean, false}; the bare :number, :string and :boolean are every value of the kind. For numbers the operation works on the set (Feel.Analysis.Intervals): the result is the set of what the operation can give, and it is null for lack of range only where the intervals say it can be, so [0..150] + 1 is [1..151] and never null in itself, and a divisor that does not have zero in it is never zero. A comparison of two sets that settles it is the boolean it settles to: [0..150] < 200 is {:boolean, true}. The strings of sets are joined for + and compared for = and !=; a boolean that is known decides and and or.

This module works out the kind of a result and whether it can be null, and puts the sets into the alternatives. What the sets of the operands come to, and whether they decide a comparison, is asked of Feel.Analysis.Refinement.Operations (D-133), which calls the modules named below; this module calls none of them.

A date and the two kinds of duration have sets too (D-109), which are sets of the numbers that stand for the values (Feel.Type.Image: the days of a date, the months or the seconds of a duration), so the same intervals work on them. The seconds of PT1H + PT2H are 3600 + 7200, and PT1H * 2 is the seconds times 2: the result is the set of the durations, and null where the numbers can overflow or divide by zero, as for a number. The months of a years and months duration do not overflow at run time, so a sum of them that the numbers say could pass the range is any years and months duration, and not null. Two dates, or two durations of one kind, are compared by their numbers: @"PT1H" < @"P1D" is {:boolean, true}. What the numbers do not carry is worked out from the sets of both: a years and months duration times a number, which is rounded (D-54) and so is not a product of images, has the months rounded as the run does (Feel.Analysis.MonthScaling, D-109), and a date plus or minus a duration, which is not a sum of images either, since the day of the month is clamped (2024-01-31 + P1M), has the dates of Feel.Analysis.DateArithmetic (D-109). A time and a date and time have sets of the second they fall in, on three lines, for the clock and for the instant, since their order depends on a zone (Feel.Type.Moment, D-109): a comparison of two of them is decided by Feel.Analysis.MomentTests, a time or a date and time plus or minus a duration has the second that the sets come to (Feel.Analysis.MomentArithmetic, D-109), and so does the difference of two of them (D-109).

The types it takes

The types are plain (Feel.IR.plain_type/1, D-152, D-152): a union whose members are the kinds whole. A kind less some of it and the empty list (Feel.Type.Union.positive/1 makes the list of nothing of it) are for what passes a type on and for the narrowing, and a function here that is given one takes it for a kind it does not know.

What a pair gives

null as an operand gives null, except for = and != (Table 50), which never give null because of an operand, and for and and or, where a false or a true decides. An alternative of the type :any or a named type is not known, so the result is [:any, :null] or [:boolean, :null], unless the other operand decides it alone: a kind with no order for < and > (D-196), a kind with no operation for + - * / ** (D-203); or unless the kinds say more: an operation on a kind that has an operation and one that is not known is the union of what the pairs give (D-203). The rest is below.

Arithmetic

A number operation can give null: it overflows past decimal128, it can be too small to write, / divides by zero, and ** has no value for a zero to a negative power or a negative number to a power that is not whole. Whether it does is decided by the sets of the operands (see above), so on any number it can, and on numbers that stay away from the ends of the range and from zero it cannot: 0 ** x for an x in [5..10] is the number 0, and x ** 2 is [25..100] (D-161).

OperatorOperandsResult
+two stringsstring
+ -two years and months durationsyears and months duration (never null in itself)
+ -two days and time durationsdays and time duration, null
+ - * / **two numbersnumber, null
*a days and time duration and a number, in either orderdays and time duration, null
/a days and time duration and a numberdays and time duration, null
/two durations of one kindnumber, null
* /a years and months duration and a numberyears and months duration, a set of the months rounded as the run does, and null (D-109)
+ -a date and a durationdate, a set of the days the run can give, and null where the year can leave the range (D-109)
+ -a date and time and a durationdate and time, a set of the seconds the run can give, and null where the clock can leave the calendar (D-109)
+ -a time and a days and time durationtime, a set of the seconds of the day round the day (D-109)
-two datesdays and time duration (a set, from the days between)
-two dates and times, two times, a date and a date and timedays and time duration, a set of the seconds between them to a second either way, and null (one has a zone, one has not, D-109)
negationa number or a durationthe same type

Every other pair of kinds is a mismatch, since the run does the operations of the temporal tables (D-54) and gives a type mismatch for the rest.

An operand of a kind that an operator has no operation for is a mismatch against any other operand, Any and a named type included (D-203): true + v is null for every v, an error for one that is not null. A boolean, a list, a range, a context, a function and a term that is no value of FEEL have no operation at all; a string has + and no other; a date, a time and a date and time have + and -; a duration has all but **; only a number has **. So true + v, [1] - v, "a" - v and d * v are Null. A kind that has an operation against one that is not known (Any, a named type) is a value of the kinds that its pairs give with a kind of the unknown one, or null (D-203): the unknown one is a value of every kind, a pair that no table names is null, and a null is null. x + v is number | Null (a number is added to a number only), "a" + v is string | Null, d + v is date | Null, x * v is number | years and months duration | days and time duration | Null, and v + w is the seven kinds that have a +, and Null. The pairs are of the kinds whole, so the set of values of the known operand does not cut the result (it is as narrow as the kinds say, and it is a function of the operator and the kinds, which is kept). -v is a number, a years and months duration, a days and time duration, or Null (D-207): the kinds that a negation has. One unknown value read twice (v - v, D-170, see arithmetic_of_one/3) is the kinds of the pair of a kind with itself, and Null. A union is null only if every pair of its alternatives is.

Negation cannot overflow, and a result that is not null is assumed for a number that is well formed: at most 34 digits and in the exponent range. A bound number that is not is null at run time and the type does not say so.

Comparison

= and != give boolean and never null for operands of one primitive kind, or when an operand is null. Lists, contexts and ranges can hold elements that cannot be compared, so they give boolean and null. <, <=, > and >= give null for a null operand, and boolean for two numbers, two strings, two dates, two times, two dates and times, or two durations of one kind (a time or a date and time with a zone that the time zone database does not have would be null, and a value of FEEL has none). Operands of one kind that has no order, such as two booleans, are a mismatch (null, with a type_mismatch warning, D-193), for <= and >= as for < and >: true <= true is null (the domain reading of D-196, D-196). Operands of different kinds are a mismatch, a date and a date and time, or the two kinds of duration, included (notes 02, A-38; D-54).

An operand that has no order (a boolean, a list, a range, a context, a function, a term that is no value of FEEL) is a mismatch against any other operand, Any and a named type included (D-196): true < v and true <= v are null for every v, an error for one that is not null.

Logic

and and or give boolean for two booleans, and boolean and null when an operand can be null or is not a boolean but the other is, since a false decides a conjunction and a true a disjunction whatever the other operand is. An operand that is never a boolean (null, a number, a string) leaves one outcome out: b and null is false or null and never true, b or null is true or null and never false (Tables 51 and 52, Feel.Runtime.Logic; D-170). When no operand can be a boolean the type is null, and not a mismatch: the run gives a plain null for an operand that is not a boolean (D-103).

Conditional

if is the one construct here that cannot be a mismatch, and conditional/3 returns a type and not a result. The then branch is taken for a condition that is true and the else branch for anything else, null and a value that is not a boolean included (Table 50), so the condition adds nothing to the type. The type is the union of the types of the branches that can be taken (reachable/1): a condition that is true for sure leaves the then branch, and one that cannot be true leaves the else branch.

Summary

Types

How fine the numbers of each operand are (Feel.Analysis.Grain, D-161): a multiple of 10^-d, or nil. It lets a product or a quotient of numbers that are not near zero be one that is never too small to write (D-161).

An arithmetic operator, as in Feel.IR.arithmetic_operator/0.

What a function of this module returns.

What the analyzer knows of a node besides its type (D-193): its type, and its structure. A list literal, a constant list, and a context bound to one have a structure: the shapes of the elements of a list of up to 32, or only the number of them; the shapes of the entries of a closed context of up to 32. Anything else has :none.

Functions

Returns the type of left <operator> right.

Returns the type of x <operator> x for one value x that is read twice (D-170), given the type of x: the two operands are not two values of the type, so the pairs of two different alternatives are not there, and x - x is 0 (a date and a time give the zero duration), x / x is 1 unless x is 0, x * x is not below 0, and x + x is 2x. An alternative that is not a number, or a duration or a date for -, or a duration for /, is typed as it is for two values. null is an alternative of the result for an x that is, as it is for two. An x of an unknown type (Any, a named type) is one value of one kind, the same at both ends (D-207): the union, over the kinds that the operator has an operation for, of the type for that kind, and null. x - x is 0 of a number, a years and months duration or a days and time duration, x + x is a number, a string or a duration, x * x, x / x and x ** x are a number; the pairs of two kinds (a date and a duration) that x - y has are not there.

Returns the type of value between low and high (D-69), which is value >= low and value <= high: the types of the two comparisons, and then of the three-valued and. So it is true when the sets of the value and of the bounds put it between them, false when they put it outside, null when an operand is null (but false if the other bound already puts it outside), and a type mismatch only when both comparisons are one: 1 between "a" and "b".

Returns the type of if … then then_type else else_type for the branches that can be taken, {then?, else?}: the union of their types. At least one is taken. reachable/1 has them for the type of the condition; the analyzer also leaves out a branch whose facts leave a name no value (D-152).

Returns the type of left <operator> right for a comparison.

Returns the type of if condition then then_type else else_type: the union of the types of the branches that reachable/1 says can be taken.

Returns the type of left and right when right was typed knowing that left is true (D-194): the names that left says something about have, in right, the types that it leaves them (Feel.Analysis.Narrowing).

Returns the type of left and right when right was typed twice (D-194): as right_when_true for the case that left is true, and as right_otherwise for the case that it is not. Where conjunction_after/2 can only say that a left that is null gives false or null, this knows what the right is for the names as they are then: x >= 1 and x <= 10 for an x of [1..3] that may be null is true, and null where x is.

Returns the type of left or right when right was typed knowing that left is not true (D-194): the names that left says something about when it is false or null, or not a boolean, have, in right, the types that it leaves them (Feel.Analysis.Narrowing).

Returns the type of value instance of type (Table 62), given the type of the value and the type, resolved.

Returns the type of a test of in that is an expression, x in [1, 2, 3] (D-61), given the type of the expression and whether it reads the input ?.

Returns the type of left and right or left or right.

Returns the type of subject in tests (D-61), given the types of the tests.

Returns the type of -operand. An operand of an unknown type (Any, a named type) is a number, a years and months duration, a days and time duration, or null (D-207): the kinds that a negation has.

Which bounds on the operands of left <operator> right would rule null out (D-161), when neither operand can be null: :near_zero for operands at least 0.001 away from zero, :too_large for operands of at most 1E+30, :range where the result needs both, and nil where it is not null, where it is certain (two single numbers, which is a warning of its own, D-161), or where these bounds do not rule it out. It is what lets the analyzer say, in an info, that tighter allowed values would rule null out, and the numbers say so: the operands are restricted as the hint says, and the result has no null left.

Returns the type of the range literal [from..to] (D-61), given the types of its endpoints.

Returns which branches of if condition then … else … a run can take, as {then?, else?}.

Returns the type of left = right and left != right for two nodes whose structure is known (D-193, D-193, D-161).

Types

comparison_operator()

@type comparison_operator() :: Feel.IR.comparison_operator()

A comparison operator, as in Feel.IR.comparison_operator/0.

grains()

@type grains() :: {integer() | nil, integer() | nil}

How fine the numbers of each operand are (Feel.Analysis.Grain, D-161): a multiple of 10^-d, or nil. It lets a product or a quotient of numbers that are not near zero be one that is never too small to write (D-161).

operator()

@type operator() :: Feel.IR.arithmetic_operator()

An arithmetic operator, as in Feel.IR.arithmetic_operator/0.

result()

@type result() :: {:ok, Feel.Type.Union.t()} | {:mismatch, Feel.Type.Union.t()}

What a function of this module returns.

shape()

@type shape() :: %{type: Feel.Type.Union.t(), structure: structure()}

What the analyzer knows of a node besides its type (D-193): its type, and its structure. A list literal, a constant list, and a context bound to one have a structure: the shapes of the elements of a list of up to 32, or only the number of them; the shapes of the entries of a closed context of up to 32. Anything else has :none.

structure()

@type structure() ::
  :none
  | {:list, [shape()]}
  | {:size, non_neg_integer()}
  | {:context, %{optional(Feel.Name.t()) => shape()}}

Functions

arithmetic(operator, left, right, grains \\ {nil, nil})

@spec arithmetic(operator(), Feel.Type.Union.t(), Feel.Type.Union.t(), grains()) ::
  result()

Returns the type of left <operator> right.

iex> Feel.Analysis.Inference.arithmetic(:add, [:string], [:string])
{:ok, [:string]}
iex> Feel.Analysis.Inference.arithmetic(:add, [:number], [:number])
{:ok, [:number, :null]}
iex> Feel.Analysis.Inference.arithmetic(:add, [:number, :null], [:null])
{:ok, [:null]}
iex> Feel.Analysis.Inference.arithmetic(:add, [:number], [:string])
{:mismatch, [:null]}
iex> Feel.Analysis.Inference.arithmetic(:add, [:number], [:number, :string])
{:ok, [:number, :null]}

arithmetic_of_one(operator, operand, grains \\ {nil, nil})

@spec arithmetic_of_one(operator(), Feel.Type.Union.t(), grains()) :: result()

Returns the type of x <operator> x for one value x that is read twice (D-170), given the type of x: the two operands are not two values of the type, so the pairs of two different alternatives are not there, and x - x is 0 (a date and a time give the zero duration), x / x is 1 unless x is 0, x * x is not below 0, and x + x is 2x. An alternative that is not a number, or a duration or a date for -, or a duration for /, is typed as it is for two values. null is an alternative of the result for an x that is, as it is for two. An x of an unknown type (Any, a named type) is one value of one kind, the same at both ends (D-207): the union, over the kinds that the operator has an operation for, of the type for that kind, and null. x - x is 0 of a number, a years and months duration or a days and time duration, x + x is a number, a string or a duration, x * x, x / x and x ** x are a number; the pairs of two kinds (a date and a duration) that x - y has are not there.

iex> Feel.Analysis.Inference.arithmetic_of_one(:subtract, [:number]) |> elem(1) |> Feel.Type.Union.format()
"number = 0"
iex> Feel.Analysis.Inference.arithmetic_of_one(:subtract, [:number, :null]) |> elem(1) |> Feel.Type.Union.format()
"number = 0 | Null"
iex> Feel.Analysis.Inference.arithmetic_of_one(:divide, [:number]) |> elem(1) |> Feel.Type.Union.format()
"number = 1 | Null"
iex> Feel.Analysis.Inference.arithmetic_of_one(:multiply, [:string])
{:mismatch, [:null]}
iex> Feel.Analysis.Inference.arithmetic_of_one(:subtract, [:any]) |> elem(1) |> Feel.Type.Union.format()
"number = 0 | years and months duration = P0M | days and time duration = PT0S | Null"
iex> Feel.Analysis.Inference.arithmetic_of_one(:multiply, [:any]) |> elem(1) |> Feel.Type.Union.format()
"number in [0..max] | Null"

between(value, low, high, after_lower \\ nil)

Returns the type of value between low and high (D-69), which is value >= low and value <= high: the types of the two comparisons, and then of the three-valued and. So it is true when the sets of the value and of the bounds put it between them, false when they put it outside, null when an operand is null (but false if the other bound already puts it outside), and a type mismatch only when both comparisons are one: 1 between "a" and "b".

after_lower is {value, high}, the types of the value and of the upper bound when they were typed knowing that the lower comparison is true (D-194), as the right operand of an and is (conjunction_after/2): x between 5 and 3 for an x in [0..10] is then false, since for an x of [5..10] the upper comparison is. Without it the two comparisons are typed apart.

{value, high, {value_otherwise, high_otherwise}} adds the types of the value and of the upper bound where the lower comparison is not true. The upper comparison is typed for each case of the lower one, as the range of an in is (conjunction_cases/3, D-194): n between 0 and 10 for an n in [0..10] that may be null is true where the lower comparison is, and null where it is not, the n being null there. An upper bound that reads a name that the lower comparison narrows has another type there (n between 0 and n).

iex> Feel.Analysis.Inference.between([:number], [:number], [:number])
{:ok, [:boolean]}
iex> Feel.Analysis.Inference.between([:number, :null], [:number], [:number])
{:ok, [:boolean, :null]}
iex> Feel.Analysis.Inference.between([:number], [:string], [:string])
{:mismatch, [:null]}
iex> Feel.Analysis.Inference.between([:number], [:string], [:number])
{:ok, [{:boolean, false}, :null]}
iex> Feel.Analysis.Inference.between([:null], [:number], [:number])
{:ok, [:null]}

branches(arg, then_type, else_type)

Returns the type of if … then then_type else else_type for the branches that can be taken, {then?, else?}: the union of their types. At least one is taken. reachable/1 has them for the type of the condition; the analyzer also leaves out a branch whose facts leave a name no value (D-152).

iex> Feel.Analysis.Inference.branches({true, true}, [:number], [:string])
[:number, :string]
iex> Feel.Analysis.Inference.branches({false, true}, [:number], [:string])
[:string]

compare(operator, left, right)

Returns the type of left <operator> right for a comparison.

iex> Feel.Analysis.Inference.compare(:lt, [:number], [:number])
{:ok, [:boolean]}
iex> Feel.Analysis.Inference.compare(:eq, [:number, :null], [:number])
{:ok, [:boolean]}
iex> Feel.Analysis.Inference.compare(:lt, [:number, :null], [:number])
{:ok, [:boolean, :null]}
iex> Feel.Analysis.Inference.compare(:lt, [:number], [:null])
{:ok, [:null]}
iex> Feel.Analysis.Inference.compare(:eq, [:number], [:string])
{:mismatch, [:null]}
iex> Feel.Analysis.Inference.compare(:lt, [{:string, ["a", "b"]}], [{:string, ["c"]}])
{:ok, [{:boolean, true}]}
iex> Feel.Analysis.Inference.compare(:lt, [{:string, ["a", "b"]}], [{:string, ["b"]}])
{:ok, [:boolean]}
iex> Feel.Analysis.Inference.compare(:gte, [{:string, ["a", "b"]}], [{:string, ["c"]}])
{:ok, [{:boolean, false}]}
iex> Feel.Analysis.Inference.compare(:eq, [{:list, [:any, :null]}], [{:list, [:null]}])
{:ok, [:boolean]}
iex> Feel.Analysis.Inference.compare(:eq, [{:list, [:number, :null]}, :null], [{:list, [:null]}])
{:ok, [:boolean]}
iex> Feel.Analysis.Inference.compare(:neq, [{:list, []}], [{:list, []}])
{:ok, [{:boolean, false}]}
iex> Feel.Analysis.Inference.compare(:eq, [{:list, [:number]}], [{:list, [:string]}])
{:ok, [:boolean, :null]}

conditional(condition, then_type, else_type)

Returns the type of if condition then then_type else else_type: the union of the types of the branches that reachable/1 says can be taken.

iex> Feel.Analysis.Inference.conditional([:boolean], [:number], [:string])
[:number, :string]
iex> Feel.Analysis.Inference.conditional([:boolean, :null], [:number], [:null])
[:number, :null]
iex> Feel.Analysis.Inference.conditional([{:boolean, true}], [:number], [:string])
[:number]
iex> Feel.Analysis.Inference.conditional([:null], [:number], [:string])
[:string]

conjunction_after(left, right)

@spec conjunction_after(Feel.Type.Union.t(), Feel.Type.Union.t()) ::
  Feel.Type.Union.t()

Returns the type of left and right when right was typed knowing that left is true (D-194): the names that left says something about have, in right, the types that it leaves them (Feel.Analysis.Narrowing).

Both operands are evaluated (D-53), but the result is right only where left is true. Where left is false it is false. Where left is null, or not a boolean, it is false or null, and nothing more is known, since right was typed for the case that left is true and is evaluated with the names as they are: x > 5 and x = null for an x that is null has x > 5 null and x = null true, though x = null is false for the x that x > 5 leaves. So the result is taken for each case of left.

iex> Feel.Analysis.Inference.conjunction_after([:boolean], [{:boolean, false}])
[{:boolean, false}]
iex> Feel.Analysis.Inference.conjunction_after([:boolean, :null], [{:boolean, false}])
[{:boolean, false}, :null]
iex> Feel.Analysis.Inference.conjunction_after([{:boolean, true}], [:boolean, :null])
[:boolean, :null]
iex> Feel.Analysis.Inference.conjunction_after([{:boolean, false}], [:boolean, :null])
[{:boolean, false}]

conjunction_cases(left, right_when_true, right_otherwise)

Returns the type of left and right when right was typed twice (D-194): as right_when_true for the case that left is true, and as right_otherwise for the case that it is not. Where conjunction_after/2 can only say that a left that is null gives false or null, this knows what the right is for the names as they are then: x >= 1 and x <= 10 for an x of [1..3] that may be null is true, and null where x is.

It is for a right operand that is cheap to type again, the upper comparison of a range.

iex> Feel.Analysis.Inference.conjunction_cases([{:boolean, true}, :null], [{:boolean, true}], [:null])
[{:boolean, true}, :null]
iex> Feel.Analysis.Inference.conjunction_cases([{:boolean, true}, :null], [{:boolean, true}], [:boolean, :null])
[:boolean, :null]
iex> Feel.Analysis.Inference.conjunction_cases([:boolean], [{:boolean, false}], [:null])
[{:boolean, false}]

disjunction_after(left, right)

@spec disjunction_after(Feel.Type.Union.t(), Feel.Type.Union.t()) ::
  Feel.Type.Union.t()

Returns the type of left or right when right was typed knowing that left is not true (D-194): the names that left says something about when it is false or null, or not a boolean, have, in right, the types that it leaves them (Feel.Analysis.Narrowing).

It is the other way round from conjunction_after/2. Both operands are evaluated (D-53), but right matters only where left is not true, since true or right is true whatever right is. And everything but true is what the facts of an else say, null and the values that are not booleans included, so the narrowed right holds for every case of left that it can matter for, and the result is left or right for each of them.

iex> Feel.Analysis.Inference.disjunction_after([{:boolean, true}], [:null])
[{:boolean, true}]
iex> Feel.Analysis.Inference.disjunction_after([:boolean], [{:boolean, false}])
[:boolean]
iex> Feel.Analysis.Inference.disjunction_after([{:boolean, false}], [:boolean])
[:boolean]
iex> Feel.Analysis.Inference.disjunction_after([:boolean], [:boolean])
[:boolean]
iex> Feel.Analysis.Inference.disjunction_after([{:boolean, false}, :null], [{:boolean, false}])
[{:boolean, false}, :null]

instance_of(operand, type)

@spec instance_of(Feel.Type.Union.t(), Feel.Type.t()) :: Feel.Type.Union.t()

Returns the type of value instance of type (Table 62), given the type of the value and the type, resolved.

It is a boolean and never null, whatever the value is: null instance of number is false, where null < 1 is null. It is true for sure when the value is never null and the union is within the type, and false for sure when the value is null and the type is not Null, or the type is Null and the value is never null, or when what the value can be, null aside, has no value in common with the type (Feel.Type.Union.intersect/2, D-194): a number is not a string. A list<string> is still boolean against list<number>, for the empty list is both. Anything else is any boolean.

iex> Feel.Analysis.Inference.instance_of([:number, :null], :string)
[{:boolean, false}]
iex> Feel.Analysis.Inference.instance_of([{:list, [:string]}], {:list, :number})
[:boolean]
iex> Feel.Analysis.Inference.instance_of([{:list, [:string]}], :number)
[{:boolean, false}]
iex> Feel.Analysis.Inference.instance_of([:number], :number)
[{:boolean, true}]
iex> Feel.Analysis.Inference.instance_of([:number, :null], :number)
[:boolean]
iex> Feel.Analysis.Inference.instance_of([:null], :number)
[{:boolean, false}]
iex> Feel.Analysis.Inference.instance_of([:null], :null)
[{:boolean, true}]
iex> Feel.Analysis.Inference.instance_of([:number], :null)
[{:boolean, false}]
iex> Feel.Analysis.Inference.instance_of([:number], :any)
[{:boolean, true}]
iex> Feel.Analysis.Inference.instance_of([:null, :any], :any)
[:boolean]

list_contains(type, uses_input?)

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

Returns the type of a test of in that is an expression, x in [1, 2, 3] (D-61), given the type of the expression and whether it reads the input ?.

A list is list contains: the input is in it when it equals an element, or lies within an element that is a range, and an element that it cannot be compared with is not equal to it. That is true or false for every input, null included (Feel.Runtime.UnaryTests.member/3). A null is the value null, which the input equals or does not; but a test that reads ? and is null is the answer itself, so it is null. Anything else is equality with the input, or a range's containment, which are null for an input they cannot be compared with. The type is the exact one of the node (Feel.IR.type/1), so that the empty list, which a plain reader takes for a list of null, is told from the rest: nothing is in it, so the answer is false for every input.

iex> Feel.Analysis.Inference.list_contains([{:list, []}], false)
[{:boolean, false}]
iex> Feel.Analysis.Inference.list_contains([{:list, []}, :null], false)
[:boolean]
iex> Feel.Analysis.Inference.list_contains([{:list, [:number]}], false)
[:boolean]
iex> Feel.Analysis.Inference.list_contains([{:list, [:number]}, :null], false)
[:boolean]
iex> Feel.Analysis.Inference.list_contains([{:list, [:number]}, :null], true)
[:boolean, :null]
iex> Feel.Analysis.Inference.list_contains([{:list, [:number]}, :number], false)
[:boolean, :null]
iex> Feel.Analysis.Inference.list_contains([:any, :null], false)
[:boolean, :null]

logic(operator, left, right)

@spec logic(:and | :or, Feel.Type.Union.t(), Feel.Type.Union.t()) :: result()

Returns the type of left and right or left or right.

iex> Feel.Analysis.Inference.logic(:and, [:boolean], [:boolean])
{:ok, [:boolean]}
iex> Feel.Analysis.Inference.logic(:or, [:boolean, :null], [:boolean])
{:ok, [:boolean, :null]}
iex> Feel.Analysis.Inference.logic(:and, [:null], [:null])
{:ok, [:null]}
iex> Feel.Analysis.Inference.logic(:and, [:number], [:string])
{:ok, [:null]}
iex> Feel.Analysis.Inference.logic(:and, [:boolean], [:null])
{:ok, [{:boolean, false}, :null]}
iex> Feel.Analysis.Inference.logic(:or, [:boolean], [:number])
{:ok, [{:boolean, true}, :null]}
iex> Feel.Analysis.Inference.logic(:and, [{:boolean, true}], [:null])
{:ok, [:null]}
iex> Feel.Analysis.Inference.logic(:or, [:any], [:null])
{:ok, [{:boolean, true}, :null]}
iex> Feel.Analysis.Inference.logic(:and, [:any], [{:boolean, false}])
{:ok, [{:boolean, false}]}
iex> Feel.Analysis.Inference.logic(:and, [:any], [:boolean])
{:ok, [:boolean, :null]}

membership(tests)

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

Returns the type of subject in tests (D-61), given the types of the tests.

The value is the three-valued OR of the tests, taken alternative by alternative (D-194): it can be true when a test can be, false when every test can be false, and null when no test is true and a test is not false either. So a test that is false or null makes the whole false or null, never true, and Null is among the alternatives only when a test can be null.

iex> Feel.Analysis.Inference.membership([[{:boolean, false}, :null]])
[{:boolean, false}, :null]
iex> Feel.Analysis.Inference.membership([[:boolean], [:null]])
[{:boolean, true}, :null]
iex> Feel.Analysis.Inference.membership([[{:boolean, true}], [:boolean, :null]])
[{:boolean, true}]
iex> Feel.Analysis.Inference.membership([[{:boolean, false}], [{:boolean, false}]])
[{:boolean, false}]
iex> Feel.Analysis.Inference.membership([[:boolean], [{:boolean, false}]])
[:boolean]
iex> Feel.Analysis.Inference.membership([[:boolean], [:boolean, :null]])
[:boolean, :null]
iex> Feel.Analysis.Inference.membership([[:null], [:null]])
[:null]

negate(operand)

@spec negate(Feel.Type.Union.t()) :: result()

Returns the type of -operand. An operand of an unknown type (Any, a named type) is a number, a years and months duration, a days and time duration, or null (D-207): the kinds that a negation has.

iex> Feel.Analysis.Inference.negate([:number])
{:ok, [:number]}
iex> Feel.Analysis.Inference.negate([:any])
{:ok, [:number, :years_and_months_duration, :days_and_time_duration, :null]}
iex> Feel.Analysis.Inference.negate([:number, :null])
{:ok, [:number, :null]}
iex> Feel.Analysis.Inference.negate([:string])
{:mismatch, [:null]}

null_bounds(operator, left, right, grains \\ {nil, nil}, same? \\ false)

@spec null_bounds(
  operator(),
  Feel.Type.Union.t(),
  Feel.Type.Union.t(),
  grains(),
  boolean()
) ::
  :near_zero | :too_large | :range | nil

Which bounds on the operands of left <operator> right would rule null out (D-161), when neither operand can be null: :near_zero for operands at least 0.001 away from zero, :too_large for operands of at most 1E+30, :range where the result needs both, and nil where it is not null, where it is certain (two single numbers, which is a warning of its own, D-161), or where these bounds do not rule it out. It is what lets the analyzer say, in an info, that tighter allowed values would rule null out, and the numbers say so: the operands are restricted as the hint says, and the result has no null left.

iex> alias Feel.Type.NumberSet
iex> to_150 = [{:number, NumberSet.interval(Decimal.new(0), Decimal.new(150))}]
iex> to_max = [{:number, NumberSet.interval(Decimal.new(0), NumberSet.max_value())}]
iex> two = [{:number, NumberSet.point(Decimal.new(2))}]
iex> fraction = [{:number, NumberSet.point(Decimal.new("1.19"))}]
iex> Feel.Analysis.Inference.null_bounds(:divide, to_150, two)
:near_zero
iex> Feel.Analysis.Inference.null_bounds(:multiply, to_max, two)
:too_large
iex> Feel.Analysis.Inference.null_bounds(:multiply, to_max, fraction)
:range
iex> Feel.Analysis.Inference.null_bounds(:multiply, to_150, two)
nil
iex> Feel.Analysis.Inference.null_bounds(:divide, to_150 ++ [:null], two)
nil

range(from, to)

@spec range(Feel.Type.Union.t(), Feel.Type.Union.t()) ::
  {:ok, Feel.Type.Union.t()} | {:mismatch, Feel.Type.Union.t()}

Returns the type of the range literal [from..to] (D-61), given the types of its endpoints.

A range is made of two endpoints of one kind that has an order, and a null endpoint is no endpoint, so the range is range<T> for the union T of what the endpoints can be, and never null, when every pair of kinds that they can be makes one. It is {:mismatch, [:null]} when no pair does ([1.."a"]), and it has Null among its alternatives when only some do. A kind that is not known (Any) can be any.

iex> Feel.Analysis.Inference.range([:number], [:number])
{:ok, [{:range, [:number]}]}
iex> Feel.Analysis.Inference.range([:number], [:null])
{:ok, [{:range, [:number]}]}
iex> Feel.Analysis.Inference.range([:number], [:string])
{:mismatch, [:null]}
iex> Feel.Analysis.Inference.range([:number, :string], [:number])
{:ok, [{:range, [:number, :string]}, :null]}
iex> Feel.Analysis.Inference.range([:any], [:number])
{:ok, [{:range, [:any]}, :null]}

reachable(condition)

@spec reachable(Feel.Type.Union.t()) :: {boolean(), boolean()}

Returns which branches of if condition then … else … a run can take, as {then?, else?}.

The then branch is taken when the condition is true, so only if its type has an alternative that can be true. The else branch is taken for any other value, so unless the condition is true for sure. One of them is always taken.

iex> Feel.Analysis.Inference.reachable([:boolean, :null])
{true, true}
iex> Feel.Analysis.Inference.reachable([{:boolean, true}])
{true, false}
iex> Feel.Analysis.Inference.reachable([{:boolean, false}])
{false, true}
iex> Feel.Analysis.Inference.reachable([:number, :null])
{false, true}

shapes_equal(operator, left, right)

@spec shapes_equal(:eq | :neq, shape(), shape()) :: Feel.Type.Union.t()

Returns the type of left = right and left != right for two nodes whose structure is known (D-193, D-193, D-161).

The structure is what the type does not have: a FEEL list type has no length, so a list in the entry of a context or in a list is list<number>. Here it is followed to every depth:

  • lists of other lengths are unequal for sure, whatever the elements (Feel.Runtime.Compare), and the result is never null;
  • lists of one length, and contexts with the same keys, are equal by the conjunction of the equality of what they hold, each pair of elements and each entry by the same rule (so {a: [x]} = {a: [x, y]} is false);
  • contexts that cannot have the same keys are unequal for sure;
  • anything else is by the types, which is what compare/3 makes of them.

The conjunction is that of the run (all_equal/1): the result can be false if a pair can, true if all can, and null if a pair can and no pair is false for sure.

iex> leaf = fn type -> %{type: type, structure: :none} end
iex> list = fn items -> %{type: [{:list, [:number, :null]}], structure: {:list, items}} end
iex> Feel.Analysis.Inference.shapes_equal(:eq, list.([leaf.([:number])]), list.([leaf.([:number]), leaf.([:number])]))
[{:boolean, false}]
iex> Feel.Analysis.Inference.shapes_equal(:eq, list.([leaf.([:number])]), list.([leaf.([:number])]))
[:boolean]
iex> Feel.Analysis.Inference.shapes_equal(:eq, list.([leaf.([:number])]), list.([leaf.([:string])]))
[:null]
iex> Feel.Analysis.Inference.shapes_equal(:neq, list.([]), list.([]))
[{:boolean, false}]
iex> inner = fn count -> list.(List.duplicate(leaf.([:number]), count)) end
iex> Feel.Analysis.Inference.shapes_equal(:eq, list.([inner.(1)]), list.([inner.(2)]))
[{:boolean, false}]