Feel.Analysis.TestSets (feel v0.1.0)

Copy Markdown View Source

The values that unary tests accept, as sets (D-39).

The allowed values of a variable are compiled to the IR of unary tests, in which each test is an expression of the input ? (Feel.Analysis.AllowedValues). restrict/2 reads that IR and narrows the type of the variable to the numbers, the strings, the booleans, the dates or the durations the tests accept, so that [0..150] makes a number of age a number in [0..150], and [@"2020-01-01"..@"2020-12-31"] makes a date one in that year (D-109).

A date and a duration are sets of numbers too, of the numbers that stand for them (Feel.Type.Image, D-109), and are ordered as the numbers are, so they are read exactly as a number is, with the image of the other side of a comparison. A time and a date and time are sets of numbers too, of the second a value falls in, but their order depends on a zone (D-54), so a value has the number of its clock and, if it has a zone, that of its instant, and they are read by Feel.Analysis.MomentTests (D-109).

Only a safe reading is made. The set of a test is a superset of the values that make it true, so a value that is accepted is always in the set, and a test that is not read (a name that is not a constant, an expression of ?, a :match) accepts everything. A set is therefore the values that the tests may accept; within the representation budget it is exact for input entries: comparisons of ? with numbers or strings, intervals, lists of values, -, and not(...) of those.

The result of a test has three values, true, false and null. null is neither accepted nor rejected: not(...) accepts only values that make all of its tests false. So each test has two sets: where it can be true, and where it can be false (both supersets), and and and or combine them as the logic does: a conjunction is true where both are, and false where either is.

A name of the type Any

Any is the union of all the kinds of value, so it is narrowed by the kinds that the tests can accept, each with the set of its values the tests accept. A comparison of ? with an operand of a known kind is true or false only for a value of that kind: "a" >= 0 is null, as is [5] >= 0 and true < 1, and so is "a" = 0, a type mismatch, and null is neither accepted nor rejected. So [0..10] accepts only numbers, and an Any becomes number in [0..10]; [0..10], true becomes number in [0..10] | boolean = true. This is read in the same way as the sets, as a pair of kinds for each test (where it may be true, where it may be false), which and and or meet and join and not(...) takes the second of.

Three things accept more kinds than the operand says: an operand that can be null (x = null is false, and x != null is true, for every value that is not null), an operand of an unknown type (= x for an x of the type Any accepts every kind, and > x every kind that has an order), and a test that is not read. When the kinds are all of them the type stays Any. A function that may be accepted is the kind function, a function of any parameters (D-39).

iex> {:ok, ast} = Feel.parse("[0..150], > 200", entry: :unary_tests)
iex> {:ok, %{ir: tests}} = Feel.Analysis.Analyzer.analyze(ast, input: :number, declarations: :ignore)
iex> Feel.Analysis.TestSets.restrict([:number, :null], tests) |> Feel.Type.Union.format()
"number in [0..150], (200..max] | Null"

Summary

Types

A set of strings (Feel.Type.StringSet): every string, a list, or one with intervals.

Functions

The values of a type that the tests certainly accept: restrict/2 is a superset of what they accept, and this is a subset, so that the two together bound a rule of a decision table from outside and from inside.

Returns {where it can be true, where it can be false} for ? operator other, when ? is a value of a kind (:number, :string, :boolean, :date or a duration) and other is of the type, a union.

Narrows the number, the string or the boolean of a type to what the tests accept, and Any to the kinds the tests can accept.

Narrows the elements of the lists of a type to what the tests accept, as restrict/2 does for a value. The tests are those of the elements (each of Feel.Analysis.AllowedValues.compile/3), and a null element is exempt, so it stays if the elements could be null.

Types

strings()

@type strings() :: Feel.Type.StringSet.set()

A set of strings (Feel.Type.StringSet): every string, a list, or one with intervals.

Functions

certain(union, tests)

@spec certain(Feel.Type.Union.t(), Feel.IR.t()) :: Feel.Type.Union.t() | []

