Current section

Files

Jump to
propcheck lib type.ex
Raw

lib/type.ex

defmodule PropCheck.Type do
@moduledoc false
# @moduledoc """
# This modules defines the syntax tree of types and translates the type definitions
# of Elixir into the internal format. It also provides functions for analysis of types.
# """
@type env :: %{mfa: __MODULE__.t}
@type kind_t :: :type | :opaque | :typep | :none
@predefined_types [:atom, :integer, :pos_integer, :neg_integer, :non_neg_integer, :boolean, :byte,
:char, :number, :char_list, :any, :term, :io_list, :io_data, :module, :mfa, :arity, :port,
:node, :timeout, :fun, :binary, :bitstring]
@unsupported_types [:port, :node, :reference, :module, :mfa]
defstruct name: :none,
params: [],
mod: :none,
kind: :none,
expr: nil,
uses: []
@type t :: %__MODULE__{name: atom, params: [atom], mod: atom, kind: kind_t,
expr: TypeExpr.t, uses: [atom | mfa]}
defmodule TypeExpr do
@moduledoc false
@typedoc """
Various type constructors, `:ref` denote a reference to an existing type or
parameter, `:literal` means a literal value, in many cases, this will be an atom value.
Nonempty lists are encoded like a list, but have a second type parameter, which is the
literal `:...`.
"""
@type constructor_t :: :union | :tuple | :list | :map | :ref | :range | :fun |
:literal | :var | :none
defstruct constructor: :none,
args: [] # elements of union, tuple, list or map; or the referenced type or the literal value
@type t :: %__MODULE__{constructor: constructor_t, args: [Macro.t | t]}
def preorder(%__MODULE__{args: []} = t), do: [t]
def preorder(%__MODULE__{args: a} = t) when not is_list(a), do: [t]
def preorder(%__MODULE__{args: a} = t), do:
[t | (a |> Enum.map(fn ta -> preorder(ta) end)) |> List.flatten()]
# this looks strange, but this is an arg value which is not a type expr.
# this is ignored in the pre-order, its value is contained in the type expr above.
def preorder(_value), do: []
defimpl Inspect, for: __MODULE__ do
import Inspect.Algebra
def inspect(%{constructor: c, args: a}, opts) do
surround_many("%#{TypeExpr}{",
[constructor: c, args: a],
"}",
%Inspect.Opts{limit: :infinity},
fn {f, v}, _o -> group glue(concat(Atom.to_string(f), ":"), to_doc(v, opts)) end
)
end
end
end
@doc """
Creates an environment of named types for a module. Expects as input the list of types
of a module.
"""
@spec create_environment([Macro.t], atom) :: env
def create_environment(types, mod) do
types
|> Stream.map(&parse_type/1)
|> Stream.map(fn %__MODULE__{name: n, params: p} = t -> {{mod, n, length(p)}, t} end)
|> Enum.into(%{})
end
@doc "Takes a type specification as an Elixir AST and returns the type def."
@spec parse_type({kind_t, Macro.t, nil, any}) :: t
def parse_type({kind, {:::, _, [header, body] = _typedef}, _env})
when kind in [:type, :opaque, :typep] do
{name, _, ps} = header
params = case ps do
nil -> []
l -> l |> Enum.map(fn({n, _, _}) -> n end)
end
# IO.puts "Type body is: #{inspect body}"
%__MODULE__{name: name, params: params, kind: kind, expr: parse_body(body, params)}
end
@doc "Parse the body of a type spec as an Elixir AST and returns the `TypeExp`"
@spec parse_body(Macro.t, [atom]) :: t
def parse_body({:|, _, children}, params) do
args = children |> Enum.map(fn child -> parse_body(child, params) end)
%TypeExpr{constructor: :union, args: args}
end
def parse_body({:{}, _, children}, params) do
args = children |> Enum.map(fn child -> parse_body(child, params) end)
%TypeExpr{constructor: :tuple, args: args}
end
def parse_body({:%{}, _, children}, params) do
args = children |> Enum.map(fn child -> parse_body(child, params) end)
%TypeExpr{constructor: :map, args: args}
end
def parse_body({:.., _, children}, params) do
args = children |> Enum.map(fn child -> parse_body(child, params) end)
%TypeExpr{constructor: :range, args: args}
end
# strange syntax tree: a ĺist containing the function type
def parse_body([{:->, _, children}], params) do
args = children |> Enum.map(fn child -> parse_body(child, params) end)
%TypeExpr{constructor: :fun, args: args}
end
# "..." is any arity of a function or a non-empty list.
def parse_body({:..., _, nil}, _params) do
%TypeExpr{constructor: :literal, args: [:...]}
end
def parse_body({type, _, nil}, _params) when type in @predefined_types do
%TypeExpr{constructor: :ref, args: [type]}
end
def parse_body({t, _, nil}, params) when is_atom(t) do
case params |> Enum.member?(t) do
true -> %TypeExpr{constructor: :var, args: [t]}
_ -> %TypeExpr{constructor: :ref, args: [t]}
end
end
# handle list(t) different because list is predefined type
def parse_body({:list, _, [subtype]}, params) do
p = parse_body subtype, params
%TypeExpr{constructor: :list, args: [p]}
end
def parse_body({type, _, sub}, params) when is_atom(type) do
ps = sub |> Enum.map(fn s -> parse_body s, params end)
%TypeExpr{constructor: :ref, args: [type, ps]}
end
def parse_body(body, params) when is_tuple(body) do
args = body
|> Tuple.to_list()
|> Enum.map(fn child -> parse_body(child, params) end)
%TypeExpr{constructor: :tuple, args: args}
end
def parse_body(body, params) when is_list(body) do
args = body
|> Enum.map(fn child -> parse_body(child, params) end)
%TypeExpr{constructor: :list, args: args}
end
def parse_body(body, _params) when not(is_tuple(body)) do
%TypeExpr{constructor: :literal, args: [body]}
end
@doc "Calculates the list of referenced types"
def referenced_types({type_gen, _, l}, params)
when is_list(l) and type_gen in [:.., :{}, :list] do
l |> Enum.map(&(referenced_types(&1, params))) |> List.flatten
end
def referenced_types({:list, _, nil}, _params) do [] end
def referenced_types(_body, _params) do
[]
end
@doc "Analyzes if the type definition is recursive"
@spec is_recursive(mfa, env ) :: boolean
def is_recursive({_m, _f, _a} = t, env) do
# ensure that t is defined in env, otherwise we cannot check anything
{:ok, type} = env |> Map.fetch(t)
%__MODULE__{} = type
is_recursive(t, type, env)
end
def is_recursive(mfa, %__MODULE__{expr: expr, params: ps}, env) do
# add all parameters to the environment. We use a TypeExpr for it
# but what is the true reason for this?
new_env = ps
|> Stream.map(fn p ->
t = %TypeExpr{constructor: :var, args: [p]}
{t, t}
end)
|> Enum.into(env)
# |> IO.inspect
# IO.puts "OK, new_env is there, now the expressions!"
is_recursive(mfa, expr, new_env)
end
@doc "Analyzes of the type expression for `mfa` is recursive"
@spec is_recursive(mfa, TypeExpr.t, env) :: boolean
def is_recursive(_mfa, %TypeExpr{constructor: con}, _env) when
con in [:literal, :range], do: false
def is_recursive(_mfa, %TypeExpr{constructor: :var}, _env), do: false
def is_recursive(_mfa, %TypeExpr{constructor: :ref, args: [type]}, _env)
when type in @predefined_types, do: false
def is_recursive(mfa, %TypeExpr{constructor: :ref, args: [type]}, env) do
# type is not predefined ==> it must existent in env, so we can look deeper into it.
case type do
^mfa -> true
_ -> # anything other must be present in the environment
#
# Hmm, what about types of other modules (=mfa) or parameterized types?
{:ok, t} = env |> Map.fetch(type)
is_recursive(mfa, t, env)
end
end
def is_recursive(mfa, %TypeExpr{constructor: con, args: args}, env)
when con in [:union, :tuple, :list, :map] do
args |> Enum.any?(fn t -> is_recursive(mfa, t, env) end)
end
def is_recursive(mfa, %TypeExpr{constructor: :ref, args: [t | args]}, env) do
case match_type(mfa, t) do
true -> true
_ -> args |> Enum.any?(fn ta -> is_recursive(mfa, ta, env) end)
end
end
def match_type(mfa1, mfa2) when mfa1 == mfa2, do: true
def match_type({_m, f1, _a}, f2) when f1 == f2, do: true
def match_type(_, _), do: false
@spec type_generators(env) :: Macro.t
def type_generators(env) do
env
|> Enum.map(fn {mfa, type} -> type_generator(mfa, type) end)
end
def type_generator(mfa, %__MODULE__{expr: type, params: ps, kind: kind}) do
header = header_for_type(mfa, ps)
body = body_for_type(type)
d = if (kind == :typep) do :defp else :def end
{d, [context: Elixir, import: Kernel],
[header, [do: body]]}
end
@doc "Generates the AST for a function head."
def header_for_type({m, f, a}, ps) when length(ps) == a do
{
f, [context: m],
if (ps == []) do nil
else
ps |> Enum.map(fn p -> {p, [], m} end)
end
}
end
def body_for_type(%TypeExpr{constructor: :ref, args: [t]})
when t in @unsupported_types, do: throw "unsupported type port"
def body_for_type(%TypeExpr{constructor: :ref, args: [t]})
when t in @predefined_types, do: body_for_predefined_type(t)
def body_for_type(%TypeExpr{constructor: :list, args: [p]}) do
%TypeExpr{constructor: :var, args: [t]} = p
quote do
list(unquote(t))
end
end
def body_for_type(%TypeExpr{constructor: _con, args: _args} = t) do
t_msg = inspect t
quote do
throw "Unimplemented generator for type " <> unquote(t_msg)
end
end
def body_for_predefined_type(:atom) do quote do atom end end
def body_for_predefined_type(:any) do quote do any end end
def body_for_predefined_type(:term) do quote do term end end
def body_for_predefined_type(:float) do quote do float(:inf, :inf) end end
def body_for_predefined_type(:integer) do quote do integer(:inf, :inf) end end
def body_for_predefined_type(:non_neg_integer) do quote do integer(0, :inf) end end
def body_for_predefined_type(:pos_integer) do quote do integer(1, :inf) end end
def body_for_predefined_type(:neg_integer) do quote do integer(:inf, -1) end end
def body_for_predefined_type(:byte) do quote do integer(0, 255) end end
def body_for_predefined_type(:arity) do quote do integer(0, 255) end end
def body_for_predefined_type(:boolean) do quote do boolean end end
def body_for_predefined_type(:char) do quote do char end end
def body_for_predefined_type(:number) do quote do number end end
def body_for_predefined_type(:char_list) do quote do char_list end end
def body_for_predefined_type(:io_list) do quote do io_list end end
def body_for_predefined_type(:io_data) do quote do io_data end end
def body_for_predefined_type(:timeout) do quote do timeout end end
def body_for_predefined_type(:fun) do quote do fun end end
def body_for_predefined_type(:binary) do quote do binary end end
def body_for_predefined_type(:bitstring) do quote do bitstring end end
def body_for_predefined_type({:.., _, [left, right]}) do quote do integer(unquote(left), unquote(right)) end end
def body_for_predefined_type({:{}, _, tuple_vars}) do quote do tuple(unquote(tuple_vars)) end end
def body_for_predefined_type({:list, _, nil}) do quote do list(any) end end
def body_for_predefined_type({:list, _, [_type]}) do :ok end
end