diff --git a/examples/typed_gen_server/lib/typed_gen_server/stage1.ex b/examples/typed_gen_server/lib/typed_gen_server/stage1.ex index 0846ee92..4e6a41af 100644 --- a/examples/typed_gen_server/lib/typed_gen_server/stage1.ex +++ b/examples/typed_gen_server/lib/typed_gen_server/stage1.ex @@ -1,17 +1,20 @@ defmodule TypedGenServer.Stage1.Server do - use GenServer - use GradualizerEx.TypeAnnotation + # use GenServer + use Gradient.TypeAnnotation ## Start IEx with: ## iex -S mix run --no-start ## + ## Start Gradient: + ## Application.ensure_all_started(:gradient) + ## ## Then use the following to recheck the file on any change: - ## recompile(); GradualizerEx.type_check_file(:code.which( TypedGenServer.Stage1.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( TypedGenServer.Stage1.Server ), [:infer]) ## Try switching between the definitions and see what happens @type message :: Contract.Echo.req() | Contract.Hello.req() - #@type message :: Contract.Echo.req() - #@type message :: {:echo_req, String.t()} | {:hello, String.t()} + # @type message :: Contract.Echo.req() + # @type message :: {:echo_req, String.t()} | {:hello, String.t()} @type state :: map() @@ -20,19 +23,18 @@ defmodule TypedGenServer.Stage1.Server do end @spec echo(pid(), String.t()) :: String.t() - # @spec echo(pid(), String.t()) :: {:echo_req, String.t()} def echo(pid, message) do - case annotate_type( GenServer.call(pid, {:echo_req, message}), Contract.Echo.res() ) do - #case call_echo(pid, message) do + case annotate_type(GenServer.call(pid, {:echo_req, message}), Contract.Echo.res()) do + # case call_echo(pid, message) do ## Try changing the pattern or the returned response {:echo_res, response} -> response end end - #@spec call_echo(pid(), String.t()) :: Contract.Echo.res() - #defp call_echo(pid, message) do + # @spec call_echo(pid(), String.t()) :: Contract.Echo.res() + # defp call_echo(pid, message) do # GenServer.call(pid, {:echo_req, message}) - #end + # end @spec hello(pid(), String.t()) :: :ok def hello(pid, name) do @@ -41,27 +43,25 @@ defmodule TypedGenServer.Stage1.Server do end end - @impl true + # @impl true def init(state) do {:ok, state} end - @impl true - def handle_call(m, from, state) do - {:noreply, handle(m, from, state)} - end + @type called(a) :: {:noreply, state()} + | {:reply, a, state()} - @spec handle(message(), any, any) :: state() + # @impl true + @spec handle_call(message(), GenServer.from(), state()) + :: called(Contract.Echo.res() | Contract.Hello.res()) ## Try breaking the pattern match, e.g. by changing 'echo_req' - def handle({:echo_req, payload}, from, state) do - GenServer.reply(from, {:echo_res, payload}) - state + def handle_call({:echo_req, payload}, _from, state) do + {:reply, {:echo_res, payload}, state} end ## Try commenting out the following clause - def handle({:hello, name}, from, state) do + def handle_call({:hello, name}, _from, state) do IO.puts("Hello, #{name}!") - GenServer.reply(from, :ok) - state + {:reply, :ok, state} end end diff --git a/examples/typed_gen_server/lib/typed_gen_server/stage2.ex b/examples/typed_gen_server/lib/typed_gen_server/stage2.ex index fe122a1e..f90b7b81 100644 --- a/examples/typed_gen_server/lib/typed_gen_server/stage2.ex +++ b/examples/typed_gen_server/lib/typed_gen_server/stage2.ex @@ -1,20 +1,23 @@ defmodule TypedGenServer.Stage2.Server do - use GenServer - use GradualizerEx.TypeAnnotation + # use GenServer + use Gradient.TypeAnnotation alias Stage2.TypedServer ## Start IEx with: ## iex -S mix run --no-start ## + ## Start Gradient: + ## Application.ensure_all_started(:gradient) + ## ## Then use the following to recheck the file on any change: - ## recompile(); GradualizerEx.type_check_file(:code.which( TypedGenServer.Stage2.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( TypedGenServer.Stage2.Server ), [:infer]) @opaque t :: pid() ## Try switching between the definitions and see what happens @type message :: Contract.Echo.req() | Contract.Hello.req() - #@type message :: Contract.Echo.req() - #@type message :: {:echo_req, String.t()} | {:hello, String.t()} + # @type message :: Contract.Echo.req() + # @type message :: {:echo_req, String.t()} | {:hello, String.t()} @type state :: map() @@ -24,10 +27,9 @@ defmodule TypedGenServer.Stage2.Server do end @spec echo(t(), String.t()) :: String.t() - # @spec echo(t(), String.t()) :: {:echo_req, String.t()} def echo(pid, message) do - case annotate_type( GenServer.call(pid, {:echo_req, message}), Contract.Echo.res() ) do - #case call_echo(pid, message) do + case annotate_type(GenServer.call(pid, {:echo_req, message}), Contract.Echo.res()) do + # case call_echo(pid, message) do ## Try changing the pattern or the returned response {:echo_res, response} -> response end @@ -46,12 +48,12 @@ defmodule TypedGenServer.Stage2.Server do end end - @impl true + # @impl true def init(state) do {:ok, state} end - @impl true + # @impl true def handle_call(m, from, state) do {:noreply, handle(m, from, state)} end @@ -62,8 +64,8 @@ defmodule TypedGenServer.Stage2.Server do ## This could register {:echo_req, payload} <-> {:echo_res, payload} mapping ## and response type at compile time to generate call_echo() automatically. ## Thanks Robert! - #TypedServer.reply( from, {:echo_res, payload}, Contract.Echo.res() ) - GenServer.reply( from, {:echo_res, payload} ) + # TypedServer.reply( from, {:echo_res, payload}, Contract.Echo.res() ) + GenServer.reply(from, {:echo_res, payload}) state end @@ -78,15 +80,26 @@ end defmodule Test.TypedGenServer.Stage2.Server do alias TypedGenServer.Stage2.Server + ## Run with: + ## recompile(); Test.TypedGenServer.Stage2.Server.test() + ## ## Typecheck with: - ## recompile(); GradualizerEx.type_check_file(:code.which( Test.TypedGenServer.Stage2.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( Test.TypedGenServer.Stage2.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( Test.TypedGenServer.Stage2.Server ), [:infer, ex_check: false]) @spec test :: any() def test do {:ok, srv} = Server.start_link() - pid = self() + + pid = + spawn(fn -> + receive do + :unlikely -> :ok + end + end) + "payload" = Server.echo(srv, "payload") ## This won't typecheck, since Server.echo only accepts Server.t(), that is our Server pids - #"payload" = Server.echo(pid, "payload") + # "payload" = Server.echo(pid, "payload") end end diff --git a/examples/typed_gen_server/lib/typed_gen_server/stage3.ex b/examples/typed_gen_server/lib/typed_gen_server/stage3.ex index 02023c74..5420af98 100644 --- a/examples/typed_gen_server/lib/typed_gen_server/stage3.ex +++ b/examples/typed_gen_server/lib/typed_gen_server/stage3.ex @@ -1,5 +1,4 @@ defmodule Stage3.TypedServer do - ## This doesn't play well with: ## {:ok, srv} = MultiServer.start_link() ## Due to: @@ -18,21 +17,21 @@ end defmodule TypedGenServer.Stage3.Server do use GenServer - use GradualizerEx.TypeAnnotation + use Gradient.TypeAnnotation alias Stage3.TypedServer ## Start IEx with: ## iex -S mix run --no-start ## ## Then use the following to recheck the file on any change: - ## recompile(); GradualizerEx.type_check_file(:code.which( TypedGenServer.Stage3.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( TypedGenServer.Stage3.Server ), [:infer]) @opaque t :: {__MODULE__, pid()} ## Try switching between the definitions and see what happens @type message :: Contract.Echo.req() | Contract.Hello.req() - #@type message :: Contract.Echo.req() - #@type message :: {:echo_req, String.t()} | {:hello, String.t()} + # @type message :: Contract.Echo.req() + # @type message :: {:echo_req, String.t()} | {:hello, String.t()} @type state :: map() @@ -44,17 +43,17 @@ defmodule TypedGenServer.Stage3.Server do @spec echo(t(), String.t()) :: String.t() # @spec echo(t(), String.t()) :: {:echo_req, String.t()} def echo(_server = {__MODULE__, _pid}, message) do - case annotate_type( GenServer.call(_pid, {:echo_req, message}), Contract.Echo.res() ) do - #case call_echo(_server, message) do + case annotate_type(GenServer.call(_pid, {:echo_req, message}), Contract.Echo.res()) do + # case call_echo(_server, message) do ## Try changing the pattern or the returned response {:echo_res, response} -> response end end - #@spec call_echo(t(), String.t()) :: Contract.Echo.res() - #defp call_echo({__MODULE__, pid}, message) do + # @spec call_echo(t(), String.t()) :: Contract.Echo.res() + # defp call_echo({__MODULE__, pid}, message) do # GenServer.call(pid, {:echo_req, message}) - #end + # end @spec hello(t(), String.t()) :: :ok def hello({__MODULE__, pid}, name) do @@ -92,7 +91,7 @@ defmodule Test.TypedGenServer.Stage3.Server do alias TypedGenServer.Stage3.Server ## Typecheck with: - ## recompile(); GradualizerEx.type_check_file(:code.which( Test.TypedGenServer.Stage3.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( Test.TypedGenServer.Stage3.Server ), [:infer]) @spec test :: any() def test do @@ -100,6 +99,6 @@ defmodule Test.TypedGenServer.Stage3.Server do pid = self() "payload" = Server.echo(srv, "payload") ## This won't typecheck, since Server.echo only accepts Server.t(), that is Server pids - #"payload" = Server.echo(pid, "payload") + # "payload" = Server.echo(pid, "payload") end end diff --git a/examples/typed_gen_server/lib/typed_gen_server/stage4.ex b/examples/typed_gen_server/lib/typed_gen_server/stage4.ex index 2ed12d42..baa56812 100644 --- a/examples/typed_gen_server/lib/typed_gen_server/stage4.ex +++ b/examples/typed_gen_server/lib/typed_gen_server/stage4.ex @@ -1,5 +1,5 @@ defmodule TypedGenServer.Stage4.Server do - use GenServer + # use GenServer use Gradient.TypeAnnotation use Gradient.TypedServer alias Gradient.TypedServer @@ -7,8 +7,12 @@ defmodule TypedGenServer.Stage4.Server do ## Start IEx with: ## iex -S mix run --no-start ## + ## Start Gradient: + ## Application.ensure_all_started(:gradient) + ## ## Then use the following to recheck the file on any change: ## recompile(); Gradient.type_check_file(:code.which( TypedGenServer.Stage4.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( TypedGenServer.Stage4.Server ), [:infer, ex_check: false]) @opaque t :: pid() @@ -27,7 +31,7 @@ defmodule TypedGenServer.Stage4.Server do @spec echo(t(), String.t()) :: String.t() # @spec echo(t(), String.t()) :: {:echo_req, String.t()} def echo(pid, message) do - #case annotate_type(GenServer.call(pid, {:echo_req, message}), Contract.Echo.res()) do + # case annotate_type(GenServer.call(pid, {:echo_req, message}), Contract.Echo.res()) do case call_echo_req(pid, message) do ## Try changing the pattern or the returned response {:echo_res, response} -> response @@ -38,22 +42,22 @@ defmodule TypedGenServer.Stage4.Server do ## thanks to using TypedServer.reply/3 instead of GenServer.reply/2. ## We don't have to define it! ## TODO: use the correct type instead of any as the second param! - #@spec call_echo_req(t(), any) :: Contract.Echo.res() - #defp call_echo_req(pid, message) do + # @spec call_echo_req(t(), any) :: Contract.Echo.res() + # defp call_echo_req(pid, message) do # GenServer.call(pid, {:echo_req, message}) - #end + # end @spec hello(t(), String.t()) :: :ok def hello(pid, name) do GenServer.call(pid, {:hello, name}) end - @impl true + # @impl true def init(state) do {:ok, state} end - @impl true + # @impl true def handle_call(m, from, state) do {:noreply, handle(m, from, state)} end @@ -64,11 +68,11 @@ defmodule TypedGenServer.Stage4.Server do ## TypedServer.reply/3 registers a {:echo_req, payload} <-> Contract.Echo.res() mapping ## and generates call_echo_req() at compile time. ## Thanks for the idea, @rvirding! - TypedServer.reply( from, {:echo_res, payload}, Contract.Echo.res() ) + TypedServer.reply(from, {:echo_res, payload}, Contract.Echo.res()) ## This will not typecheck - awesome! - #TypedServer.reply( from, {:invalid_tag, payload}, Contract.Echo.res() ) + # TypedServer.reply( from, {:invalid_tag, payload}, Contract.Echo.res() ) ## And this is the well known untyped equivalent. - #GenServer.reply(from, {:echo_res, payload}) + # GenServer.reply(from, {:echo_res, payload}) state end @@ -85,13 +89,21 @@ defmodule Test.TypedGenServer.Stage4.Server do ## Typecheck with: ## recompile(); Gradient.type_check_file(:code.which( Test.TypedGenServer.Stage4.Server ), [:infer]) + ## recompile(); Gradient.type_check_file(:code.which( Test.TypedGenServer.Stage4.Server ), [:infer, ex_check: false]) @spec test :: any() def test do {:ok, srv} = Server.start_link() - pid = self() + + pid = + spawn(fn -> + receive do + :unlikely -> :ok + end + end) + "payload" = Server.echo(srv, "payload") ## This won't typecheck, since Server.echo only accepts Server.t(), that is our Server pids - #"payload" = Server.echo(pid, "payload") + # "payload" = Server.echo(pid, "payload") end end diff --git a/examples/typed_gen_server/mix.exs b/examples/typed_gen_server/mix.exs index fd9946b4..5c00fe3f 100644 --- a/examples/typed_gen_server/mix.exs +++ b/examples/typed_gen_server/mix.exs @@ -26,15 +26,14 @@ defmodule TypedGenServer.MixProject do {:gradient, path: "../../"}, {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, {:gradualizer, - github: "erszcz/Gradualizer", ref: "typed-gen-server", manager: :rebar3, override: true}, - {:gradualizer_ex, github: "erszcz/gradualizer-ex", branch: "rs/wip"} + github: "erszcz/Gradualizer", ref: "typed-gen-server", manager: :rebar3, override: true} ] end defp dialyzer do [ plt_add_deps: :app_tree, - #ignore_warnings: "dialyzer.ignore-warnings", + # ignore_warnings: "dialyzer.ignore-warnings", flags: ~w( error_handling race_conditions @@ -43,5 +42,4 @@ defmodule TypedGenServer.MixProject do )a ] end - end diff --git a/examples/typed_gen_server/mix.lock b/examples/typed_gen_server/mix.lock index 846fa08c..793b743f 100644 --- a/examples/typed_gen_server/mix.lock +++ b/examples/typed_gen_server/mix.lock @@ -1,6 +1,5 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "gradualizer": {:git, "https://github.com/erszcz/Gradualizer.git", "bd54184dae19d5117534c6e6885fa5abd1fe5da4", [ref: "typed-gen-server"]}, - "gradualizer_ex": {:git, "https://github.com/erszcz/gradualizer-ex.git", "dc64f484c83a5ce286696fb79756577a1b9853f5", [branch: "rs/wip"]}, + "gradualizer": {:git, "https://github.com/erszcz/Gradualizer.git", "764ddc6b4cc007008ea68eb9ec7a3f3adc2f1951", [ref: "typed-gen-server"]}, } diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex index 0be7abe7..00ebdac0 100644 --- a/lib/gradient/elixir_fmt.ex +++ b/lib/gradient/elixir_fmt.ex @@ -69,6 +69,39 @@ defmodule Gradient.ElixirFmt do format_expr_type_error(expression, actual_type, expected_type, opts) end + def format_type_error({:nonexhaustive, anno, example}, opts) do + formatted_example = + case example do + [x | xs] -> + :lists.foldl( + fn a, acc -> + [pp_expr(a, opts), "\n\t" | acc] + end, + [pp_expr(x, opts)], + xs + ) + |> Enum.reverse() + + x -> + pp_expr(x, opts) + end + + :io_lib.format( + "~sNonexhaustive patterns~s~s", + [ + format_location(anno, :brief, opts), + format_location(anno, :verbose, opts), + case :proplists.get_value(:fmt_location, opts, :verbose) do + :brief -> + :io_lib.format(": ~s\n", formatted_example) + + :verbose -> + :io_lib.format("\nExample values which are not covered:~n\t~s~n", [formatted_example]) + end + ] + ) + end + def format_type_error( {:spec_error, :wrong_spec_name, anno, name, arity}, opts diff --git a/lib/gradient/type_annotation.ex b/lib/gradient/type_annotation.ex index 9028adc8..57d97a69 100644 --- a/lib/gradient/type_annotation.ex +++ b/lib/gradient/type_annotation.ex @@ -8,7 +8,7 @@ defmodule Gradient.TypeAnnotation do defp annotate(type_op, expr, type) do erlang_type = elixir_type_to_erlang(type) # IO.inspect(erlang_type, label: "erlang type") - {type_op, [], [expr, Macro.to_string(erlang_type)]} + {type_op, [], [expr, erlang_type]} # |> IO.inspect(label: "annotation node") end diff --git a/lib/gradient/typed_server/compile_hooks.ex b/lib/gradient/typed_server/compile_hooks.ex index 0cddbe1b..d2415f27 100644 --- a/lib/gradient/typed_server/compile_hooks.ex +++ b/lib/gradient/typed_server/compile_hooks.ex @@ -17,11 +17,6 @@ defmodule Gradient.TypedServer.CompileHooks do end def __on_definition__(env, _kind, name, args, _guards, body) do - if name == :handle do - # IO.inspect({name, env}, limit: :infinity) - IO.inspect({env.module, Module.get_attribute(env.module, :spec)}) - end - request_handler = Module.get_attribute(env.module, :request_handler, :handle) case request_handler do