I spent 3 months analyzing failures caused by - what looked like - dirty builds but was caused by unstable compilation order. Which is quite obvious.
The solution is dynamic dependency resolution but this causes problem with macros.
The problem is easy to validate. Compile application multiple time and compare hashes. I'm not sure if it's sufficiently visible in bootstrapped Phoenix but I saw it in as small as <1000 LoC toy apps.
https://github.com/phoenixframework/phoenix/issues/6697
In case you want to see files affected I made extended writeup on my blog - for reference. https://xlii.space/eng/elixir-cycles/
Does Dialyzer understand Elixir? Last I knew, it could only process Erlang source code and BEAM files. Looking around, it seems like folks running Dialyzer against Elixir code are using some "dialyxer" thing.
You talk about circular dependencies causing minor compilation troubles, so it doesn't sound like you're talking about types defined in terms of each other. I might be unaware of something important, given that I've never had the opportunity to do Erlang professionally [0]... but aren't the only "dependencies" of BEAM files the exported functions they call in other modules? If I'm not wrong about that, then what happens when you run Dialyzer against BEAM files compiled from Elixir that has circular dependencies? Do its reports become more reliable, or does the reliability of those reports become irrelevant because the transformations the Elixir build system makes to your code make the structure of the BEAM code difficult to trace back to the Elixir source code?
[0] ...and have written nearly zero Elixir in any context...
I still use the Typespec syntax for its documentation benefits, and for catching "dumb" bugs, but as the Elixir compiler has improved I have found Dialyzer to be less relevant as the compiler usually catches things before Dialyzer would as it's not built into the compiler and isn't able to be.
Once compiled, it boils down to BEAM files that Dialyzer can understand, yes. And the [Dialyxir](https://dialyxir.hexdocs.pm) wrapper helps translating error messages in Elixir. But, there is a significant limitation compared to plain Erlang: Elixir protocols (which are quite used in core parts of the language) are not an Erlang construct, so Dialyzer will be clueless about them, just accepting any term. Enum.map(nil, & &foo/1) or to_string(%{}) will be invisible to it.
As for how the problem manifests: even obvious contract violations stops being shown (making it feel like "Dialyzer is useless") but the second tell is very long check times (tens of seconds up to minutes).
[W]hat happens when you run Dialyzer against BEAM files compiled from Elixir that has circular dependencies? Do its reports become more reliable, or does the reliability of those reports become irrelevant because the transformations the Elixir build system makes to your code make the structure of the BEAM code difficult to trace back to the Elixir source code?