This dialyzer warning just happened randomly once I apply defmacro in my module.
I got 2 modules, the issues only appear in StoreFront module.
defmodule Store do
defmacro __using__(_opt) do
quote do
import unquote(__MODULE__)
@before_compile unquote(__MODULE__)
end
end
defmacro fruits(do: block) do
fn_name = String.to_atom("run_fruits")
quote do
@fn_names unquote(fn_name)
def unquote(fn_name)(), do: unquote(block)
end
end
defmacro apple(clause) do
quote do
if unquote(clause) in [true, :ok] do
:ok
else
:failure
end
end
end
defmacro __before_compile__(_env) do
quote do
def run() do
apply(__MODULE__, :run_fruits, [])
|> IO.inspect
end
end
end
end
defmodule StoreFront do
use Store
fruits do
apple true # emit warning
apple :ok # emit warning
apple false # emit warning
end
apple true # no warning
end
The complete dialyzer warning messages:
The pattern
'false' can never match the type
'true'ElixirLS Dialyzer
The test
'ok' =:=
'true' can never evaluate to 'true'
I think here dialyzer is detecting clauses that can never match (dead code). Dialyzer can see that some of the checks in the code are redundant. Your macros are probably generating a little bit more code that is really needed compared to if the code was written by hand (Dialyzer was designed for Erlang and I think Erlang only has simple substitution macros). That’s probably fine but I didn’t analyze your code too deeply.
“Expand” your macros, as well as elixirs stdlib macros until only defmodule and def are left in your StoreFront module. Then you will roughly see what dialyzer sees. That will help to understand the warnings.
Alright, I can refactor as functions and it doesn’t emit any dialyzer warning.
def run_fruits() do
apple(true)
apple(:ok)
apple(false)
end
def apple(value) do
if value in [true, :ok] do
:ok
else
:failure
end
end
def run() do
run_fruits()
|> IO.inspect()
end
Writing functions which do roughly what your macro does, is not writing the code the way it had been generated by the macro call.
So it becomes this:
defmodule StoreFront do
use Store # its okay to leave this as is, as it is not mentioned in the warnings
@fn_names :run_fruits
def run_fruits() do
case true === true or true === :ok do # apple true
x in [false, nil] -> :failure
_ -> :ok
end
case :ok === true or :ok === :ok do # apple :ok
x in [false, nil] -> :failure
_ -> :ok
end
case false === true or false === :ok do
x in [false, nil] -> :failure
_ -> :ok
end
end
# apple true # no need to expand this, dialyzer won't see it anyway, as it does not expand into a function
end
And dialyzer is wondering why you say or true === :ok, when you already know that true === true statically.
It asks why you have a x in [false, nil] clause when you already know in advance, that the condition will always be true.
To check what code you exactly end up with, you might want to use the following incancation by the way:
f = './_build/dev/lib/your_library_name/ebin/Elixir.YourModuleName.beam'
result = :beam_lib.chunks(f,[:abstract_code])
{:ok,{_,[{:abstract_code,{_,ac}}]}} = result
IO.puts :erl_prettypr.format(:erl_syntax.form_list(ac))
(where your_library_name and YourModuleName are replaced by the mix project and module name you’re working on, respectively)
This will show you the core Erlang that ends up being generated after all macros (both your macros and the built-in ones) are expanded. This is actually what Dialyzer is looking at.
(If someone knows a more concise or clean way to look at the core erlang of a module, I’d love to know, by he way!)
The pattern
'false' can never match the type
'true'ElixirLS Dialyzer
The test
'ok' =:=
'true' can never evaluate to 'true'
Ya you’re right, but you can do this on unit test like assert true and still works fine.
I just simple expression for this sample, in my case I have some complex calculations like apple Compute.diff_value(5, 2) < 7, this depends on the variable in runtime but it still emit warning even I have no idea how dialyzer able to check the return value before I put the parameters…
defmacro apple(clause) do
quote generated: true do
if unquote(clause) == :ok do
:ok
else
:failure
end
end
end
fruits do
apple :ok
apple :something_else
end
apple :ok
According to your method, it generate these lines:
-file("lib/store_front.ex", 1).
-module('Elixir.StoreFront').
-compile([no_auto_import]).
-export(['__info__'/1, run/0, run_fruits/0]).
-spec '__info__'(attributes | compile | functions |
macros | md5 | module | deprecated) -> any().
'__info__'(module) -> 'Elixir.StoreFront';
'__info__'(functions) -> [{run, 0}, {run_fruits, 0}];
'__info__'(macros) -> [];
'__info__'(Key = attributes) ->
erlang:get_module_info('Elixir.StoreFront', Key);
'__info__'(Key = compile) ->
erlang:get_module_info('Elixir.StoreFront', Key);
'__info__'(Key = md5) ->
erlang:get_module_info('Elixir.StoreFront', Key);
'__info__'(deprecated) -> [].
run() ->
'Elixir.IO':inspect(erlang:apply('Elixir.StoreFront',
run_fruits, [])).
run_fruits() ->
case ok == ok of
false -> failure;
true -> ok
end,
case something_else == ok of
false -> failure;
true -> ok
end.
:ok
I could see the root cause right now, however I’m wondering how other libraries e.g. ExUnit didn’t emit the same warning.
Here the problem is no matter things I put next to apple macro under fruits, it emits warning messsages.
Sorry to bump an old topic, but wow, this comment is gold!! It helped me figure out a mind-bending dialyzer issue related to nesting a case statement inside the else of a with. Thanks @Qqwy I am going to keep this in my toolbelt from now to help understand Elixir’s dark corners and edge cases.