Elixir v1.20.0-rc.0 (and rc.1) released: type inference of all constructs

It would work. Personally, I dislike constructs that blur the lines between the type annotations and runtime code/checks. I would prefer a solution that doesn’t require me to change any of my application or test code since this is related to inference in the first place, but that’s just my preference. I dunno what the popular opinion on that is. I just know I really like how Python clearly separates static types from runtime behavior and TypeScript gets really messy when it’s unclear which context some code is in. For example, accessing object attributes on an Enum in typescript is pretty messy cause it changes the context of the code from the compile-time type to the runtime object implicitly without clear indication in the syntax. :slight_smile:

You always have those type::Person wanting to fix those things and spoil the fun. Thanks @Rubas :wink:

1 Like

Yes, this is a good call. In Elixir we have to use `Process.get` because we are often getting warnings in the language constructs themselves (which are now type checked) and we can’t apply them. Perhaps `apply` should be our official recommendation from now on.

6 Likes

Yes, it is possible to do it in the same way we do atoms, where we have the `atom()` type but also literals such as `:foo` and `:bar`. The issue is that the more precise the types are, the more you have to prove them. So if you have a function that only accepts 2 to 26, you can’t give it the result of `a + b`, because you haven’t proved that the result is in the range 2..26. So you either need to check upfront or mark the result of `a + b` as a dynamic, both which can be seen as cumbersome.

5 Likes

Hmm, yeah probably a reasonable recommendation for testing application code. apply/3 is also nice for testing because it parametrizes really well. Testing groups of similar functions like a validation layer or modules of composable/dynamic ecto query functions is pretty clean with apply/3.

Something like this:


cases = [
  {StringHandlers, :iso8601_to_datetime, ["2026-01-12T12:30:00Z"], ~U[2026-01-12 12:30:00Z]},
  {StringHandlers, :iso8601_to_date, ["2026-01-12T12:30:00Z"], ~D[2026-01-12]},
  {NumberHandlers, :timestamp_to_datetime, [1768221000], ~U[2026-01-12 12:30:00Z]},
]

for {mod, fun, args, expected} <- cases do
  test "parse inputs - #{fun} - #{args}" do
    mod = unquote(mod)
    fun = unquote(fun)
    args = unquote(args)

    assert apply(mod, fun, args) == expected
  end
end
2 Likes

1. Bug fixes

Elixir

  • [Kernel] Improve the performance of the type system when working with large unions of open maps
  • [Kernel] Do not crash on map types with struct keys when performing type operations
  • [Kernel] Mark the outcome of bitstring types as dynamic
  • [Kernel] <<expr::bitstring>> will have type binary instead of bitstring if expr is a binary
  • [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable

I am keeping it all in the same thread as we only had bug fixes.

15 Likes

Only issue I discovered was when running credo which suddenly warns of missing @moduledoc. This started to appear with 1.20.0-rc.0 and is still present in 1.20.0-rc.1.

ex_doc does find the @moduledoc which indicates that it’s an issue in credo, but it also indicates that something must have changed in elixir

I haven’t dug deeper… will do so when I find some time

This has been fixed in credo by @Eiji (already discussed above): Fix 1.20.x compatibility issue with block expression AST by Eiji7 · Pull Request #1241 · rrrene/credo · GitHub.

3 Likes

I must have missed that post. Thx

I just finished upgrading a rather big project to 1.19.5 and resolving all warnings. After setting the elixir version to 1.20.0-rc.1 and running mix.compile the compiler revealed many new warnings

  • Found a lot of unused required modules. Mostly Logger and Ecto.Query (TIL you don’t need to require the Logger to make use of Logger.metadata)

  • Possible false positive or future deprecation? This warning was emitted, but pinning the variable here has no effect aside from removing the warning

    warning: the variable "chunk_size" is accessed inside size(...) of a bitstring but it was defined outside of the match. You must precede it with the pin operator
        │
    118 │     <<chunk::binary-size(chunk_size), rest::binary>> = binary
    
  • It found a couple of interesting, unused function clauses, similar to the cases below. In this particular case it was fine, but there are some cases where I cannot figure out the reason. Still a great addition to the compiler :+1:

    current = Enum.at(search_results, current_index)
    highlight_term(current.sentence_text, current)}
    
         warning: this clause of defp highlight_term/2 is never used
         │
     194 │   defp highlight_term(text, nil), do: text
    
  • In this particular case it didn’t complain about the fact that we (accidentally) assumed that uri.query is a map, but we’re trying to pass a map to URI.decode_query. Still a great find :+1:

         warning: incompatible types given to URI.decode_query/1:
    
             URI.decode_query(query)
    
         given types:
    
             dynamic(map())
    
         but expected one of:
    
             binary()
    
         where "query" was given the type:
    
             # type: dynamic(map())
             # from: my_code.ex:187:33
             is_map(query)
    
         typing violation found at:
         │
     188 │            %{"latency" => latency_raw} <- URI.decode_query(query),
    
        uri = URI.parse(url)
    
        latency =
          with %{query: query} when is_map(query) <- uri,
               %{"latency" => latency_raw} <- URI.decode_query(query),
               {latency, ""} <- Integer.parse(latency_raw) do
    
    

I’ve also measured the performance of the compilation of this project a bit. ~190 dependencies and +800 files. Overall very happy with the results and MIX_OS_DEPS_COMPILE_PARTITION_COUNT is a blessing for sure
deps = mix deps.compile
project = mix compile

