Feel.Analysis.AllowedValues (feel v0.1.0)

Copy Markdown View Source

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

compile_option()

@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).

compiled()

@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.

constraint()

@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

accepts_nothing?(type, tests)

@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

at(arg1, span)

@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}]

compile(scope, name, options \\ [])

@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. nil by default.
  • :limits - a Feel.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

compile_all(scope, name, options \\ [])

@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

compile_result(scope, source, type, options \\ [])

@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

compile_result_constraint(scope, constraint, type, options \\ [])

@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}

tests(unary_tests)

@spec tests(Feel.IR.t()) :: Feel.IR.t()

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

unsatisfiable?(scope, name, tests, steps \\ [], kind \\ :allowed_values)

@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