Compiles allowed values: those of a variable of a Feel.Scope, and those of the result of
an expression (D-138).
Allowed values are the source text of a list of unary tests, as in the allowedValues of a
DMN item definition. A variable can have more than these for its value: a type constraint (for the value as a whole), and
allowed values and type constraints for its members (D-161, step E); compile_all/3 compiles all of them. compile/3 reads the text of a variable, with the other names of the
scope in view, and analyzes the tests with ? typed as the variable, so that [0..150] for a
number is ? >= 0 and ? <= 150. For a variable declared as a list, the tests are for its
elements, as the allowedValues of a DMN item definition with isCollection are (notes 03,
section 4.3): ? is typed as an element, and each is true in what compile/3 returns.
compile_result/4 does the same for the text given for the
result of an expression, with ? typed as the expression. The result is the IR of the tests,
which Feel.Analysis.Analyzer puts in a :verify_bindings node (for a variable) or an
:allowed node (for the result), and what a later step reads ranges and sets of values from.
The tests of a constraint read the names of the scope as the expression does (D-170): converted to their declared types,
and verified against their allowed values, so that a name that the run turns down is null in a test, and a name that is
allowed has the type its allowed values give it (age with <= max age is cut by the type of max age, which is a type
that holds only if the value a test sees is one that is allowed). The constraints that are being compiled, and the ones whose
tests read them, are not applied to a name that is read, and it is read as it was bound as far as they go: a test that reads
the name it belongs to, or a cycle of two, does not loop, and the tests of person.age that read person.limit see a
person that the other constraints of it (those of limit) hold. The stances that leave a value as it is (:report,
:audit, :trust, :ignore) read every name as it was bound, as they did.
The tests are read and analyzed with the extensions of the expression (:extensions, D-81), so a test may call a function
that an extension adds, as the expression may.
A test that cannot be read or analyzed is an error with the code :invalid_allowed_values
at the span of the name that was read, or of the expression for its result. The warnings of a test that can are given the same
span, since the text of the tests is not the source of the expression.
Summary
Types
An option of compile/3 and compile_result/4. :reads is how the tests read the names of the scope: nil as they were
bound, or {stance, unverified}, the stance (:enforce or :enforce_wide) the expression is analyzed with and the
constraints that are not applied to a name that is read (D-170).
What compile/3 returns: the IR of the tests, the names they read, their diagnostics, and whether the
tests are for each element of a list (each) and not for the value.
A constraint of a name, as Feel.Scope.constraints/2 has it: the name, the steps down to the member, the kind and the index.
Functions
Whether the tests accept no value of the type, null apart. A type that the tests do not narrow (one with several kinds
besides null, a list, a context) is never taken for one that nothing satisfies.
What compile/3 gave, for a read of the variable at another span: the diagnostics are given that span. The tests are
read once and put at the span of each read, and are those that reading them there would give.
Compiles the allowed values of a variable.
Compiles everything that a variable has constrained (D-161, step E): its allowed values, its type constraint, and those of its
members, each as compile/3 does, with the steps down to the member and the kind. The tests of a member are tested
against the member (? is typed as it), and the allowed values of a list are for its elements, where a type constraint is
for the list.
Compiles the allowed values of the result of an expression, given as source text.
Compiles one constraint of the result of an expression: what a declared type has the value, or a member of it, satisfy (the
constraints of Feel.ItemDefinitions.variable/2), in the order of compile_all/3 for a variable.
The unary tests of what compile/3 and compile_all/3 gave, without the conversion and the verification of the names they
read (D-170): the tests are ir itself where they read no name with a declared type, and are inside the nodes of those
names where they do. It is what the sets of the narrowing and the proofs read.
Whether the tests of a variable, as compile/3 gave them, accept no value of its declared type (or of the type of its
elements, for a list), null apart: every value that is not null is then not allowed (D-138).
Types
@type compile_option() :: {:span, Feel.Diagnostic.span() | nil} | {:limits, Feel.Limits.t()} | {:extensions, Feel.Extensions.t() | [atom()]} | {:reads, {Feel.Analysis.Declarations.stance(), [constraint()]} | nil}
An option of compile/3 and compile_result/4. :reads is how the tests read the names of the scope: nil as they were
bound, or {stance, unverified}, the stance (:enforce or :enforce_wide) the expression is analyzed with and the
constraints that are not applied to a name that is read (D-170).
@type compiled() :: %{ ir: Feel.IR.t(), free_names: MapSet.t(Feel.Name.t()), diagnostics: [Feel.Diagnostic.t()], each: boolean(), grain: Feel.Analysis.Grain.t(), steps: [Feel.Scope.step()], kind: Feel.Scope.kind(), index: non_neg_integer() }
What compile/3 returns: the IR of the tests, the names they read, their diagnostics, and whether the
tests are for each element of a list (each) and not for the value.
@type constraint() :: {Feel.Name.t(), [Feel.Scope.step()], Feel.Scope.kind(), non_neg_integer()}
A constraint of a name, as Feel.Scope.constraints/2 has it: the name, the steps down to the member, the kind and the index.
Functions
@spec accepts_nothing?(Feel.Type.t() | Feel.Type.Union.t(), Feel.IR.t()) :: boolean()
Whether the tests accept no value of the type, null apart. A type that the tests do not narrow (one with several kinds
besides null, a list, a context) is never taken for one that nothing satisfies.
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile_result(nil, ~s("a"), :number)
iex> Feel.Analysis.AllowedValues.accepts_nothing?(:number, compiled.ir)
true
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile_result(nil, "[0..10]", :number)
iex> Feel.Analysis.AllowedValues.accepts_nothing?(:number, compiled.ir)
false
@spec at( {:ok, compiled()} | :none | {:error, [Feel.Diagnostic.t(), ...]}, Feel.Diagnostic.span() | nil ) :: {:ok, compiled()} | :none | {:error, [Feel.Diagnostic.t(), ...]}
What compile/3 gave, for a read of the variable at another span: the diagnostics are given that span. The tests are
read once and put at the span of each read, and are those that reading them there would give.
iex> scope = Feel.Scope.new(%{"age" => :number}, allowed_values: %{"age" => ~s(<= "x")})
iex> compiled = Feel.Analysis.AllowedValues.compile(scope, "age")
iex> {:ok, at} = Feel.Analysis.AllowedValues.at(compiled, {4, 7})
iex> {:ok, ^at} = Feel.Analysis.AllowedValues.compile(scope, "age", span: {4, 7})
iex> Enum.map(at.diagnostics, & &1.span)
[{4, 7}]
@spec compile(Feel.Scope.t(), Feel.Name.t(), [compile_option()]) :: {:ok, compiled()} | :none | {:error, [Feel.Diagnostic.t(), ...]}
Compiles the allowed values of a variable.
Returns :none when the variable has none. The name must be canonical.
Options
:span- the span of the name in the expression that reads it, which goes on the diagnostics.nilby default.:limits- aFeel.Limits.t/0.:extensions- the extensions that are on (Feel.Extensions, D-81), a list or a resolved set: the tests are read and analyzed with them, as the expression is, so a test may call a function that an extension adds.
Examples
iex> scope = Feel.Scope.new(%{"age" => :number}, allowed_values: %{"age" => "[0..150]"})
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile(scope, "age")
iex> {Feel.IR.kind(compiled.ir), compiled.free_names, compiled.each}
{:unary_tests, MapSet.new([]), false}
iex> scope = Feel.Scope.new(%{"ages" => {:list, :number}}, allowed_values: %{"ages" => "[0..150]"})
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile(scope, "ages")
iex> compiled.each
true
iex> Feel.Analysis.AllowedValues.compile(Feel.Scope.new(%{"age" => :number}), "age")
:none
@spec compile_all(Feel.Scope.t(), Feel.Name.t(), [compile_option()]) :: {:ok, [compiled(), ...]} | :none | {:error, [Feel.Diagnostic.t(), ...]}
Compiles everything that a variable has constrained (D-161, step E): its allowed values, its type constraint, and those of its
members, each as compile/3 does, with the steps down to the member and the kind. The tests of a member are tested
against the member (? is typed as it), and the allowed values of a list are for its elements, where a type constraint is
for the list.
Returns :none when the variable has no constraint, and the first error otherwise: a constraint that cannot be read, or
a member the declared type does not have.
iex> scope = Feel.Scope.new(%{"xs" => {:list, :number}}, allowed_values: %{"xs" => "[0..10]"}, type_constraints: %{"xs" => "count(?) > 0"})
iex> {:ok, [allowed, constraint]} = Feel.Analysis.AllowedValues.compile_all(scope, "xs")
iex> {allowed.kind, allowed.each, constraint.kind, constraint.each}
{:allowed_values, true, :type_constraint, false}
iex> scope = Feel.Scope.new(%{"p" => {:context, %{"age" => :number}}}, allowed_values: %{{"p", ["age"]} => "[0..150]"})
iex> {:ok, [age]} = Feel.Analysis.AllowedValues.compile_all(scope, "p")
iex> age.steps
["age"]
iex> scope = Feel.Scope.new(%{"p" => {:context, %{"age" => :number}}}, allowed_values: %{{"p", ["height"]} => "> 0"})
iex> {:error, [diagnostic]} = Feel.Analysis.AllowedValues.compile_all(scope, "p")
iex> diagnostic.code
:invalid_allowed_values
@spec compile_result( Feel.Scope.t() | nil, String.t(), Feel.Type.t() | Feel.Type.Union.t(), [ compile_option() | {:declared, Feel.Type.t()} ] ) :: {:ok, compiled()} | {:error, [Feel.Diagnostic.t(), ...]}
Compiles the allowed values of the result of an expression, given as source text.
type is the type of the result, a Feel.Type.t/0 or a Feel.Type.Union.t/0. The tests
run only for a result that is not null, so ? has that type without the null. The scope
is the one the expression is analyzed in, or nil when it has none. The options are those
of compile/3, and :declared, the declared type of the result as a Feel.Type.t/0 with its names resolved: when
it is a list (possibly marked !) the tests are for its elements, as those of a variable declared as a list are (D-51):
? has the type of the elements of type, and each is true in what is returned.
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile_result(nil, "[0..100]", :number)
iex> Feel.IR.kind(compiled.ir)
:unary_tests
iex> {:error, [diagnostic]} = Feel.Analysis.AllowedValues.compile_result(nil, "[0..)", :number)
iex> diagnostic.code
:invalid_allowed_values
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile_result(nil, "[0..100]", [{:list, [:number]}], declared: {:list, :number})
iex> compiled.each
true
@spec compile_result_constraint( Feel.Scope.t() | nil, Feel.ItemDefinitions.constraint(), Feel.Type.t() | Feel.Type.Union.t(), [compile_option() | {:declared, Feel.Type.t() | nil}] ) :: {:ok, compiled()} | {:error, [Feel.Diagnostic.t(), ...]}
Compiles one constraint of the result of an expression: what a declared type has the value, or a member of it, satisfy (the
constraints of Feel.ItemDefinitions.variable/2), in the order of compile_all/3 for a variable.
constraint is %{steps: steps, kind: :allowed_values | :type_constraint, source: text}: the unary tests of source are for the
member of the result at the end of steps (a key of a context, :each for the elements of a list), or for the result itself
with none. The allowed values of a list are for its elements, and a type constraint is for the list. type is the type of the
result, and :declared the declared type with its names resolved, which the steps go down; a member it does not have is an error
(:invalid_allowed_values), and so is any step when nothing is declared. The result has steps and kind of the constraint
(and each as for compile_result/4), which the :allowed node of the analyzer puts in its meta.
iex> type = {:context, %{"age" => :number}}
iex> constraint = %{steps: ["age"], kind: :allowed_values, source: "[0..150]"}
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile_result_constraint(nil, constraint, type, declared: type)
iex> {compiled.steps, compiled.kind, compiled.each}
{["age"], :allowed_values, false}
iex> constraint = %{steps: ["height"], kind: :allowed_values, source: "> 0"}
iex> {:error, [diagnostic]} = Feel.Analysis.AllowedValues.compile_result_constraint(nil, constraint, type, declared: type)
iex> diagnostic.code
:invalid_allowed_values
iex> constraint = %{steps: [], kind: :type_constraint, source: "count(?) > 0"}
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile_result_constraint(nil, constraint, {:list, :number}, declared: {:list, :number})
iex> {compiled.kind, compiled.each}
{:type_constraint, false}
The unary tests of what compile/3 and compile_all/3 gave, without the conversion and the verification of the names they
read (D-170): the tests are ir itself where they read no name with a declared type, and are inside the nodes of those
names where they do. It is what the sets of the narrowing and the proofs read.
iex> scope = Feel.Scope.new(%{"age" => :number, "max" => :number}, allowed_values: %{"age" => "<= max", "max" => "[18..65]"})
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile(scope, "age", reads: {:enforce, ["age"]})
iex> Feel.IR.kind(compiled.ir)
:conform_bindings
iex> Feel.IR.kind(Feel.Analysis.AllowedValues.tests(compiled.ir))
:unary_tests
@spec unsatisfiable?( Feel.Scope.t(), Feel.Name.t(), Feel.IR.t(), [Feel.Scope.step()], Feel.Scope.kind() ) :: boolean()
Whether the tests of a variable, as compile/3 gave them, accept no value of its declared type (or of the type of its
elements, for a list), null apart: every value that is not null is then not allowed (D-138).
iex> scope = Feel.Scope.new(%{"age" => :number}, allowed_values: %{"age" => ~s("a")})
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile(scope, "age")
iex> Feel.Analysis.AllowedValues.unsatisfiable?(scope, "age", compiled.ir)
true
iex> scope = Feel.Scope.new(%{"age" => :number}, allowed_values: %{"age" => "[0..150]"})
iex> {:ok, compiled} = Feel.Analysis.AllowedValues.compile(scope, "age")
iex> Feel.Analysis.AllowedValues.unsatisfiable?(scope, "age", compiled.ir)
false