The values of a type that the tests certainly accept: restrict/2 is a superset of what they accept, and this is a subset, so that the two together bound a rule of a decision table from outside and from inside.

A test is read only where its answer is known to be exact: ? op other for an other that is one number, string, boolean, date or duration (the sets of compared_sets/3 are then the values for which the comparison is true and for which it is false), joined by and and or. Anything else (an other that is a range of values or is unknown, a time, a test that is not a comparison) accepts nothing for certain. The result has no null: whether a test accepts null is not a question about a set, and is left to whoever runs it. It is [] when nothing is certain, and a type of the members that have values.

Numeric combinations keep inner bounds. String alternatives use bulk union, and negated lists use bulk intersection, before the point/exclusion budget is checked. Interval ranges do not count toward that budget; the bulk operations avoid rebuilding a growing range set for every entry.

iex> {:ok, ast} = Feel.parse("[0..150], > 200", entry: :unary_tests)
iex> {:ok, %{ir: tests}} = Feel.Analysis.Analyzer.analyze(ast, input: :number, declarations: :ignore)
iex> Feel.Analysis.TestSets.certain([:number, :null], tests) |> Feel.Type.Union.format()
"number in [0..150], (200..max]"
iex> {:ok, ast} = Feel.parse("> x", entry: :unary_tests)
iex> {:ok, %{ir: tests}} = Feel.Analysis.Analyzer.analyze(ast, input: :number, declarations: :ignore)
iex> Feel.Analysis.TestSets.certain([:number, :null], tests)
[]

compared_sets(kind, operator, type)

@spec compared_sets(
  :number | :string | :boolean | Feel.Type.Image.kind(),
  atom(),
  Feel.Type.Union.t()
) ::
  {term(), term()}

Returns {where it can be true, where it can be false} for ? operator other, when ? is a value of a kind (:number, :string, :boolean, :date or a duration) and other is of the type, a union.

Both are supersets of the values of the kind for which the comparison is true, and false; it is null, which is neither, for the values that other cannot be compared with. For a number, a date and a duration they are Feel.Type.NumberSets (of the images, for the last two, D-109), for a string a Feel.Type.StringSet (a list, or :all), and for a boolean a list of true and false. Feel.Analysis.Narrowing reads the types of the names in a branch from them.

iex> five = [{:number, Feel.Type.NumberSet.point(Decimal.new(5))}]
iex> {true_set, false_set} = Feel.Analysis.TestSets.compared_sets(:number, :gt, five)
iex> {Feel.Type.NumberSet.format(true_set), Feel.Type.NumberSet.format(false_set)}
{"(5..max]", "[-max..5]"}
iex> Feel.Analysis.TestSets.compared_sets(:boolean, :eq, [{:boolean, true}])
{[true], [false]}

restrict(union, whole)

@spec restrict(Feel.Type.Union.t(), Feel.IR.t()) :: Feel.Type.Union.t()

Narrows the number, the string or the boolean of a type to what the tests accept, and Any to the kinds the tests can accept.

The type must have one alternative besides null, a :number, :string, :boolean, :date, a duration, or :any, bare or with the set of its values; any other type is returned as it is.

restrict_elements(union, tests)

@spec restrict_elements(Feel.Type.Union.t(), Feel.IR.t()) :: Feel.Type.Union.t()

Narrows the elements of the lists of a type to what the tests accept, as restrict/2 does for a value. The tests are those of the elements (each of Feel.Analysis.AllowedValues.compile/3), and a null element is exempt, so it stays if the elements could be null.

iex> {:ok, ast} = Feel.parse("[0..150]", entry: :unary_tests)
iex> {:ok, %{ir: tests}} = Feel.Analysis.Analyzer.analyze(ast, input: :number, declarations: :ignore)
iex> Feel.Analysis.TestSets.restrict_elements([{:list, [:number, :null]}, :null], tests) |> Feel.Type.Union.format()
"list<number in [0..150] | Null> | Null"