Dialyzer complains about invalid type spec - but the struct seems correct

:wave:

Hello everyone,

we have a dialyzer typespec error over at benchee that I can’t wrap my head around. It only seems to fail on Erlang 19+ and the issues seems to be with a struct type that we provide (typespec says we take that type and dialyzer seems to understand but still complains when it’s there).

The weirdest thing is that we have what I’d call virtually identical case that passes just fine (see below).

This includes code samples, for the full code you can check out the benchee PR trying to introduce the type spec

Basically it is the following (the only place where the offending function format_scenario_extended/3 is called):

  @spec extended_statistics([Scenario.t], unit_per_statistic, integer)
    :: [String.t]
  defp extended_statistics(scenarios, units, label_width) do
    Enum.map(scenarios, fn(scenario) ->
      format_scenario_extended(scenario, units, label_width)
    end)
  end
  
  # problem seems to be Scenario.t, pattern match on Scenario omitted for brevity
  @spec format_scenario_extended(Scenario.t, unit_per_statistic, integer)
    :: String.t
  defp format_scenario_extended(%Scenario{}, %{run_time: run_time_unit}, label_width) do
    "~*s~*ts~*ts~*ts~*ts\n"
    |> :io_lib.format([lots_of_options])
    |> to_string
  end

We get the following error:

lib/benchee/formatters/console.ex:173: Invalid type specification for function 'Elixir.Benchee.Formatters.Console':format_scenario_extended/3. The success typing is (#{'__struct__':='Elixir.Benchee.Benchmark.Scenario', 'job_name':=_, 'run_time_statistics':=#{'__struct__':='Elixir.Benchee.Statistics', 'maximum':=number(), 'minimum':=number(), 'mode':=[number()], 'sample_size':=_, _=>_}, _=>_},#{'ips':=#{'__struct__':='Elixir.Benchee.Conversion.Unit', 'label':=binary(), 'long':=binary(), 'magnitude':=non_neg_integer(), 'name':=atom()}, 'run_time':=#{'__struct__':='Elixir.Benchee.Conversion.Unit', 'label':=binary(), 'long':=binary(), 'magnitude':=non_neg_integer(), 'name':=atom()}},integer()) -> binary(

The problem seems to be with dialyzers interpretation what Scenario.t is. If it is exchanged against any or map for format_scenario_extended/3 dialyzer instead complains about underspecs:

lib/benchee/formatters/console.ex:173: Type specification 'Elixir.Benchee.Formatters.Console':format_scenario_extended(map(),unit_per_statistic(),integer()) -> 'Elixir.String':t() is a supertype of the success typing: 'Elixir.Benchee.Formatters.Console':format_scenario_extended(#{'__struct__':='Elixir.Benchee.Benchmark.Scenario', 'job_name':=_, 'run_time_statistics':=#{'__struct__':='Elixir.Benchee.Statistics', 'maximum':=number(), 'minimum':=number(), 'mode':=[number()], 'sample_size':=_, _=>_}, _=>_},#{'ips':=#{'__struct__':='Elixir.Benchee.Conversion.Unit', 'label':=binary(), 'long':=binary(), 'magnitude':=non_neg_integer(), 'name':=atom()}, 'run_time':=#{'__struct__':='Elixir.Benchee.Conversion.Unit', 'label':=binary(), 'long':=binary(), 'magnitude':=non_neg_integer(), 'name':=atom()}},integer()) -> binary()

One of the weirdest things about this is that we have virtually the same structure elsewhere in the same file and it works just fine:

  @spec scenario_reports([Scenario.t], unit_per_statistic, integer)
    :: [String.t]
  defp scenario_reports(scenarios, units, label_width) do
    Enum.map(scenarios, fn(scenario) ->
      format_scenario(scenario, units, label_width)
    end)
  end

  # pattern match on scenario omitted for brevity
  @spec format_scenario(Scenario.t, unit_per_statistic, integer) :: String.t
  defp format_scenario(%Scenario{},
                       %{run_time: run_time_unit,
                         ips:      ips_unit,
                       }, label_width) do
    "~*s~*ts~*ts~*ts~*ts~*ts\n"
    |> :io_lib.format([options])
    |> to_string
  end

So umm help - help is rewarded with gratefulness and cute bunny pictures (:rabbit:) Bonus points if you can lay out how to debug something like this :slight_smile:

Thanks! :tada:
Tobi

Dialyzer says the code will work only if mode is a list of numbers (“success typing is bla-bla-bla”). Indeed, the function calls mode_out, whose spec incorrectly says it only accepts a list:

But, the type spec here says mode is a number, not a list:

3 Likes

Thanks a ton for taking the time and helping out :green_heart:

That was indeed a bug in the type specs! How’d you figure the bug was with mode - did you just look at all the fields of scenario and cross checked or is there something else you did?

thanks a ton, works locally - CI is currently running but I’m hopeful that it’ll also pass!

Yep, I cross-checked the fields. Dialyzer errors look scarier than they are because of the lack of formatting, it would be a nice help if we had messages in a friendly Elixir format someday.

I think the Dialyzer in Erlang 18 had really basic support for maps so it would not catch that kind of problem.

1 Like

I can live with the formatting, my problem is just more that it goes"

Here is what I’d expected it to be …4 lines of stuff… but something in there was different, go figure which one I mean"

if there was some sort of diff that’d be helpful (like exunit tests have) - so that it could go in this case:

hey I determined mode as [number] but your typespec over at Statistics.t says it’s number, please help"

(In more computer terms but I think you know what I mean) that’d be like 5 times as helpful :grin:

Anyhow, that’s more for erlang core :smile:

Thanks a ton!

That would be very nice, yes. OTOH, I tend to interpret huge messages from dialyzer (especially with nesting) as a sign that my structs are getting too complicated. This is a good counterpoint: http://prog21.dadgum.com/17.html

The formatter you’re referring to exists as of ~6 months ago in Dialyxir via the Erlex project. Re: diffs, dialyzer’s output drops fields from the expected/actual types when there’s a very large struct, for example, and replaces with ... => ..., so a diff is impossible in many cases, unfortunately.

Edit: oops, just realized this was a super old thread. Came up in a search and I misread the date.

1 Like