elixir_version stage partitions seconds
1.18.4-otp-28 deps unset 73.95
1.18.4-otp-28 project 11.57
1.19.3-otp-28 deps unset 68.80
1.19.3-otp-28 deps 2 41.78
1.19.3-otp-28 deps 4 31.60
1.19.3-otp-28 project 13.65
1.19.5-otp-28 deps unset 68.00
1.19.5-otp-28 deps 2 41.60
1.19.5-otp-28 deps 4 31.72
1.19.5-otp-28 project 12.88
1.20.0-rc.1-otp-28 deps unset 69.54
1.20.0-rc.1-otp-28 deps 2 41.56
1.20.0-rc.1-otp-28 deps 4 32.22
1.20.0-rc.1-otp-28 project 13.25
12 Likes

Thank you for the invididual break down, this is very useful.

Aye, this is a future deprecation. They should be addressed!

3 Likes

The blog post gives the following example.

def example(x) when tuple_size(x) < 3

A subsequent call elem(x, 3) shall get caught by the type system.

Would this work in the following code?

i = 3
el = elem(x, i)

Or, would it fall back to a runtime check?

Thanks.

It will fallback to a runtime check! We may want to revisit this in the future and force elem to always be constant, and use elem! for runtime. Too early to say!

1 Like

Noted, @josevalim, thanks.

The detection of unused require is very nice!
There a a bunch of libraries out which are going to need a fix.
Namely require Logger.
And of course I got hit by that one too.
Just saying

1 Like

I tried the RC1 at our codebase, and we had 3 classes of errors:

  1. foo.bar when foo was dynamic(atom), easy fix by foo.bar().
  2. unused require.
  3. unused defp which implement liveview function components.

For 1 I initially thought there was a bug in the type inference, as tests failed after changing it. Though I was just hit by a flaky test twice in a row :smiley:

2 really will clean up a lot and probably will even improve compilation times, as we break compiletime dependencies between modules. Half of the warnings is about Logger, though among the remaining half a good part is internal modules.

And with 3 I am wondering why those haven’t been recognized before. What I recognized was, was that the error spoke about “unused defoveridable”, which feels like leaking phoenix implementation details. I think the error message could be improved, though I am not sure how exactly. Among the samples I removed, there have been no false positives. Fixing all those warnings might get rid of a huge portion of frontend related code.

Sadly I will not be able to do more experiments than what I have already done before the actual release, and I am looking forward to it!

PS: compilation times remained stable

6 Likes

Just testing this out now - like everyone else the main warning I’m getting from my apps/libraries is about unused requires :sweat_smile:

Though I’m also getting an error at the end of my test suite, after running mix test:

** (EXIT from #PID<0.94.0>) an exception was raised:
    ** (Protocol.UndefinedError) protocol Enumerable not implemented for :term (a struct)

Got value:

    %{__struct__: :term}

        (elixir 1.20.0-rc.1) lib/enum.ex:5: Enumerable.impl_for!/1
        (elixir 1.20.0-rc.1) lib/enum.ex:184: Enumerable.reduce/3
        (elixir 1.20.0-rc.1) lib/enum.ex:4672: Enum.reverse/1
        (elixir 1.20.0-rc.1) lib/enum.ex:3919: Enum.to_list/1
        (elixir 1.20.0-rc.1) lib/module/types/descr.ex:4194: Module.Types.Descr.map_literal_to_quoted/2
        (elixir 1.20.0-rc.1) lib/enum.ex:1725: Enum."-map/2-lists^map/1-1-"/2
        (elixir 1.20.0-rc.1) lib/enum.ex:1272: anonymous fn/3 in Enum.flat_map/2
        (stdlib 7.2) maps.erl:894: :maps.fold_1/4
        (elixir 1.20.0-rc.1) lib/enum.ex:2629: Enum.flat_map/2
        (elixir 1.20.0-rc.1) lib/module/types/descr.ex:719: Module.Types.Descr.non_term_type_to_quoted/2
        (elixir 1.20.0-rc.1) lib/module/types/apply.ex:1975: anonymous fn/2 in Module.Types.Apply.clause_args_to_quoted_string/3
        (elixir 1.20.0-rc.1) lib/enum.ex:1725: Enum."-map/2-lists^map/1-1-"/2
        (elixir 1.20.0-rc.1) lib/enum.ex:1725: Enum."-map/2-lists^map/1-1-"/2
        (elixir 1.20.0-rc.1) lib/module/types/apply.ex:1975: Module.Types.Apply.clause_args_to_quoted_string/3
        (elixir 1.20.0-rc.1) lib/module/types/apply.ex:1967: anonymous fn/4 in Module.Types.Apply.clauses_args_to_quoted_string/3
        (elixir 1.20.0-rc.1) lib/enum.ex:5084: Enum.with_index_list/3
        (elixir 1.20.0-rc.1) lib/module/types/apply.ex:1963: Module.Types.Apply.clauses_args_to_quoted_string/3
        (elixir 1.20.0-rc.1) lib/module/types/apply.ex:1786: Module.Types.Apply.format_diagnostic/1
        (elixir 1.20.0-rc.1) lib/module/parallel_checker.ex:338: anonymous fn/2 in Module.ParallelChecker.group_warnings/1
        (elixir 1.20.0-rc.1) lib/enum.ex:2617: Enum."-reduce/3-lists^foldl/2-0-"/3

And no summary output is printed.

Tested with Elixir 1.20.0-rc.1-otp-28, Erlang 28.3.1 with the main branch of the GitHub - sevenseacat/cinder: A powerful, intelligent data collection component for Ash Framework resources in Phoenix LiveView repo. It didn’t happen with Elixir 1.19.5-otp-28 (and same Erlang version)!

We fixed this one in Elixir main :slight_smile: can you please give it a try too?

2 Likes

ah I should have known! cheers :slight_smile:

It’s extremely impressive how Elixir keeps getting faster and more powerful without breaking (almost any) backwards compatibility. Kudos to @josevalim and the team!

8 Likes