Erlang’s Halloween 👻

Some dead code that not even Dialyzer can find

Brujo Benavides
Erlang Battleground
4 min readOct 26, 2021

--

It’s relatively unusual to watch me talking about the shortcomings or limitations of such an excellent tool as Dialyzer. Still, today I found some very dead code blocks that slipped through its world-famous success type analysis.

So, since Halloween is close, let’s raise some dead code from its grave!

Zombie Programmer
By Zombie Programmer — A great blog. Check it out!

When Dialyzer produces a Warning

So, in the past, I’ve seen code like this one…

warn(X) ->
{just, X};
warn(6) ->
{six, 6}.

Trigger warnings from both the compiler and Dialyzer like the following ones…

===> Compiling my_app
src/el.erl:8: Warning: this clause for warn/1 cannot match because a previous clause at line 6 always matches
===> Dialyzer starting, this may take a while...
…
src/el.erl
Line 8: The pattern 6 can never match since previous clauses completely covered the type any()

Lovely! The second clause in that function is clearly dead code (i.e., it will never be evaluated) since 6 matches X and thus falls through the first clause when provided as input. This is one of my favorite Dialyzer warnings ever. It’s clear and actionable. It provides a proper context with line numbers and what-not. It’s awesome.

When Dialyzer doesn’t produce a Warning

But… Today I found a piece of dead code that was very, very similar and yet… Dialyzer and the Compiler were both utterly silent about it. Let me show you…

warn(X, X) ->
{just, X};
warn(6, 6) ->
{six, 6}.

Have you spotted the difference?
Let me show you yet another example…

warn(X, Y) ->
{just, X and Y};
warn(6, 6) ->
{six, 6}.

This one does produce the expected warnings…

===> Compiling my_app
src/el.erl:8: Warning: this clause for warn/2 cannot match because a previous clause at line 6 always matches
===> Dialyzer starting, this may take a while...
…
src/el.erl
Line 8: The pattern <6, 6> can never match since previous clauses completely covered the type <_,_>

What’s Going on Here?

Well… It seems that not every clause that cannot match can be detected by either the compiler or Dialyzer. Sadly, those that include bounded variables (like the second X in warn/2) or guards or any other semantic constructs are outside the scope of these detection tools.

So, if you have functions like that, the compiler and Dialyzer won’t detect your dead code, and it may end up living as a ghost for a long, long time.

Well, in that case, who you’re gonna call?

No, not the Ghost Busters

Test Coverage, of course!

If you have a good test coverage policy in place (maybe one like the Inakos had where we demanded 100% coverage everywhere, perhaps a lighter one), you should immediately see that no test can ever cover those clauses. Then, you can remove them.

As usual, if you like my blog, you can buy me a coffee and if you want to participate, we’re always looking for new writers for Erlang Battleground. Hit me up on Twitter, Facebook, Slack, etc.…

Bonus Track

While writing this article, I checked several other scenarios to see what was detectable and what was not. You can see the module I used for that below…

And these are the only warnings that I get with it…

===> Analyzing applications...
…
===> Compiling lapp
src/el.erl:8: Warning: this clause for warn/2 cannot match because a previous clause at line 6 always matches
src/el.erl:19: Warning: this clause cannot match because a previous clause at line 17 always matches
src/el.erl:25: Warning: this clause cannot match because a previous clause at line 23 always matches
src/el.erl:27: Warning: this clause cannot match because a previous clause at line 23 always matches
===> Dialyzer starting, this may take a while...
…
src/el.erl
Line 8: The pattern <6, 6> can never match since previous clauses completely covered the type <_,_>
Line 19: The pattern <> can never match since previous clauses completely covered the type <>
Line 25: The pattern 6 can never match since previous clauses completely covered the type any()
Line 27: The variable _ can never match since previous clauses completely covered the type any()

If you find more scenarios worth mentioning, please write a comment about them below.

--

--