Feel.Type (feel v0.1.0)

Copy Markdown View Source

The FEEL type system (DMN 1.6, section 10.3.2.9).

Types form a lattice with :any on top and :null at the bottom. Apart from the parameterised families (list, range, context, function), the lattice is flat: the primitive types do not conform to each other, and in particular :date does not conform to :date_and_time. There are no union types, and no common supertype of the two duration types.

t/0FEEL
:any, :nullAny, Null
:boolean, :number, :stringboolean, number, string
:date, :timedate, time
:date_and_timedate and time
:years_and_months_durationyears and months duration
:days_and_time_durationdays and time duration
{:list, type}list<type>
{:range, type}range<type>
{:context, %{"k" => type}}context<k: type>
{:function, [type], result}function<type> -> result
{:recursive, name, graph}guarded recursive item data
{:named, "Name"}a model-defined type, by name
{:non_null, type}type! (an extension, D-48)
{:not, type}Any except type (an extension, D-178)
{:union, [type, ...]}type | type (an extension, D-178)

A context type lists its members by name; the order of members does not matter. Parameter names are not part of a function type.

The last three go past the lattice: T! says that a name is never null, Any except T that it is not a T, and T | U that it is one or the other. They are for what a host declares (Feel.Scope, the result type of an expression, a type of the scope that an expression names), and parse/2 reads them; the types in an expression (instance of, the parameters of a function) are those of the specification and T!. See Feel.Type.Composite for what the last two mean.

Types and values

This module describes types as data (t/0), for checking and inference. It does not hold values. The values of each FEEL type, and how they are represented, are in the modules under Feel.Value, such as Feel.Value.Number and Feel.Value.List. Each of them has a type/0 (or type/1 for the parameterised types) that returns its descriptor, so Feel.Value.Number.type() is :number. Feel.Value itself is the type Any.

What is here

The relations work on resolved types, with no {:named, _} left in them.

In these types Null is under every type, so number means "a number or null". The analyzer needs to say that a result is never null, and uses Feel.Type.Union, in which the null is an alternative of its own (D-39).

Summary

Functions

Returns the built-in type with the given canonical name.

Returns the names of the built-in types, sorted.

Converts a value to fit a type with the implicit conversions of section 10.3.2.9.4, such as wrapping a value in a singleton list. Returns :error when no conversion applies. See Feel.Type.Conversion.

Returns true when the first type conforms to the second: an instance of the first can be used wherever the second is expected. See Feel.Type.Conformance.

Returns true when two types are equivalent. See Feel.Type.Conformance.

Formats a type as FEEL type syntax.

Turns a type node of the syntax tree (see Feel.Syntax.TypeParser) into a type.

The instance of operator (Table 62): null is an instance of :null only, and every other value must fit the type. See Feel.Type.Instance.

Returns a conservative common supertype that both types conform to. It need not be the least expressible supertype for functions or composite declarations. See Feel.Type.Conformance.

Marks a type as not null (D-48): the type of a name that must have a value.

Returns the type of a value (Table 43). See Feel.Type.Instance.

Parses a type expression such as list<number> or context<a: string>.

Reads a type expression as the node of the syntax tree that from_ast/2 takes, with the places of the text. known is the set of the names of the types that exist besides the built-in ones. :error for a text that is not a type expression, which parse/2 says why.

Replaces every {:named, name} in a type by the definition found in the scope, recursively.

Returns true when a value fits a type, where null fits every type. See Feel.Type.Instance.

Types

t()

@type t() ::
  :any
  | :null
  | :boolean
  | :number
  | :string
  | :date
  | :time
  | :date_and_time
  | :years_and_months_duration
  | :days_and_time_duration
  | {:list, t()}
  | {:range, t()}
  | {:context, %{optional(String.t()) => t()}}
  | {:function, [t()], t()}
  | {:named, String.t()}
  | Feel.Type.Recursive.t()
  | {:non_null, t()}
  | {:not, t()}
  | {:union, [t(), ...]}

Functions

builtin(name)

@spec builtin(Feel.Name.t()) :: {:ok, t()} | :error

Returns the built-in type with the given canonical name.

iex> Feel.Type.builtin("date and time")
{:ok, :date_and_time}
iex> Feel.Type.builtin("Employee")
:error

builtin_names()

@spec builtin_names() :: [Feel.Name.t()]

Returns the names of the built-in types, sorted.

coerce(value, type, options \\ [])

@spec coerce(Feel.Value.t(), t(), [{:compose, boolean()}]) ::
  {:ok, Feel.Value.t()} | :error

Converts a value to fit a type with the implicit conversions of section 10.3.2.9.4, such as wrapping a value in a singleton list. Returns :error when no conversion applies. See Feel.Type.Conversion.

conforms?(type, target)

@spec conforms?(t(), t()) :: boolean()

Returns true when the first type conforms to the second: an instance of the first can be used wherever the second is expected. See Feel.Type.Conformance.

equivalent?(type, other)

@spec equivalent?(t(), t()) :: boolean()

Returns true when two types are equivalent. See Feel.Type.Conformance.

format(arg1)

@spec format(t()) :: String.t()

Formats a type as FEEL type syntax.

Context members are listed in key order, and an empty context type is written context<>, which is not valid FEEL syntax.

