212 lines
6.7 KiB
Elixir
212 lines
6.7 KiB
Elixir
defmodule Typespec do
|
|
## Public API
|
|
|
|
defmacro deftype({:"::", _, [name, definition]}, options), do: _deftype(name, definition, true, __CALLER__, options)
|
|
defmacro deftype(name, options), do: _deftype(name, (quote do: term), true, __CALLER__, options)
|
|
defmacro deftype({:"::", _, [name, definition]}), do: _deftype(name, definition, true, __CALLER__, [])
|
|
defmacro deftype(name), do: _deftype(name, (quote do: term), true, __CALLER__, [])
|
|
|
|
defmacro deftypep({:"::", _, [name, definition]}), do: _deftype(name, definition, false, __CALLER__, [])
|
|
defmacro deftypep(name), do: _deftype(name, (quote do: term), false, __CALLER__, [])
|
|
|
|
defmacro defspec({name, line, args},[{:do,return}]) do
|
|
spec = { :type, line, :fun, fn_args(line, args, return, [], __CALLER__) }
|
|
code = Macro.escape { { name, length(args) }, [spec] }
|
|
quote do
|
|
code = unquote(code)
|
|
@__specs__ code
|
|
{ :spec, code }
|
|
end
|
|
end
|
|
|
|
## Callbacks
|
|
|
|
defmacro __using__(options) do
|
|
Module.add_compile_callback(__CALLER__.module, __MODULE__, :__aggregate_specs__)
|
|
Module.register_attribute(__CALLER__.module, :__specs__, accumulate: true, persist: false)
|
|
Module.register_attribute(__CALLER__.module, :opaque, accumulate: false, persist: true)
|
|
|
|
quote do
|
|
import Typespec
|
|
@__typespec_options__ unquote(options)
|
|
end
|
|
end
|
|
|
|
defmacro __aggregate_specs__(module) do
|
|
options = Module.read_attribute(module, :__typespec_options__)
|
|
specs = List.reverse(Module.read_attribute(module, :__specs__))
|
|
specs = lc {k, _} inlist specs, do: { k, :proplists.append_values(k, specs) }
|
|
specs = :lists.ukeysort(1, specs)
|
|
lc attr inlist specs, do: Module.add_attribute module, :spec, attr
|
|
case options[:keep_data] do
|
|
nil -> :ok
|
|
true ->
|
|
quote do
|
|
def __specs__, do: unquote(Macro.escape(specs))
|
|
end
|
|
end
|
|
end
|
|
|
|
## Typespec conversion
|
|
|
|
# Handle unions
|
|
defp typespec({ :|, line, [_,_] } = exprs, vars, caller) do
|
|
exprs = List.reverse(collect_union(exprs))
|
|
union = lc e inlist exprs, do: typespec(e, vars, caller)
|
|
{ :type, line, :union, union }
|
|
end
|
|
|
|
# Handle binaries
|
|
defp typespec({:<<>>, line, []}, _,_) do
|
|
{:type, line, :binary, [{:integer, line, 0}, {:integer, line, 0}]}
|
|
end
|
|
|
|
defp typespec({:<<>>, line, [{:|, _, [{:_, line1, atom}, {:*, _, [{:_, line2, atom}, unit]}]}]}, _, _) when is_atom(atom) do
|
|
{:type, line, :binary, [{:integer, line1, 0}, {:integer, line2, unit}]}
|
|
end
|
|
|
|
defp typespec({:<<>>, line, [{:|, line1, [{:_, line2, atom}, base]}]}, _, _) when is_atom(atom) do
|
|
{:type, line, :binary, [{:integer, line1, base}, {:integer, line2, 0}]}
|
|
end
|
|
|
|
# Handle ranges
|
|
defp typespec({:"..", line, args}, vars, caller) do
|
|
typespec({:range, line, args}, vars, caller)
|
|
end
|
|
|
|
# Handle aliases
|
|
defp typespec({:__aliases__, _, _} = alias, vars, caller) do
|
|
atom = Macro.expand alias, caller
|
|
typespec(atom, vars, caller)
|
|
end
|
|
|
|
# Handle type operator
|
|
defp typespec({:"::", line, [var, expr] }, vars, caller) do
|
|
left = typespec(var, [elem(var,1)|vars], caller)
|
|
right = typespec(expr, vars, caller)
|
|
{ :ann_type, line, [left, right] }
|
|
end
|
|
|
|
# Handle unary ops
|
|
defp typespec({op, line, [integer]}, _, _) when op in [:+, :-] and is_integer(integer) do
|
|
{ :op, line, op, {:integer, line, integer} }
|
|
end
|
|
|
|
# Handle remote calls
|
|
defp typespec({{:., line, [remote, name]}, _, args}, vars, caller) do
|
|
remote = Macro.expand remote, caller
|
|
unless is_atom(remote), do: raise(ArgumentError, message: "Invalid remote in typespec")
|
|
remote_type({typespec(remote, vars, caller), line, typespec(name, vars, caller), args}, vars, caller)
|
|
end
|
|
|
|
# Handle tuples
|
|
defp typespec({:tuple, line, atom}, vars, caller) when is_atom(atom) do
|
|
typespec({:{}, line, []}, vars, caller)
|
|
end
|
|
|
|
defp typespec({:{}, line, []}, _, _) do
|
|
{ :type, line, :tuple, :any }
|
|
end
|
|
|
|
defp typespec({:{}, line, t}, vars, caller) when is_list(t) do
|
|
args = lc e inlist t, do: typespec(e, vars, caller)
|
|
{ :type, line, :tuple, args }
|
|
end
|
|
|
|
# Handle funs
|
|
defp typespec({:fun, line, arguments}, vars, caller) when is_list(arguments) do
|
|
args =
|
|
case List.reverse(arguments) do
|
|
[[{:do,h}]|t] -> fn_args(line, List.reverse(t), h, vars, caller)
|
|
[] -> []
|
|
_ -> [fn_args(line, arguments, vars, caller)]
|
|
end
|
|
|
|
{ :type, line, :fun, args }
|
|
end
|
|
|
|
# Handle variables or local calls
|
|
defp typespec({name, line, atom}, vars, caller) when is_atom(atom) do
|
|
if List.member?(vars, name) do
|
|
{ :var, line, name }
|
|
else
|
|
typespec({name, line, []}, vars, caller)
|
|
end
|
|
end
|
|
|
|
# Handle local calls
|
|
defp typespec({name, line, arguments}, vars, caller) do
|
|
arguments = lc arg inlist arguments, do: typespec(arg, vars, caller)
|
|
{ :type, line, name, arguments }
|
|
end
|
|
|
|
# Handle literals
|
|
defp typespec(atom, _, _) when is_atom(atom) do
|
|
{ :atom, 0, atom }
|
|
end
|
|
|
|
defp typespec(integer, _, _) when is_integer(integer) do
|
|
{ :integer, 0, integer }
|
|
end
|
|
|
|
defp typespec([], vars, caller) do
|
|
typespec({ nil, 0, [] }, vars, caller)
|
|
end
|
|
|
|
defp typespec([spec], vars, caller) do
|
|
typespec({ :list, 0, [spec] }, vars, caller)
|
|
end
|
|
|
|
defp typespec(l, _, _) when is_list(l) do
|
|
raise(ArgumentError, message: "Unexpected list #{inspect l}")
|
|
end
|
|
|
|
defp typespec(t, vars, caller) when is_tuple(t) do
|
|
args = lc e inlist tuple_to_list(t), do: typespec(e, vars, caller)
|
|
{ :type, 0, :tuple, args }
|
|
end
|
|
|
|
## Helpers
|
|
|
|
defp remote_type({remote, line, name, arguments}, vars, caller) do
|
|
arguments = lc arg inlist arguments, do: typespec(arg, vars, caller)
|
|
{ :remote_type, line, [ remote, name, arguments ] }
|
|
end
|
|
|
|
defp collect_union({ :|, _, [a, b] }), do: [b|collect_union(a)]
|
|
defp collect_union(v), do: [v]
|
|
|
|
defp fn_args(line, args, return, vars, caller) do
|
|
[fn_args(line, args, vars, caller), typespec(return, vars, caller)]
|
|
end
|
|
|
|
defp fn_args(line, [{:"...", _, _}], _vars, _caller) do
|
|
{ :type, line, :any }
|
|
end
|
|
|
|
defp fn_args(line, args, vars, caller) do
|
|
args = lc arg inlist args, do: typespec(arg, vars, caller)
|
|
{ :type, line, :product, args }
|
|
end
|
|
|
|
defp _deftype({name, _, args}, definition, export, caller, options) do
|
|
args = if is_atom(args), do: [], else: lc(arg inlist args, do: variable(arg))
|
|
vars = lc {:var, _, var} inlist args, do: var
|
|
spec = typespec(definition, vars, caller)
|
|
|
|
vars = lc ({:var, _, _} = var) inlist args, do: var
|
|
type = { name, spec, vars }
|
|
attr = if options[:opaque], do: :opaque, else: :type
|
|
|
|
module = caller.module
|
|
Module.add_attribute module, attr, type
|
|
if export do
|
|
Module.add_attribute(module, :export_type, [{name, length(vars)}])
|
|
end
|
|
{ attr, Macro.escape(type) }
|
|
end
|
|
|
|
defp variable({name, line, _}) do
|
|
{:var, line, name}
|
|
end
|
|
end |