From e9334fb7dc406d0e86c08da5fc40529022161f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 5 Nov 2012 14:51:51 +0100 Subject: [PATCH] Tidy up Typespec API, improve docs --- lib/elixir/lib/kernel/typespec.ex | 451 +++++++++++++---------- lib/elixir/lib/protocol.ex | 16 +- lib/elixir/src/elixir_module.erl | 8 - lib/elixir/test/elixir/typespec_test.exs | 150 ++++---- lib/iex/lib/iex/helpers.ex | 123 ++++--- 5 files changed, 406 insertions(+), 342 deletions(-) diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex index 3b977b7f1d..f6ec450e4b 100644 --- a/lib/elixir/lib/kernel/typespec.ex +++ b/lib/elixir/lib/kernel/typespec.ex @@ -1,24 +1,69 @@ defmodule Kernel.Typespec do - @moduledoc false + @moduledoc """ + Holds macros and functions for working with typespecs. + The attributes `@type`, `@opaque`, `@typep`, `@spec` and + `@callback` available in modules are handled by the equivalent + macros defined by this module. + + See http://www.erlang.org/doc/reference_manual/typespec.html + for more information. + """ + + @doc """ + Defines a type. + This macro is the one responsible to handle the attribute @type. + + ## Examples + + @type my_type :: atom + + """ defmacro deftype(type) do quote do Kernel.Typespec.deftype(:type, (quote line: :keep, do: unquote(type)), __ENV__) end end + @doc """ + Defines an opaque type. + This macro is the one responsible to handle the attribute @opaque. + + ## Examples + + @opaque my_type :: atom + + """ defmacro defopaque(type) do quote do Kernel.Typespec.deftype(:opaque, (quote line: :keep, do: unquote(type)), __ENV__) end end + @doc """ + Defines a private type. + This macro is the one responsible to handle the attribute @typep. + + ## Examples + + @typep my_type :: atom + + """ defmacro deftypep(type) do quote do Kernel.Typespec.deftype(:typep, (quote line: :keep, do: unquote(type)), __ENV__) end end + @doc """ + Defines a spec. + This macro is the one responsible to handle the attribute @spec. + + ## Examples + + @spec add(number, number), do: number + + """ defmacro defspec(spec, block) do quote do Kernel.Typespec.defspec(:spec, (quote line: :keep, do: unquote spec), @@ -26,6 +71,15 @@ defmodule Kernel.Typespec do end end + @doc """ + Defines a callback. + This macro is the one responsible to handle the attribute @callback. + + ## Examples + + @callback add(number, number), do: number + + """ defmacro defcallback(spec, block) do quote do Kernel.Typespec.defspec(:callback, (quote line: :keep, do: unquote spec), @@ -33,6 +87,174 @@ defmodule Kernel.Typespec do end end + ## Helpers + + @doc """ + Defines a `type`, `typep` or `opaque` by receiving Erlang's typespec. + """ + def define_type(module, kind, { name, _, vars } = type) when kind in [:type, :typep, :opaque] do + { kind, export } = + case kind do + :type -> { :type, true } + :typep -> { :type, false } + :opaque -> { :opaque, true } + end + + Module.compile_typespec module, kind, type + + if export do + Module.compile_typespec module, :export_type, [{ name, length(vars) }] + end + + type + end + + @doc """ + Defines a `spec` by receiving Erlang's typespec. + """ + def define_spec(module, tuple, definition) do + Module.compile_typespec module, :spec, { tuple, definition } + end + + @doc """ + Defines a `callback` by receiving Erlang's typespec. + """ + def define_callback(module, tuple, definition) do + Module.compile_typespec module, :callback, { tuple, definition } + end + + @doc """ + Returns true if the current module defines a given type + (private, opaque or not). This function is only available + for modules being compiled. + """ + def defines_type?(module, name, arity) do + finder = match?({ ^name, _, vars } when length(vars) == arity, &1) + :lists.any(finder, Module.get_attribute(module, :type)) or + :lists.any(finder, Module.get_attribute(module, :opaque)) + end + + @doc """ + Returns true if the current module defines a given spec. + This function is only available for modules being compiled. + """ + def defines_spec?(module, name, arity) do + tuple = { name, arity } + :lists.any(match?(^tuple, &1), Module.get_attribute(module, :spec)) + end + + @doc """ + Returns true if the current module defines a callback. + This function is only available for modules being compiled. + """ + def defines_callback?(module, name, arity) do + tuple = { name, arity } + :lists.any(match?(^tuple, &1), Module.get_attribute(module, :callback)) + end + + @doc """ + Converts a spec clause back to Elixir AST. + Returns a 2-items tuple with the spec arguments and return result. + """ + def spec_to_ast({ :type, _line, :fun, [{:type, _, :product, args},result] }) do + args = lc arg inlist args, do: typespec_to_ast(arg) + { args, typespec_to_ast(result) } + end + + def spec_to_ast({ :type, _, :fun, [] }) do + { [], quote do: term } + end + + @doc """ + Converts a type clause back to Elixir AST. + """ + def type_to_ast({ name, type, args }) do + args = lc arg inlist args, do: typespec_to_ast(arg) + quote do: unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_ast(type)) + end + + @doc """ + Returns all types available from the beam. + + It is returned as a list of tuples where the first + element is the type (`:typep`, `:type` and `:opaque`). + + The module has to have a corresponding beam file + on the file system. + """ + def beam_types(module) do + case abstract_code(module) do + { :ok, abstract_code } -> + exported_types = lc { :attribute, _, :export_type, types } inlist abstract_code, do: types + exported_types = List.flatten(exported_types) + + lc { :attribute, _, kind, { name, _, args } = type } inlist abstract_code, kind in [:opaque, :type] do + cond do + kind == :opaque -> { :opaque, type } + List.member?(exported_types, { name, length(args) }) -> { :type, type } + true -> { :typep, type } + end + end + _ -> + [] + end + end + + @doc """ + Returns all specs available from the beam. + + It is returned as a list of tuples where the first + element is spec name and arity and the second is the spec. + + The module has to have a corresponding beam file + on the file system. + """ + def beam_specs(module) do + from_abstract_code(module, :spec) + end + + @doc """ + Returns all callbacks available from the beam. + + It is returned as a list of tuples where the first + element is spec name and arity and the second is the spec. + + The module has to have a corresponding beam file + on the file system. + """ + def beam_callbacks(module) do + from_abstract_code(module, :callback) + end + + defp from_abstract_code(module, kind) do + case abstract_code(module) do + { :ok, abstract_code } -> + lc { :attribute, _, abs_kind, value } inlist abstract_code, kind == abs_kind, do: value + _ -> + [] + end + end + + defp abstract_code(module) do + case :beam_lib.chunks(abstract_code_beam(module), [:abstract_code]) do + {:ok, { _, [{ :abstract_code, { raw_abstract_v1, abstract_code } }] } } -> + { :ok, abstract_code } + _ -> + [] + end + end + + defp abstract_code_beam(module) when is_atom(module) do + case :code.which(module) do + :non_existing -> module + file -> file + end + end + + defp abstract_code_beam(binary) when is_binary(binary) do + binary + end + ## Macro callbacks @doc false @@ -44,7 +266,7 @@ defmodule Kernel.Typespec do do_deftype(kind, type, { :term, caller.line, nil }, caller) end - defp do_deftype(raw_kind, { name, _, args }, definition, caller) do + defp do_deftype(kind, { name, _, args }, definition, caller) do args = if is_atom(args) do [] @@ -52,26 +274,13 @@ defmodule Kernel.Typespec do lc(arg inlist args, do: variable(arg)) end - { kind, export } = - case raw_kind do - :type -> { :type, true } - :typep -> { :type, false } - :opaque -> { :opaque, true } - end - 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 } - Module.compile_typespec caller.module, kind, type - - if export do - Module.compile_typespec(caller.module, :export_type, [{ name, length(vars) }]) - end - - { raw_kind, type } + define_type(caller.module, kind, type) end @doc false @@ -83,224 +292,68 @@ defmodule Kernel.Typespec do code end - ## Helpers + ## To AST conversion - @doc """ - Returns true if the current module defines a given type - (opaque or not). This function is only available for modules - being compiled. - """ - def defines_type?(module, name, arity) do - finder = match?({ ^name, _, vars } when length(vars) == arity, &1) - :lists.any(finder, Module.get_attribute(module, :type)) or - :lists.any(finder, Module.get_attribute(module, :opaque)) - end - - @doc """ - Defines a callback from a spec if one is available. - Returns true if successful, false otherwise. - """ - def callback_from_spec(module, name, arity) do - tuple = { name, arity } - specs = Module.get_attribute(module, :spec) - - found = lc { k, v } inlist specs, k == tuple do - Module.compile_typespec(module, :callback, { tuple, v }) - true - end - - found != [] - end - - @doc """ - Returns type spec retrieved from a BEAM file in a readable form - """ - def to_binary(typespec) do - Macro.to_binary(typespec_to_ast(typespec)) - end - - ## to_ast - - defp typespec_to_ast({{:spec, {name, _arity}}, args}) do - contents = lc arg inlist args do - {:__block__, _, [v]} = spec_typespec_to_ast(arg) - [product|args] = Enum.reverse(v) - args = Enum.reverse(args) - quote do - @spec unquote(name)(unquote_splicing(args)), unquote(product) - end - end - quote do - unquote_splicing(contents) - end - end - - defp typespec_to_ast({:type, {name, type, args}}) do - args = lc arg inlist args, do: typespec_to_ast(arg) - quote do: @type unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_ast(type)) - end - - defp typespec_to_ast({:typep, {name, type, args}}) do - args = lc arg inlist args, do: typespec_to_ast(arg) - quote do: @typep unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_ast(type)) - end - - defp typespec_to_ast({:opaque, {name, type, args}}) do - args = lc arg inlist args, do: typespec_to_ast(arg) - quote do: @opaque unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_ast(type)) - end - - defp typespec_to_ast({:type, line, :tuple, :any}) do + defp typespec_to_ast({ :type, line, :tuple, :any }) do typespec_to_ast({:type, line, :tuple, []}) end - defp typespec_to_ast({:type, line, :tuple, args}) do + defp typespec_to_ast({ :type, line, :tuple, args }) do args = lc arg inlist args, do: typespec_to_ast(arg) { :{}, line, args } end - defp typespec_to_ast({:type, _line, :binary, [arg1, arg2]}) do + defp typespec_to_ast({ :type, line, :binary, [arg1, arg2] }) do [arg1, arg2] = lc arg inlist [arg1, arg2], do: typespec_to_ast(arg) cond do arg2 == 0 -> - quote do: <<_ :: unquote(arg1)>> + quote line: line, do: <<_ :: unquote(arg1)>> arg1 == 0 -> - quote do: <<_ :: _ * unquote(arg2)>> + quote line: line, do: <<_ :: _ * unquote(arg2)>> true -> - quote do: <<_ :: unquote(arg1) * unquote(arg2)>> + quote line: line, do: <<_ :: unquote(arg1) * unquote(arg2)>> end end - defp typespec_to_ast({:type, _line, :union, args}) do + defp typespec_to_ast({ :type, line, :union, args }) do args = lc arg inlist args, do: typespec_to_ast(arg) Enum.reduce tl(args), hd(args), - fn(arg, expr) -> - quote do: unquote(expr) | unquote(arg) - end + fn(arg, expr) -> { :|, line, [expr, arg] } end end - defp typespec_to_ast({:type, _line, :fun, [{:type, _, :product, args},result]}) do + defp typespec_to_ast({ :type, line, :fun, [{:type, _, :product, args},result] }) do args = lc arg inlist args, do: typespec_to_ast(arg) - quote do - fun(unquote_splicing(args ++ [[do: typespec_to_ast(result)]])) - end + { :fun, line, args ++ [[do: typespec_to_ast(result)]] } end - defp typespec_to_ast({:type, _, :fun, []}) do - quote do: fun() + defp typespec_to_ast({ :type, line, :fun, [] }) do + { :fun, line, [] } end - defp typespec_to_ast({:type, _, name, args}) do + defp typespec_to_ast({ :type, line, name, args }) do args = lc arg inlist args, do: typespec_to_ast(arg) - quote do: unquote(name)(unquote_splicing(args)) + { name, line, args } end - defp typespec_to_ast({:var, line, var}) do - quote hygiene: false, do: unquote({var, line, nil}) + defp typespec_to_ast({ :var, line, var }) do + { var, line, nil } end - defp typespec_to_ast({:remote_type, _line, [mod, name, args]}) do + defp typespec_to_ast({ :remote_type, line, [mod, name, args] }) do args = lc arg inlist args, do: typespec_to_ast(arg) - quote do: unquote(typespec_to_ast(mod)).unquote(typespec_to_ast(name))(unquote_splicing(args)) + dot = { :., line, [typespec_to_ast(mod), typespec_to_ast(name)] } + { dot, line, args } end - defp typespec_to_ast({:ann_type, _line, [var, type]}) do - quote do: unquote(typespec_to_ast(var)) :: unquote(typespec_to_ast(type)) + defp typespec_to_ast({ :ann_type, line, [var, type] }) do + { :::, line, [typespec_to_ast(var), typespec_to_ast(type)] } end - defp typespec_to_ast({t, _line, imm}) when is_atom(t) do - imm + defp typespec_to_ast({ t, _line, atom }) when is_atom(t) do + atom end - defp spec_typespec_to_ast({:type, _line, :fun, [{:type, _, :product, args},result]}) do - args = lc arg inlist args, do: typespec_to_ast(arg) - quote do - [unquote_splicing(args), [do: unquote(typespec_to_ast(result))]] - end - end - - defp spec_typespec_to_ast({:type, _, :fun, []}) do - quote do: [] - end - - @doc """ - Retrieves all type specifications from a module. - - The module has to have a corresponding beam file on the file system. - """ - def types(module) do - types(module, nil, nil) - end - - @doc """ - Retrieves all type specifications from a module with a given name and arity. - - The module has to have a corresponding beam file on the file system. - """ - def types(module, type, arity) do - case abstract_code(module) do - {:ok, abstract_code} -> - exported_types = List.flatten(lc {:attribute, _, :export_type, types} inlist abstract_code, do: types) - lc {:attribute, _, type_a, {t, _, a} = ti} inlist abstract_code, type_a in [:opaque, :type], (nil?(type) and nil?(arity)) or (t == type and length(a) == arity) do - cond do - type_a == :opaque -> - {:opaque, ti} - List.member?(exported_types, {t, length(a)}) -> - {:type, ti} - true -> - {:typep, ti} - end - end - _ -> - [] - end - end - - - @doc """ - Retrieves all functions specifications from a module. - - The module has to have a corresponding beam file on the file system. - """ - def specs(module) do - specs(module, nil, nil) - end - - @doc """ - Retrieves all functions specifications from a module for a given name and arity. - - The module has to have a corresponding beam file on the file system. - """ - def specs(module, function, arity) do - case abstract_code(module) do - {:ok, abstract_code} -> - lc {:attribute, _, :spec, {{f,a}=fa, s}} inlist abstract_code, (nil?(function) and nil?(arity)) or (f == function and a == arity), do: {{:spec, fa}, s} - _ -> - [] - end - end - - - defp abstract_code(module) do - case :beam_lib.chunks(abstract_code_beam(module), [:abstract_code]) do - {:ok, {_, [{:abstract_code, {raw_abstract_v1, abstract_code}}]}} -> - {:ok, abstract_code} - _ -> - :error - end - end - - defp abstract_code_beam(module) when is_atom(module) do - case :code.which(module) do - :non_existing -> module - file -> file - end - end - defp abstract_code_beam(binary) when is_binary(binary) do - binary - end - - ## Typespec conversion + ## From AST conversion # Handle unions defp typespec({ :|, line, [_,_] } = exprs, vars, caller) do @@ -400,7 +453,7 @@ defmodule Kernel.Typespec do # Handle local calls defp typespec({:string, line, arguments}, vars, caller) do - IO.write "#{caller.file}.#{caller.line}: warning: string() type use is discouraged. For character lists, use char_list() type, for strings, String.t()\n" + IO.write "#{caller.file}:#{caller.line}: warning: string() type use is discouraged. For character lists, use char_list() type, for strings, String.t()\n" arguments = lc arg inlist arguments, do: typespec(arg, vars, caller) { :type, line, :string, arguments } end @@ -432,7 +485,7 @@ defmodule Kernel.Typespec do end defp typespec(l, _, _) when is_list(l) do - raise(ArgumentError, message: "Unexpected list #{inspect l}") + raise ArgumentError, message: "Unexpected list #{inspect l}" end defp typespec(t, vars, caller) when is_tuple(t) do diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index fe390c3b03..84bd75c51f 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -276,6 +276,7 @@ defmodule Protocol.DSL do # We need to use :lists because Enum is not available yet require :lists, as: L + @doc false def args_and_body(module, name, arity) do # Generate arguments according the arity. The arguments # are named xa, xb and so forth. We cannot use string @@ -304,6 +305,19 @@ defmodule Protocol.DSL do { args, body } end + @doc false + def callback_from_spec(module, name, arity) do + tuple = { name, arity } + specs = Module.get_attribute(module, :spec) + + found = lc { k, v } inlist specs, k == tuple do + Kernel.Typespec.define_callback(module, tuple, v) + true + end + + found != [] + end + defp conversions_for(module) do only = Module.get_attribute(module, :only) except = Module.get_attribute(module, :except) @@ -378,7 +392,7 @@ defmodule Protocol.DSL do # Convert the spec to callback if possible, # otherwise generate a dummy callback - Kernel.Typespec.callback_from_spec(__MODULE__, name, arity) || + Protocol.DSL.callback_from_spec(__MODULE__, name, arity) || @callback unquote(name)(unquote_splicing(type_args)), do: term end end diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 88f02be41e..009997fb12 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -33,9 +33,6 @@ data_table(Module) -> docs_table(Module) -> ?ELIXIR_ATOM_CONCAT([o, Module]). -spec_table(Module) -> - ?ELIXIR_ATOM_CONCAT([s, Module]). - %% TRANSFORMATION FUNCTIONS %% Transformation of args and scope into a compiled erlang call. @@ -87,7 +84,6 @@ compile(Line, Module, Block, Vars, #elixir_scope{} = S) when is_atom(Module) -> after ets:delete(data_table(Module)), ets:delete(docs_table(Module)), - ets:delete(spec_table(Module)), elixir_def:delete_table(Module), elixir_import:delete_table(Module) end; @@ -130,10 +126,6 @@ build(Line, File, Module) -> DocsTable = docs_table(Module), ets:new(DocsTable, [ordered_set, named_table, public]), - %% Holds specs in a format it is easier to query. - SpecTable = spec_table(Module), - ets:new(SpecTable, [duplicate_bag, named_table, public]), - %% We keep a separated table for function definitions %% and another one for imports. We keep them in different %% tables for organization and speed purpose (since the diff --git a/lib/elixir/test/elixir/typespec_test.exs b/lib/elixir/test/elixir/typespec_test.exs index 5e3cde716e..08287724fa 100644 --- a/lib/elixir/test/elixir/typespec_test.exs +++ b/lib/elixir/test/elixir/typespec_test.exs @@ -21,42 +21,42 @@ defmodule Typespec.Test.Type do spec = test_module do @type mytype end - assert {:type,{:mytype,{:type,_,:term,[]},[]}} = spec + assert {:mytype,{:type,_,:term,[]},[]} = spec end test "@type with a single type" do spec = test_module do @type mytype :: term end - assert {:type,{:mytype,{:type,_,:term,[]},[]}} = spec + assert {:mytype,{:type,_,:term,[]},[]} = spec end test "@type with an atom" do spec = test_module do @type mytype :: :atom end - assert {:type,{:mytype,{:atom,_,:atom},[]}} = spec + assert {:mytype,{:atom,_,:atom},[]} = spec end test "@type with an atom alias" do spec = test_module do @type mytype :: Atom end - assert {:type,{:mytype,{:atom,_,Atom},[]}} = spec + assert {:mytype,{:atom,_,Atom},[]} = spec end test "@type with an integer" do spec = test_module do @type mytype :: 10 end - assert {:type,{:mytype,{:integer,_,10},[]}} = spec + assert {:mytype,{:integer,_,10},[]} = spec end test "@type with a negative integer" do spec = test_module do @type mytype :: -10 end - assert {:type,{:mytype,{:op, _, :-, {:integer,_,10}},[]}} = spec + assert {:mytype,{:op, _, :-, {:integer,_,10}},[]} = spec end test "@type with a remote type" do @@ -65,50 +65,50 @@ defmodule Typespec.Test.Type do t2 = @type mytype_arg :: Remote.type(integer) {t1, t2} end - assert {:type,{:mytype,{:remote_type, _, [{:atom, _, Remote.Some},{:atom, _, :type}, []]},[]}} = spec1 - assert {:type,{:mytype_arg,{:remote_type, _, [{:atom, _, Remote},{:atom, _, :type},[{:type,_, :integer,[]}]]},[]}} = spec2 + assert {:mytype,{:remote_type, _, [{:atom, _, Remote.Some},{:atom, _, :type}, []]},[]} = spec1 + assert {:mytype_arg,{:remote_type, _, [{:atom, _, Remote},{:atom, _, :type},[{:type,_, :integer,[]}]]},[]} = spec2 end test "@type with a binary" do spec = test_module do @type mytype :: binary end - assert {:type,{:mytype,{:type,_,:binary, []},[]}} = spec + assert {:mytype,{:type,_,:binary, []},[]} = spec end test "@type with an empty binary" do spec = test_module do @type mytype :: <<>> end - assert {:type,{:mytype,{:type,_,:binary, [{:integer, _, 0},{:integer, _, 0}]},[]}} = spec + assert {:mytype,{:type,_,:binary, [{:integer, _, 0},{:integer, _, 0}]},[]} = spec end test "@type with a binary with a base size" do spec = test_module do @type mytype :: <<_ :: 3>> end - assert {:type,{:mytype,{:type,_,:binary, [{:integer, _, 3},{:integer, _, 0}]},[]}} = spec + assert {:mytype,{:type,_,:binary, [{:integer, _, 3},{:integer, _, 0}]},[]} = spec end test "@type with a binary with a unit size" do spec = test_module do @type mytype :: <<_ :: _ * 8>> end - assert {:type,{:mytype,{:type,_,:binary, [{:integer, _, 0},{:integer, _, 8}]},[]}} = spec + assert {:mytype,{:type,_,:binary, [{:integer, _, 0},{:integer, _, 8}]},[]} = spec end test "@type with a range" do spec = test_module do @type mytype :: range(1,10) end - assert {:type,{:mytype,{:type,_,:range, [{:integer, _, 1},{:integer, _, 10}]},[]}} = spec + assert {:mytype,{:type,_,:range, [{:integer, _, 1},{:integer, _, 10}]},[]} = spec end test "@type with a range op" do spec = test_module do @type mytype :: 1..10 end - assert {:type,{:mytype,{:type,_,:range, [{:integer, _, 1},{:integer, _, 10}]},[]}} = spec + assert {:mytype,{:type,_,:range, [{:integer, _, 1},{:integer, _, 10}]},[]} = spec end test "@type with a tuple" do @@ -117,40 +117,40 @@ defmodule Typespec.Test.Type do t2 = @type mytype1 :: {} {t1, t2} end - assert {:type,{:mytype,{:type,_,:tuple, :any},[]}} = spec1 - assert {:type,{:mytype1,{:type,_,:tuple, :any},[]}} = spec2 + assert {:mytype,{:type,_,:tuple, :any},[]} = spec1 + assert {:mytype1,{:type,_,:tuple, :any},[]} = spec2 end test "@type with a fun" do spec = test_module do @type mytype :: fun end - assert {:type,{:mytype,{:type,_,:fun, []},[]}} = spec + assert {:mytype,{:type,_,:fun, []},[]} = spec end test "@type with a fun with arguments and return type" do spec = test_module do @type mytype :: fun(integer, integer, do: integer) end - assert {:type,{:mytype,{:type,_,:fun, [{:type, _, :product, - [{:type, _, :integer, []}, {:type, _, :integer, []}]}, - {:type, _, :integer, []}]},[]}} = spec + assert {:mytype,{:type,_,:fun, [{:type, _, :product, + [{:type, _, :integer, []}, {:type, _, :integer, []}]}, + {:type, _, :integer, []}]},[]} = spec end test "@type with a fun with no arguments and return type" do spec = test_module do @type mytype :: fun(do: integer) end - assert {:type,{:mytype,{:type,_,:fun, [{:type, _, :product, []}, - {:type, _, :integer, []}]}, []}} = spec + assert {:mytype,{:type,_,:fun, [{:type, _, :product, []}, + {:type, _, :integer, []}]}, []} = spec end test "@type with a fun with any arity and return type" do spec = test_module do @type mytype :: fun(..., do: integer) end - assert {:type,{:mytype,{:type,_,:fun, [{:type, _, :any}, - {:type, _, :integer, []}]}, []}} = spec + assert {:mytype,{:type,_,:fun, [{:type, _, :any}, + {:type, _, :integer, []}]}, []} = spec end test "@type with a union" do @@ -158,17 +158,17 @@ defmodule Typespec.Test.Type do @type mytype :: integer | char_list | atom end - assert {:type,{:mytype,{:type,_,:union, [{:type, _, :integer, []}, - {:remote_type, _, [{:atom, _, :elixir},{:atom, _, :char_list}, []]}, - {:type, _, :atom, []}]}, []}} = spec + assert {:mytype,{:type,_,:union, [{:type, _, :integer, []}, + {:remote_type, _, [{:atom, _, :elixir},{:atom, _, :char_list}, []]}, + {:type, _, :atom, []}]}, []} = spec end test "@type with an access macro" do spec = test_module do @type mytype :: Range[first: integer] end - assert {:type,{:mytype,{:type, _, :tuple, - [{:atom, _, Range}, {:type, _, :integer, []}, {:type, _, :any, []}]}, []}} = spec + assert {:mytype,{:type, _, :tuple, + [{:atom, _, Range}, {:type, _, :integer, []}, {:type, _, :any, []}]}, []} = spec end test "@type with parameters" do @@ -178,9 +178,9 @@ defmodule Typespec.Test.Type do t3 = @type mytype2(x,y) :: {x,y} {t1,t2,t3} end - assert {:type,{:mytype,{:var,_,:x},[{:var,_,:x}]}} = spec1 - assert {:type,{:mytype1,{:type,_,:list,[{:var,_,:x}]},[{:var,_,:x}]}} = spec2 - assert {:type,{:mytype2,{:type,_,:tuple,[{:var,_,:x},{:var,_,:y}]},[{:var,_,:x},{:var, _, :y}]}} = spec3 + assert {:mytype,{:var,_,:x},[{:var,_,:x}]} = spec1 + assert {:mytype1,{:type,_,:list,[{:var,_,:x}]},[{:var,_,:x}]} = spec2 + assert {:mytype2,{:type,_,:tuple,[{:var,_,:x},{:var,_,:y}]},[{:var,_,:x},{:var, _, :y}]} = spec3 end test "@type with annotations" do @@ -189,15 +189,15 @@ defmodule Typespec.Test.Type do t2 = @type mytype1 :: fun(a :: integer, do: integer) {t1,t2} end - assert {:type, {:mytype, {:ann_type, _, [{:var, _, :named}, {:type, _, :integer, []}]}, []}} = spec1 - assert {:type, {:mytype1,{:type,_,:fun,[{:type,_,:product,[{:ann_type,_,[{:var,_,:a},{:type,_,:integer,[]}]}]},{:type,_,:integer,[]}]},[]}} = spec2 + assert {:mytype, {:ann_type, _, [{:var, _, :named}, {:type, _, :integer, []}]}, []} = spec1 + assert {:mytype1,{:type,_,:fun,[{:type,_,:product,[{:ann_type,_,[{:var,_,:a},{:type,_,:integer,[]}]}]},{:type,_,:integer,[]}]},[]} = spec2 end test "@opaque(type)" do spec = test_module do @opaque mytype(x) :: x end - assert {:opaque,{:mytype,{:var,_,:x},[{:var,_,:x}]}} = spec + assert {:mytype,{:var,_,:x},[{:var,_,:x}]} = spec end test "@type + opaque" do @@ -267,27 +267,10 @@ defmodule Typespec.Test.Type do ] = List.sort(specs) end - test "defines a callback from a spec" do - callbacks = test_module do - def myfun(x), do: x - @spec myfun(integer), do: integer - @spec myfun(char_list), do: char_list - Kernel.Typespec.callback_from_spec(__MODULE__, :myfun, 1) - @callback - end + # Conversion to AST - assert [ - { {:myfun,1}, {:type,_,:fun,[{:type,_,:product,[{:type,_,:integer,[]}]},{:type,_,:integer,[]}]} }, - { {:myfun,1}, {:type,_,:fun,[{:type,_,:product,[ - {:remote_type, _, [{:atom, _, :elixir},{:atom, _, :char_list}, []]}]}, - {:remote_type, _, [{:atom, _, :elixir},{:atom, _, :char_list}, []]}]} } - ] = List.sort(callbacks) - end - - # to_binary - - test "to_binary types" do - types = [ + test "type_to_ast" do + quoted = [ (quote do: @type with_ann() :: (t :: atom())), (quote do: @type empty_tuple_type() :: {}), (quote do: @type imm_type_1() :: 1), @@ -302,19 +285,20 @@ defmodule Typespec.Test.Type do (quote do: @type ftype() :: fun() | fun(do: integer()) | fun(integer(), do: integer())), ] - spec = test_module do - Module.eval_quoted __MODULE__, quote do: unquote_splicing(types) + types = test_module do + Module.eval_quoted __MODULE__, quote do: unquote_splicing(quoted) @type ++ @opaque end - spec = Enum.reverse(spec) + types = Enum.reverse(types) - lc { typespec, definition } inlist Enum.zip(spec, types) do - assert Kernel.Typespec.to_binary({ :type, typespec }) == Macro.to_binary(definition) + lc { type, definition } inlist Enum.zip(types, quoted) do + ast = Kernel.Typespec.type_to_ast(type) + assert Macro.to_binary(quote do: @type unquote(ast)) == Macro.to_binary(definition) end end - test "to_binary spec" do + test "spec_to_ast" do specs = [ (quote do: @spec a(), do: integer()), (quote do: @spec a(atom()), do: integer()), @@ -327,28 +311,34 @@ defmodule Typespec.Test.Type do Enum.reverse @spec end - lc { { tuple, spec }, definition } inlist Enum.zip(compiled, specs) do - assert Kernel.Typespec.to_binary({ { :spec, tuple }, [spec] }) == Macro.to_binary(definition) + lc { { { name, _ }, spec }, definition } inlist Enum.zip(compiled, specs) do + { args, result } = Kernel.Typespec.spec_to_ast(spec) + quoted = quote do: @spec unquote(name)(unquote_splicing(args)), do: unquote(result) + assert Macro.to_binary(quoted) == Macro.to_binary(definition) end end - # test/spec retrieval + # types/specs retrieval test "specs retrieval" do Code.compiler_options debug_info: true + { :module, T, binary, _ } = defmodule T do @spec a, do: any - def a, do: nil + def a, do: nil end - Code.compiler_options debug_info: false + :code.delete(T) :code.purge(T) - assert [{{:spec,{:a,_}},[{:type,_,:fun,[{:type,_,:product,[]},{:type,_,:any,[]}]}]}] = Kernel.Typespec.specs(binary) - assert [{{:spec,{:a,_}},[{:type,_,:fun,[{:type,_,:product,[]},{:type,_,:any,[]}]}]}] = Kernel.Typespec.specs(binary, :a, 0) + + assert [{{:a,_},[{:type,_,:fun,[{:type,_,:product,[]},{:type,_,:any,[]}]}]}] = Kernel.Typespec.beam_specs(binary) + after + Code.compiler_options debug_info: false end test "types retrieval" do Code.compiler_options debug_info: true + { :module, T, binary, _ } = defmodule T do @type a :: any @typep b :: any @@ -357,23 +347,15 @@ defmodule Typespec.Test.Type do @opaque c :: any end - assert [ - {:opaque, {:c,{:type,_,:any,[]},[]}}, - {:type, {:a,{:type,_,:any,[]},[]}}, - {:typep, {:b,{:type,_,:any,[]},[]}}, - ] = Kernel.Typespec.types(binary) - assert [ - {:type, {:a,{:type,_,:any,[]},[]}}, - ] = Kernel.Typespec.types(binary, :a, 0) - assert [ - {:typep, {:b,{:type,_,:any,[]},[]}}, - ] = Kernel.Typespec.types(binary, :b, 0) - assert [ - {:opaque, {:c,{:type,_,:any,[]},[]}}, - ] = Kernel.Typespec.types(binary, :c, 0) - after - Code.compiler_options debug_info: false :code.delete(T) :code.purge(T) + + assert [ + {:opaque, {:c,{:type,_,:any,[]},[]}}, + {:type, {:a,{:type,_,:any,[]},[]}}, + {:typep, {:b,{:type,_,:any,[]},[]}}, + ] = Kernel.Typespec.beam_types(binary) + after + Code.compiler_options debug_info: false end end \ No newline at end of file diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index b8027feba9..5d67187f8b 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -13,7 +13,8 @@ defmodule IEx.Helpers do * `c/2` - compiles a file in the given path * `h/0`,`h/1`, `h/2` - prints help/documentation - * `t/1`,`t/3` — prints type specification information + * `t/1` — prints types information + * `s/1`, `s/3` — prints specs information * `m/0` - prints loaded modules * `r/0` - recompiles and reloads the given module's source file * `v/0` - prints all commands and values @@ -214,69 +215,91 @@ defmodule IEx.Helpers do atom_to_binary(var) end - # FIXME: this can't be shown as h(t/1) @doc """ - Prints all types and function specifications from a given module + Prints all types for the given module ## Examples - t(Enum) - - """ - def __t_module__(module) do - types = - lc type inlist Kernel.Typespec.types(module) do - IO.puts Kernel.Typespec.to_binary(type) - type - end - specs = - lc spec inlist Kernel.Typespec.specs(module) do - IO.puts Kernel.Typespec.to_binary(spec) - spec - end - if specs == [] and types == [] do - IO.puts "No type specifications for #{inspect module} has been found" - end - :ok - end - - @doc """ - Prints the type specification for a given function or a type - - ## Examples - - t(Enum.all?/2) - t(Enum.t/0) + t(Enum) """ - def t(module, function, arity) do - types = - lc type inlist Kernel.Typespec.types(module, function, arity) do - IO.puts Kernel.Typespec.to_binary(type) - type + def t(module) do + types = lc type inlist Kernel.Typespec.beam_types(module), do: print_type(type) + + if types == [] do + IO.puts "No types for #{inspect module} has been found" end - specs = - lc spec inlist Kernel.Typespec.specs(module, function, arity) do - IO.puts Kernel.Typespec.to_binary(spec) - spec - end - if specs == [] and types == [] do - IO.puts "No type specification for #{inspect module}.#{function}/#{arity} has been found" - end + :ok end - defmacro t({ :/, _, [{ { :., _, [mod, fun] }, _, [] }, arity] }) do + @doc """ + Prints all specs from a given module. + + ## Examples + + s(Enum) + + """ + defmacro s({ :/, _, [{ { :., _, [mod, fun] }, _, [] }, arity] }) do quote do - t(unquote(mod), unquote(fun), unquote(arity)) - end - end - defmacro t(module) do - quote do - __t_module__(unquote(module)) + s(unquote(mod), unquote(fun), unquote(arity)) end end + defmacro s(module) do + quote do + s(unquote(module), :all) + end + end + + @doc false + def s(module, :all) do + specs = lc spec inlist Kernel.Typespec.beam_specs(module), do: print_spec(spec) + + if specs == [] do + IO.puts "No specs for #{inspect module} has been found" + end + + :ok + end + + @doc """ + Prints the specs for a given function. + + ## Examples + + s(Enum.all?/2) + s(Enum.t/0) + + """ + def s(module, function, arity) do + spec = List.keyfind(Kernel.Typespec.beam_specs(module), { function, arity }, 0) + + if spec do + print_spec(spec) + else + IO.puts "No spec for #{inspect module}.#{function}/#{arity} has been found" + end + + :ok + end + + defp print_type({ kind, type }) do + ast = Kernel.Typespec.type_to_ast(type) + IO.puts "@#{kind} #{Macro.to_binary(ast)}" + true + end + + defp print_spec({ { name, _arity }, specs }) do + Enum.each specs, fn(spec) -> + { args, result } = Kernel.Typespec.spec_to_ast(spec) + bin_args = Macro.to_binary { name, 0, args } + bin_result = Macro.to_binary result + IO.puts "@spec #{bin_args}, do: #{bin_result}" + end + true + end @doc """ Retrieves nth query's value from the history. Use negative