iex> Feel.Type.format({:list, :number})
"list<number>"
iex> Feel.Type.format({:function, [:number, :string], :boolean})
"function<number, string> -> boolean"
iex> Feel.Type.format({:context, %{"name" => :string, "age" => :number}})
"context<age: number, name: string>"
iex> Feel.Type.format({:non_null, {:list, :number}})
"list<number>!"
iex> Feel.Type.format({:union, [:number, {:not, :string}]})
"number | (Any except string)"
iex> Feel.Type.format({:non_null, {:not, {:union, [:number, :string]}}})
"(Any except number | string)!"

from_ast(arg, scope)

@spec from_ast(Feel.AST.type_expression(), Feel.Scope.t()) ::
  {:ok, t()} | {:error, [Feel.Diagnostic.t(), ...]}

Turns a type node of the syntax tree (see Feel.Syntax.TypeParser) into a type.

Names are looked up among the built-in types and then in the scope. Returns the diagnostics of the first problem: an unknown or self-referential type name, or a context type with the same member twice.

instance?(value, type)

@spec instance?(Feel.Value.t(), t()) :: boolean()

The instance of operator (Table 62): null is an instance of :null only, and every other value must fit the type. See Feel.Type.Instance.

join(type, other)

@spec join(t(), t()) :: t()

Returns a conservative common supertype that both types conform to. It need not be the least expressible supertype for functions or composite declarations. See Feel.Type.Conformance.

non_null(type)

@spec non_null(t()) :: {:ok, t()} | :error

Marks a type as not null (D-48): the type of a name that must have a value.

A type that is already marked stays as it is. Null! has no values, so it is :error.

iex> Feel.Type.non_null(:number)
{:ok, {:non_null, :number}}
iex> Feel.Type.non_null({:non_null, :number})
{:ok, {:non_null, :number}}
iex> Feel.Type.non_null(:null)
:error

of(value)

@spec of(Feel.Value.t()) :: t()

Returns the type of a value (Table 43). See Feel.Type.Instance.

parse(source, options \\ [])

@spec parse(String.t(), [{:scope, Feel.Scope.t()}]) ::
  {:ok, t()} | {:error, Feel.Error.t()}

Parses a type expression such as list<number> or context<a: string>.

The names of types defined by a model are looked up in the :scope option, a Feel.Scope.t/0, and replaced by their definitions. A name that is neither built in nor in the scope is an error, as is a name that refers to itself. A context member can be a name or a string, so context<"first name": string> works.

This is the reading of a type that a host declares, and it goes past the types of an expression (D-178): Any except T is {:not, T} and T | U is {:union, [T, U]}, and a type can be put in parentheses. Any except takes everything to its right up to the end of the bracket it is in, so Any except number | string is neither, and (Any except number) | string is Any. A ! after a member of a union or after except is refused, since it is not read; mark the whole type, (Any except number)!. The type of an expression (instance of, the parameters of a function) is parsed by Feel.Syntax.TypeParser without these.

Errors

The error has kind :syntax when the text is not a type expression, and :analysis when it is one but names a type that does not exist.

iex> Feel.Type.parse("list<date and time>")
{:ok, {:list, :date_and_time}}
iex> Feel.Type.parse("function<number, string> -> boolean")
{:ok, {:function, [:number, :string], :boolean}}
iex> Feel.Type.parse("list<number!>!")
{:ok, {:non_null, {:list, {:non_null, :number}}}}
iex> Feel.Type.parse("Any except number")
{:ok, {:not, :number}}
iex> Feel.Type.parse("number | list<string | boolean>")
{:ok, {:union, [:number, {:list, {:union, [:string, :boolean]}}]}}
iex> Feel.Type.parse("(Any except number | string)!")
{:ok, {:non_null, {:not, {:union, [:number, :string]}}}}
iex> {:error, error} = Feel.Type.parse("list<Employee>")
iex> {error.kind, error.code}
{:analysis, :invalid_type_expression}

parse_ast(source, known)

@spec parse_ast(String.t(), [Feel.Name.t()]) ::
  {:ok, Feel.AST.type_expression()} | :error

Reads a type expression as the node of the syntax tree that from_ast/2 takes, with the places of the text. known is the set of the names of the types that exist besides the built-in ones. :error for a text that is not a type expression, which parse/2 says why.

iex> {:ok, {:non_null_type, _, {:type_name, _, ["number"]}}} = Feel.Type.parse_ast("number!", [])
iex> Feel.Type.parse_ast("number +", [])
:error

resolve(type, scope)

@spec resolve(t(), Feel.Scope.t()) ::
  {:ok, t()} | {:error, {:unknown_type | :cyclic_type, Feel.Name.t()}}

Replaces every {:named, name} in a type by the definition found in the scope, recursively.

Returns {:error, {:unknown_type, name}} for a name that is not in the scope, and {:error, {:cyclic_type, name}} for a type whose definition leads back to itself through unresolved named references. Guarded descriptors produced by Feel.ItemDefinitions.new/2 with recursive: :guarded are already resolved finite graphs; they are retained without recursively expanding their back references.

iex> scope = Feel.Scope.put_type(%Feel.Scope{}, "Address", {:context, %{"zip" => :string}})
iex> Feel.Type.resolve({:list, {:named, "Address"}}, scope)
{:ok, {:list, {:context, %{"zip" => :string}}}}
iex> Feel.Type.resolve({:named, "Missing"}, scope)
{:error, {:unknown_type, "Missing"}}

value_conforms?(value, type)

@spec value_conforms?(Feel.Value.t(), t()) :: boolean()

Returns true when a value fits a type, where null fits every type. See Feel.Type.Instance.