Elixir: Unexpected type error

Created on 17 Apr 2020  路  7Comments  路  Source: elixir-lang/elixir

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

Most helpful comment

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:

  1. 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

  2. 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.

All 7 comments

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:

  1. 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

  2. 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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ckampfe picture ckampfe  路  3Comments

LucianaMarques picture LucianaMarques  路  3Comments

andrewcottage picture andrewcottage  路  3Comments

chulkilee picture chulkilee  路  3Comments

Paddy3118 picture Paddy3118  路  3Comments