defmodule TypeCheck.Spec do
defstruct [
:name,
:param_types,
:return_type,
:location
]
import TypeCheck.Internals.Bootstrap.Macros
if_recompiling? do
use TypeCheck
alias TypeCheck.DefaultOverrides.String
@type! t() :: %__MODULE__{
name: String.t(),
param_types: list(TypeCheck.Type.t()),
return_type: TypeCheck.Type.t(),
location: [] | list({:file, String.t()} | {:line, non_neg_integer()})
}
@type! problem_tuple ::
{t(), :param_error,
%{
index: non_neg_integer(),
problem: lazy(TypeCheck.TypeError.Formatter.problem_tuple())
}, list(any())}
| {t(), :return_error,
%{
arguments: list(term()),
problem: lazy(TypeCheck.TypeError.Formatter.problem_tuple())
}, list(any())}
end
defp spec_fun_name(function, arity) do
:"__TypeCheck spec for '#{function}/#{arity}'__"
end
@doc """
Looks up the spec for a particular `{module, function, arity}`.
On success, returns `{:ok, spec}`.
On failure, returns `{:error, :not_found}`.
This is quite an advanced low-level function,
which you usually won't need to interact with directly.
c.f. `lookup!/3`.
iex(1)> defmodule Example do
...(2)> use TypeCheck
...(3)> @spec! greeter(name :: binary()) :: binary()
...(4)> def greeter(name), do: "Hello, \#{name}!"
...(5)> end
...(6)>
...(7)> {:ok, spec} = TypeCheck.Spec.lookup(Example, :greeter, 1)
...(8)> spec
#TypeCheck.Spec< greeter(name :: binary()) :: binary() >
iex> TypeCheck.Spec.lookup(Example, :nonexistent, 0)
{:error, :not_found}
"""
def lookup(module, function, arity) do
Code.ensure_loaded(module)
if function_exported?(module, spec_fun_name(function, arity), 0) do
{:ok, apply(module, spec_fun_name(function, arity), [])}
else
{:error, :not_found}
end
end
@doc """
Looks up the spec for a particular `{module, function, arity}`.
On success, returns `spec`.
Raises when the spec cannot be found.
c.f. `lookup/3`.
iex(1)> defmodule Example2 do
...(2)> use TypeCheck
...(3)> @spec! greeter(name :: binary()) :: binary()
...(4)> def greeter(name), do: "Hello, \#{name}!"
...(5)> end
...(6)>
...(7)> TypeCheck.Spec.lookup!(Example2, :greeter, 1)
#TypeCheck.Spec< greeter(name :: binary()) :: binary() >
iex> TypeCheck.Spec.lookup!(Example2, :nonexistent, 0)
** (ArgumentError) No spec found for `Example2.nonexistent/0`
"""
def lookup!(module, function, arity) do
case lookup(module, function, arity) do
{:ok, spec} ->
spec
_ ->
raise ArgumentError,
"No spec found for `#{inspect(module)}.#{to_string(function)}/#{arity}`"
end
end
@doc """
True if a spec was added to `{module, function, arity}`.
c.f. `lookup/3`.
iex(1)> defmodule Example3 do
iex(2)> use TypeCheck
iex(3)> @spec! greeter(name :: binary()) :: binary()
iex(4)> def greeter(name), do: "Hello, \#{name}!"
iex(5)> end
iex(6)>
iex(7)> TypeCheck.Spec.defined?(Example3, :greeter, 1)
true
iex(8)> TypeCheck.Spec.defined?(Example3, :nonexistent, 0)
false
"""
def defined?(module, function, arity) do
Code.ensure_loaded(module)
function_exported?(module, spec_fun_name(function, arity), 0)
end
@doc false
def create_spec_def(name, arity, param_types, return_type, {file, line}) do
spec_fun_name = spec_fun_name(name, arity)
escaped_param_types = Enum.map(param_types, &TypeCheck.Internals.Escaper.escape/1)
quote generated: true, location: :keep do
@doc false
def unquote(spec_fun_name)() do
# import TypeCheck.Builtin
%TypeCheck.Spec{
name: unquote(name),
param_types: unquote(escaped_param_types),
return_type: unquote(TypeCheck.Internals.Escaper.escape(return_type)),
location: {unquote(file), unquote(line)}
}
end
end
end
@doc false
def check_function_kind(module, function, arity) do
cond do
Module.defines?(module, {function, arity}, :def) ->
:def
Module.defines?(module, {function, arity}, :defp) ->
:defp
Module.defines?(module, {function, arity}, :defmacro) ->
:defmacro
Module.defines?(module, {function, arity}, :defmacrop) ->
:defmacrop
true ->
raise TypeCheck.CompileError,
"cannot add spec to #{to_string(module)}.#{inspect(function)}/#{inspect(arity)} because it was not defined"
end
end
@doc false
def wrap_function_with_spec(
name,
_location,
arity,
clean_params,
params_spec_code,
return_spec_code,
typespec,
caller
) do
return_spec_fun_name = :"__#{name}__type_check_return_spec__"
body =
quote do
unquote(params_spec_code)
super_result = super(unquote_splicing(clean_params))
unquote(return_spec_fun_name)(super_result, unquote_splicing(clean_params))
end
# Check if original function is public or private
function_kind = TypeCheck.Spec.check_function_kind(caller.module, name, arity)
quote generated: true, location: :keep do
if Module.get_attribute(__MODULE__, :autogen_typespec) do
@spec unquote(typespec)
end
defoverridable([{unquote(name), unquote(arity)}])
Kernel.unquote(function_kind)(unquote(name)(unquote_splicing(clean_params)),
do: unquote(body)
)
# The result is checked in a separate function
# This ensures we can convince Dialyzer to skip it. c.f. #85
@compile {:inline, [{unquote(return_spec_fun_name), unquote(arity + 1)}]}
@dialyzer {:nowarn_function, [{unquote(return_spec_fun_name), unquote(arity + 1)}]}
Kernel.defp unquote(return_spec_fun_name)(
var!(super_result, nil),
unquote_splicing(clean_params)
) do
unquote(return_spec_code)
end
end
end
@doc false
def prepare_spec_wrapper_code(name, param_types, clean_params, return_type, caller, location) do
arity = length(clean_params)
params_code = params_check_code(name, arity, param_types, clean_params, caller, location)
return_code = return_check_code(name, arity, clean_params, return_type, caller, location)
{params_code, return_code}
end
defp params_check_code(_name, _arity = 0, _param_types, _clean_params, _caller, _location) do
# No check needed for arity-0 functions.
# Also gets rid of a compiler warning 'else will never match'
quote generated: true, location: :keep do
end
end
defp params_check_code(name, arity, param_types, clean_params, caller, location) do
paired_params =
param_types
|> Enum.zip(clean_params)
|> Enum.with_index()
|> Enum.flat_map(fn {{param_type, clean_param}, index} ->
param_check_code(param_type, clean_param, index, caller, location)
end)
quote generated: true, location: :keep do
with unquote_splicing(paired_params) do
# Run actual code
else
{{:error, problem}, index} ->
raise TypeCheck.TypeError,
{
{__MODULE__.unquote(spec_fun_name(name, arity))(), :param_error,
%{index: index, problem: problem}, unquote(clean_params)},
unquote(Macro.Env.location(caller))
}
end
end
end
defp param_check_code(param_type, clean_param, index, _caller, _location) do
impl = TypeCheck.Protocols.ToCheck.to_check(param_type, clean_param)
# {file, line} = location
quote generated: true, location: :keep do
[
{{:ok, _bindings, altered_param}, _index} <- {unquote(impl), unquote(index)},
clean_param = altered_param
]
end
end
defp return_check_code(name, arity, clean_params, return_type, _caller, _location) do
return_code_check =
TypeCheck.Protocols.ToCheck.to_check(return_type, Macro.var(:super_result, nil))
# {file, line} = location
quote generated: true, location: :keep do
case unquote(return_code_check) do
{:ok, _bindings, altered_return_value} ->
# IO.inspect(unquote(return_code_check), label: :return_check_code1, limit: :infinity)
# IO.inspect(altered_return_value, label: :return_check_code2)
altered_return_value
{:error, problem} ->
raise TypeCheck.TypeError,
{__MODULE__.unquote(spec_fun_name(name, arity))(), :return_error,
%{problem: problem, arguments: unquote(clean_params)}, var!(super_result, nil)}
end
end
end
@doc false
def to_typespec(name, params_ast, return_type_ast, caller) do
clean_param_types =
Enum.map(params_ast, &TypeCheck.Internals.ToTypespec.full_rewrite(&1, caller))
clean_return_type = TypeCheck.Internals.ToTypespec.full_rewrite(return_type_ast, caller)
quote generated: true, location: :keep do
unquote(name)(unquote_splicing(clean_param_types)) :: unquote(clean_return_type)
end
end
defimpl TypeCheck.Protocols.Inspect do
def inspect(struct, opts) do
body =
Inspect.Algebra.container_doc(
Inspect.Algebra.color("(", :named_type, opts),
struct.param_types,
Inspect.Algebra.color(")", :named_type, opts),
opts,
&TypeCheck.Protocols.Inspect.inspect/2,
separator: ", ",
break: :maybe
)
|> Inspect.Algebra.group()
|> Inspect.Algebra.color(:named_type, opts)
to_string(struct.name)
|> Inspect.Algebra.color(:named_type, opts)
|> Inspect.Algebra.concat(body)
|> Inspect.Algebra.glue(Inspect.Algebra.color("::", :named_type, opts))
|> Inspect.Algebra.glue(TypeCheck.Protocols.Inspect.inspect(struct.return_type, opts))
|> Inspect.Algebra.color(:named_type, opts)
|> Inspect.Algebra.group()
|> Inspect.Algebra.color(:named_type, opts)
end
end
defimpl Elixir.Inspect do
def inspect(struct, opts) do
"#TypeCheck.Spec< "
|> Inspect.Algebra.glue(TypeCheck.Protocols.Inspect.inspect(struct, opts))
|> Inspect.Algebra.glue(">")
|> Inspect.Algebra.group()
|> Inspect.Algebra.color(:named_type, opts)
end
end
if Code.ensure_loaded?(StreamData) do
defimpl TypeCheck.Protocols.ToStreamData do
def to_gen(s) do
s.param_types
|> Enum.map(&TypeCheck.Protocols.ToStreamData.to_gen/1)
|> StreamData.fixed_list()
# |> List.to_tuple
# |> StreamData.tuple
end
end
end
end