In some cases Code.eval_quoted causes unexpected and weird Dialyzer errors.
It might be caused by bug in Erlang, Elixir or Dialyzer itself.
But it's definitely indicates what something is wrong somewhere.
Sample project and details how to reproduce bug are there
https://github.com/tim2CF/dialyzer_bug#dialyzerbug
I was able to further reduce the issue by breaking apart the eval_quoted call into its private calls.
def hello do
x =
quote do
Map.new
end
{%{}, _} = eval_forms(x)
end
defp eval_forms(x) do
{example, _, _} = :elixir.quoted_to_erl(x, %{__ENV__ | tracers: []})
case example do
{:atom, _, atom} ->
{atom, []}
_ ->
{:value, expr, binding} = :erl_eval.expr(example, [])
{expr, binding}
end
end
Which emits these warnings:
lib/dialyzer_bug.ex:15:no_return
Function hello/0 has no local return.
________________________________________________________________________________
lib/dialyzer_bug.ex:21:pattern_match
The pattern can never match the type.
Pattern:
{%{}, _}
Type:
{atom(), []}
________________________________________________________________________________
lib/dialyzer_bug.ex:32:call_without_opaque
Function call without opaqueness type mismatch.
Call does not have expected term of type
{nil, :erl_anno.anno()}
| {atom(), :erl_anno.anno(), _}
| {:bc
| :call
| :case
| :cons
| :lc
| :map
| :match
| :named_fun
| :op
| :record
| :record_index, :erl_anno.anno(), _, _}
| {:op, :erl_anno.anno(), atom(), _, _}
| {:receive, :erl_anno.anno(),
[
{:clause, :erl_anno.anno(),
[
{nil, :erl_anno.anno()}
| {:atom | :bin | :char | :float | :integer | :map | :string | :tuple | :var,
:erl_anno.anno(), atom() | [any()] | number()}
| {:cons, :erl_anno.anno(), _, _}
| {:match, :erl_anno.anno(), _, _}
| {:op, :erl_anno.anno(), :+ | :- | :bnot | :not, _}
| {:record, :erl_anno.anno(), atom(), [any()]}
| {:record_index, :erl_anno.anno(), atom(), {_, _, _}}
| {:op, :erl_anno.anno(), atom(), _, _}
],
[
[
{nil, :erl_anno.anno()}
| {:atom | :bin | :char | :float | :integer | :map | :string | :tuple | :var,
:erl_anno.anno(), atom() | [any()] | number()}
| {:call | :cons | :map | :op | :record | :record_index, :erl_anno.anno(), _, _}
| {:op, :erl_anno.anno(), atom(), _, _}
| {:record_field, :erl_anno.anno(), _, atom(), {_, _, _}},
...
]
], [any(), ...]},
...
], _, [any(), ...]}
| {:record, :erl_anno.anno(), _, atom(),
[{:record_field, :erl_anno.anno(), {:atom, :erl_anno.anno(), atom()}, _}]}
| {:record_field, :erl_anno.anno(), _, atom(), {:atom, :erl_anno.anno(), atom()}}
| {:try, :erl_anno.anno(), [any()],
[
{:clause, :erl_anno.anno(),
[
{nil, :erl_anno.anno()}
| {:atom | :bin | :char | :float | :integer | :map | :string | :tuple | :var,
:erl_anno.anno(), atom() | [any()] | number()}
| {:cons, :erl_anno.anno(), _, _}
| {:match, :erl_anno.anno(), _, _}
| {:op, :erl_anno.anno(), :+ | :- | :bnot | :not, _}
| {:record, :erl_anno.anno(), atom(), [any()]}
| {:record_index, :erl_anno.anno(), atom(), {_, _, _}}
| {:op, :erl_anno.anno(), atom(), _, _}
],
[
[
{nil, :erl_anno.anno()}
| {:atom | :bin | :char | :float | :integer | :map | :string | :tuple | :var,
:erl_anno.anno(), atom() | [any()] | number()}
| {:call | :cons | :map | :op | :record | :record_index, :erl_anno.anno(), _, _}
| {:op, :erl_anno.anno(), atom(), _, _}
| {:record_field, :erl_anno.anno(), _, atom(), {_, _, _}},
...
]
], [any(), ...]}
],
[
{:clause, :erl_anno.anno(),
[
{nil, :erl_anno.anno()}
| {:atom | :bin | :char | :float | :integer | :map | :string | :tuple | :var,
:erl_anno.anno(), atom() | [any()] | number()}
| {:cons, :erl_anno.anno(), _, _}
| {:match, :erl_anno.anno(), _, _}
| {:op, :erl_anno.anno(), :+ | :- | :bnot | :not, _}
| {:record, :erl_anno.anno(), atom(), [any()]}
| {:record_index, :erl_anno.anno(), atom(), {_, _, _}}
| {:op, :erl_anno.anno(), atom(), _, _}
],
[
[
{nil, :erl_anno.anno()}
| {:atom | :bin | :char | :float | :integer | :map | :string | :tuple | :var,
:erl_anno.anno(), atom() | [any()] | number()}
| {:call | :cons | :map | :op | :record | :record_index, :erl_anno.anno(), _, _}
| {:op, :erl_anno.anno(), atom(), _, _}
| {:record_field, :erl_anno.anno(), _, atom(), {_, _, _}},
...
]
], [any(), ...]}
], [any()]}
(with opaque subterms) in the 1st position.
:erl_eval.expr(
_example ::
{nil, 0}
| {:bin, 0, [any()]}
| {:float, 0, float()}
| {:integer, 0, integer()}
| {:tuple, 0, [any()]}
| {:call, 0, {_, _, _, _}, [any(), ...]}
| {:cons, 0, {_, _} | {_, _, _} | {_, _, _, _}, {_, _} | {_, _, _} | {_, _, _, _}},
[]
)
There are a couple issues here:
erl_anno:anno is listed as opaque in Erlang but I really don't think it should - people have been building Erlang ASTs by hand for quite some time and the documentation still refers to LINE directly
Dialyzer for some reason does not consider :bin nodes to be a valid input to erl_eval expr, even though they are explicitly listed at the root: https://github.com/erlang/otp/blame/ec9151458d0d425011b3ee2cb8aed7796b6bdbc8/lib/stdlib/src/erl_parse.yrl#L697 - this leads Dialyzer to think the second case clause will fail and says only the first clause is valid output?
Given I have already spent 2h+ in this and I don't have any evidence this is an issue specific to Elixir at all, I will go ahead and close this. I guess this is the issue with Dialyzer in a nutshell and it reminds me why we generally don't debug Dialyzer errors in the first place: the whole process is a time sink and often you don't come out any wiser. Even if it turns out to be a faulty assumption on Elixir side somehow, I doubt it is worth the time investment.
In any case, thanks for the report.
@josevalim I think 2. may not be an issue and is caused by 1. but I'm not sure
As to erl_anno:anno being opaque, as long as erl_anno allows to create the type this should be fine? I can see at least one spot where elixir passes an integer instead of using erl_anno:new here - I tried replacing Line with erl_anno:new(Line) but this didn't compile, I don't know erlang well enough to understand why
edit: it may work, I have other elixir compilation errors
The problem is that the docs do not say we need to use erl_anno and reference LINE directly. I have opened up an issue. It is also not possible for 2 to be caused by 1, because 1 is the output of the call site of 2. But I will be very glad to be proven wrong. :)
Makes sense, thanks!
Maybe I couldn't get it to compile because elixir master cannot bootstrap itself with erlang 23-rc3? I'll switch to stable erlang and try to fix a few of the ~180 dialyzer warnings/errors that show when analyzing the elixir application.
edit: I had a bad asdf-elixir setup, elixir should be able to compile itself just fine
related erlang issue for reference: https://bugs.erlang.org/browse/ERL-1228
@tim2CF Can you confirm this is fixed in your project on master? Seems to be fixed for the example you linked. Thanks!
Seems like you fixed it! Thanks for investigation and fix!
Most helpful comment
I was able to further reduce the issue by breaking apart the eval_quoted call into its private calls.
Which emits these warnings:
There are a couple issues here:
erl_anno:anno is listed as opaque in Erlang but I really don't think it should - people have been building Erlang ASTs by hand for quite some time and the documentation still refers to
LINEdirectlyDialyzer for some reason does not consider
:binnodes to be a valid input to erl_eval expr, even though they are explicitly listed at the root: https://github.com/erlang/otp/blame/ec9151458d0d425011b3ee2cb8aed7796b6bdbc8/lib/stdlib/src/erl_parse.yrl#L697 - this leads Dialyzer to think the secondcaseclause will fail and says only the first clause is valid output?Given I have already spent 2h+ in this and I don't have any evidence this is an issue specific to Elixir at all, I will go ahead and close this. I guess this is the issue with Dialyzer in a nutshell and it reminds me why we generally don't debug Dialyzer errors in the first place: the whole process is a time sink and often you don't come out any wiser. Even if it turns out to be a faulty assumption on Elixir side somehow, I doubt it is worth the time investment.
In any case, thanks for the report.