The names and types an expression may refer to, known before it runs.
Feel.prepare/2 uses a scope to resolve names, to match names that contain
spaces or operator characters, and to check calls and infer types. A scope has
these parts:
names- the variables, with the type of each value they will be bound to. A function is a variable whose type is a function type.types- the type names an expression may use, such as the item definitions of a DMN model.allowed_values- for some variables, the values they may have, as the source text of a list of unary tests, as in theallowedValuesof a DMN item definition ("[0..150]",""a", "b"","< 100"). See below.constraints- the allowed values and type constraints for variables and their members, with key steps through contexts and:eachthrough lists (:type_constraintsinnew/2).result_typeandresult_allowed_values- what the expression gives: the type of its result, and the values it may have. They are thetypeRefand theallowedValuesof the variable of a DMN decision.
Allowed values
The bound value of a variable with allowed values is verified when a run starts (D-138), for
the variables that the expression reads: a value that none of the tests accepts is replaced by
null, with a :value_not_allowed error, and null itself is exempt. The tests are tested
against the value as ?, and may use the other names of the scope. They are read when an
expression that uses the variable is prepared, and a test that cannot be read is an error of that
expression, with the code :invalid_allowed_values. Their source is kept as it is written, since
a scope is data; Feel.Analysis.AllowedValues compiles it.
A variable declared as a list ({:list, :number}) has the tests applied to each element, as the
allowedValues of a DMN item definition with isCollection are (D-51): "[0..10]" for
list<number> means that every element, other than null, is in [0..10]. The first element
that is not makes the whole value null, with an error that says which. So are the allowed values of
the result, when its result_type is a list (D-138).
Type constraints, and the members of a value
The typeConstraint of a DMN item definition is a list of unary tests like the allowed values, but for the value as a whole:
for a list, "count(?) > 0" says the list has an element, where allowed values are for each element. They are given as
type_constraints, by the name of the variable. For a value that is not a list the two are the same.
Both can be given for a member of a value, by a subject {name, steps}: the steps are the keys of the context to
go down, and :each to go to the elements of a list. {"person", ["age"]} is the age of a person, {"orders", [:each, "total"]} the total of each order. The tests are for the member as the tests of a variable are for the variable, and a
member that is not there or is null is exempt. A value with a member that is not allowed is not allowed, as a list with
an element that is not (the whole value is null, with an error that says which member). They narrow the type of the
member, so person.age is a number in [0..150].
Names that are never null
In the specification null is a value of every type, so a variable declared :number may be
null, and so is anything computed from it. A type marked not null ({:non_null, :number}, or
"number!" for Feel.Type.parse/2, D-48) says that the variable is never null: with the default
declarations: :enforce the analysis types the expression without the null that the variable would
bring, and a run ends with an error of the kind :input if the bound value is null, is not
bound, cannot be converted to the type, or is not one of its allowed values. This is an extension
to standard FEEL: expression type syntax also accepts the mark in function parameters and
instance of targets. Preparation currently rejects an outer non-null result declaration
with :not_implemented; result constraints are not a guarantee that evaluation cannot return null.
Types the lattice cannot say
The types of the specification have no union and no negation. A name that is anything but a number is {:not, :number}
("Any except number"), and one that is a number or a string is {:union, [:number, :string]} ("number | string"), for
Feel.Type.parse/2 (D-178). They are declared like any other type, in the names, the :types, the :result_type and the
:input, inside a list or a context, and under the mark: {:non_null, {:union, [:number, :string]}}. null is in them as it is in every
declared type, unless the whole is marked. The type of the name in the analysis is the union they make, and with the default
declarations: :enforce the run makes it true: a value that is not in it is null with a :type_mismatch error. See
Feel.Type.Composite.
The result
The result of the expression is converted to the result_type and has to conform to it, and
then it has to satisfy the result_allowed_values (D-138). In DMN this is how a decision gives
its value to its variable. A scope is shared by the expressions that see the same names, so the
scope of one expression is made with put_result/2 from the scope of the others, which is
cheap. The options :result_type and :result_allowed_values of Feel.prepare/2 take the
place of those of the scope.
The built-in functions are not stored here; they are always in scope, in the outermost layer, and a name in this scope shadows them.
All names in a scope are canonical (see Feel.Name). new/2 canonicalizes
the names it is given. The functions that put and fetch single entries expect
canonical names already, since the analyzer gets them from the parser.
Summary
Types
The allowed values (for each element of a list) or the type constraint (for the value as a whole).
A context key, :each for list elements, or a guarded declaration graph selector. The latter visits
each occurrence of its target declaration from the root, retaining concrete runtime paths.
What is constrained: the variable itself (no steps), or a member of it.
Functions
Returns the allowed values of the scope: a map from variable name to source text.
Everything that is constrained of a variable, whole or by member: the allowed values of the variable itself, its type
constraint, and those of its members, each with the steps down to it, the kind and the source text. The name must be
canonical.
Looks up the allowed values of a variable. The name must be canonical.
Looks up the type of a variable. The name must be canonical.
Looks up a type by name. The name must be canonical.
Merges two scopes. Entries of the second scope replace those of the first.
Returns the variable names in the scope, sorted.
Builds a scope from variable names and, optionally, type names and allowed values.
Sets the allowed values of a variable: the source text of a list of unary tests.
Adds or replaces a variable. The name must be canonical.
Marks the types of variables as not null (D-48): :all of them, or the ones in a list of canonical names.
Sets what the expression gives: the type of its result, and its allowed values.
Adds or replaces a type name. The name must be canonical.
Sets the type constraint of a variable or of a member of it: the source text of a list of unary tests for the value as a
whole, which for a list is the list ("count(?) > 0") and not each element.
Returns the allowed values of the result, as source text, or nil when there are none.
Returns the type of the result of the expression, or nil when there is none.
Returns the type names in the scope, sorted.
Types
@type kind() :: :allowed_values | :type_constraint
The allowed values (for each element of a list) or the type constraint (for the value as a whole).
@type step() :: Feel.Name.t() | :each | {:recursive, String.t(), String.t(), Feel.Type.Recursive.graph()}
A context key, :each for list elements, or a guarded declaration graph selector. The latter visits
each occurrence of its target declaration from the root, retaining concrete runtime paths.
@type subject() :: Feel.Name.t() | {Feel.Name.t(), [step()]}
What is constrained: the variable itself (no steps), or a member of it.
@type t() :: %Feel.Scope{ allowed_values: %{optional(Feel.Name.t()) => String.t()}, constraints: %{ optional({Feel.Name.t(), [step()], kind()}) => [String.t(), ...] }, names: %{optional(Feel.Name.t()) => Feel.Type.t()}, result_allowed_values: String.t() | nil, result_type: Feel.Type.t() | nil, types: %{optional(Feel.Name.t()) => Feel.Type.t()} }
Functions
@spec allowed_values(t()) :: %{optional(Feel.Name.t()) => String.t()}
Returns the allowed values of the scope: a map from variable name to source text.
@spec constraints(t(), Feel.Name.t()) :: [ %{steps: [step()], kind: kind(), source: String.t(), index: non_neg_integer()} ]
Everything that is constrained of a variable, whole or by member: the allowed values of the variable itself, its type
constraint, and those of its members, each with the steps down to it, the kind and the source text. The name must be
canonical.
iex> scope = Feel.Scope.new(%{"p" => {:context, %{"age" => :number}}}, allowed_values: %{{"p", ["age"]} => "[0..150]"})
iex> Feel.Scope.constraints(scope, "p")
[%{steps: ["age"], kind: :allowed_values, source: "[0..150]", index: 0}]
@spec fetch_allowed_values(t(), Feel.Name.t()) :: {:ok, String.t()} | :error
Looks up the allowed values of a variable. The name must be canonical.
@spec fetch_name(t(), Feel.Name.t()) :: {:ok, Feel.Type.t()} | :error
Looks up the type of a variable. The name must be canonical.
@spec fetch_type(t(), Feel.Name.t()) :: {:ok, Feel.Type.t()} | :error
Looks up a type by name. The name must be canonical.
Merges two scopes. Entries of the second scope replace those of the first.
@spec names(t()) :: [Feel.Name.t()]
Returns the variable names in the scope, sorted.
@spec new(%{optional(String.t()) => Feel.Type.t()}, types: %{optional(String.t()) => Feel.Type.t()}, allowed_values: %{optional(subject()) => String.t() | [String.t()]}, type_constraints: %{optional(subject()) => String.t() | [String.t()]}, result_type: Feel.Type.t(), result_allowed_values: String.t(), not_null: :all | [String.t()] ) :: t()
Builds a scope from variable names and, optionally, type names and allowed values.
Options
:types- a map from type name toFeel.Type.t/0.:allowed_values- a map from the name of a variable, or a subject{name, steps}(a member of it), to the source text of its allowed values, a list of unary tests, or a list of such texts, which all have to hold (a definition that has allowed values and refers to one that has). Every name must be a variable of the scope.:type_constraints- the same for the type constraint, which is for the value as a whole.:result_type- the type of the result of the expression, aFeel.Type.t/0.:result_allowed_values- the allowed values of the result, as above.:not_null-:all, or a list of names: the variables whose types are marked not null (put_not_null/2, D-48).
Names are canonicalized with Feel.Name.canonical/1. Raises ArgumentError for an
allowed value that is not a string, or that is for a name that is not in the scope.
iex> scope = Feel.Scope.new(%{"age" => :number}, allowed_values: %{"age" => "[0..150]"})
iex> Feel.Scope.fetch_allowed_values(scope, "age")
{:ok, "[0..150]"}
iex> scope = Feel.Scope.new(%{"age" => :number, "name" => :string}, not_null: ["age"])
iex> {Feel.Scope.fetch_name(scope, "age"), Feel.Scope.fetch_name(scope, "name")}
{{:ok, {:non_null, :number}}, {:ok, :string}}
Sets the allowed values of a variable: the source text of a list of unary tests.
The name must be canonical and a variable of the scope. Raises ArgumentError when it
is not, or when the source is not a string.
iex> scope = Feel.Scope.put_name(%Feel.Scope{}, "age", :number)
iex> scope = Feel.Scope.put_allowed_values(scope, "age", "[0..150]")
iex> Feel.Scope.allowed_values(scope)
%{"age" => "[0..150]"}
@spec put_name(t(), Feel.Name.t(), Feel.Type.t()) :: t()
Adds or replaces a variable. The name must be canonical.
@spec put_not_null(t(), :all | [Feel.Name.t()]) :: t()
Marks the types of variables as not null (D-48): :all of them, or the ones in a list of canonical names.
A caller whose inputs are all required says so once, and not for each type. The mark is the one of a type written
with !: with the default declarations: :enforce the analysis types an expression without the null that the variable would
bring, and a run ends with an error of the kind :input if the bound value is null or is not bound. A type that is
marked already stays as it is, and a variable of the type Null, which has no value but null, is left as it is. Raises
ArgumentError for a name that is not a variable of the scope.
iex> scope = Feel.Scope.new(%{"age" => :number, "ages" => {:list, :number}})
iex> scope = Feel.Scope.put_not_null(scope, :all)
iex> {Feel.Scope.fetch_name(scope, "age"), Feel.Scope.fetch_name(scope, "ages")}
{{:ok, {:non_null, :number}}, {:ok, {:non_null, {:list, :number}}}}
iex> Feel.Scope.put_not_null(scope, ["other"])
** (ArgumentError) not_null for "other", which is not a name of the scope
@spec put_result(t(), result_type: Feel.Type.t() | nil, result_allowed_values: String.t() | nil ) :: t()
Sets what the expression gives: the type of its result, and its allowed values.
Options
:result_type- aFeel.Type.t/0. A type that names a type of the scope ({:named, "tAge"}) is resolved when the expression is prepared.:result_allowed_values- the source text of a list of unary tests, as for a variable.
An option that is not given, or is nil, leaves what the scope has. Raises ArgumentError
for allowed values that are not a string.
iex> scope = Feel.Scope.put_result(%Feel.Scope{}, result_type: :number, result_allowed_values: "[0..100]")
iex> {Feel.Scope.result_type(scope), Feel.Scope.result_allowed_values(scope)}
{:number, "[0..100]"}
@spec put_type(t(), Feel.Name.t(), Feel.Type.t()) :: t()
Adds or replaces a type name. The name must be canonical.
Sets the type constraint of a variable or of a member of it: the source text of a list of unary tests for the value as a
whole, which for a list is the list ("count(?) > 0") and not each element.
iex> scope = Feel.Scope.new(%{"xs" => {:list, :number}}, type_constraints: %{"xs" => "count(?) > 0"})
iex> Feel.Scope.constraints(scope, "xs")
[%{steps: [], kind: :type_constraint, source: "count(?) > 0", index: 0}]
Returns the allowed values of the result, as source text, or nil when there are none.
@spec result_type(t()) :: Feel.Type.t() | nil
Returns the type of the result of the expression, or nil when there is none.
@spec type_names(t()) :: [Feel.Name.t()]
Returns the type names in the scope, sorted.