Changelog for Elixir v1.20

Copy Markdown View Source

This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.

Type system improvements

This release includes type inference of all constructs.

Type inference of function definitions

Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:

def add_foo_and_bar(data) do
  data.foo + data.bar
end

Elixir now infers that the function expects a map as first argument, and the map must have the keys .foo and .bar whose values are either integer() or float(). The return type will be either integer() or float().

Here is another example:

def sum_to_string(a, b) do
  Integer.to_string(a + b)
end

Even though the + operator works with both integers and floats, Elixir infers that a and b must be both integers, as the result of + is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.

Type inference of guards

This release also performs inference of guards! Let's see some examples:

def example(x, y) when is_list(x) and is_integer(y)

The code above correctly infers x is a list and y is an integer.

def example({:ok, x} = y) when is_binary(x) or is_integer(x)

The one above infers x is a binary or an integer, and y is a two element tuple with :ok as first element and a binary or integer as second.

def example(x) when is_map_key(x, :foo)

The code above infers x is a map which has the :foo key, represented as %{..., foo: dynamic()}. Remember the leading ... indicates the map may have other keys.

def example(x) when not is_map_key(x, :foo)

And the code above infers x does not have the :foo key (hence x.foo will raise a typing violation), which has the type: %{..., foo: not_set()}.

You can also have expressions that assert on the size of data structures:

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

Elixir will correctly track the tuple has at most two elements, and therefore accessing elem(x, 3) will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).

Typing across clauses

Elixir now infers the type of a given clause based on previous clauses. Let's see an example:

case System.get_env("SOME_VAR") do
  nil -> :not_found
  value -> {:ok, String.upcase(value)}
end

System.get_env("SOME_VAR") returns either nil or a binary(). Because the first clause matches on nil, the type system now knows value can no longer be nil, and therefore it must only be a binary(), which allows the second clause to also type check without violations.

This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases.

Complete typing of maps keys

Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as dynamic().

As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:

%{123 => "hello", 456.0 => :ok}

will have the type:

%{integer() => binary(), float() => :ok}

It is also possible to mix domain keys, as above, with atom keys, yielding the following:

%{integer() => integer(), root: integer()}

This system is an implementation of Typing Records, Maps, and Structs, by Giuseppe Castagna (2023).

Typing of map operations

We have typed the majority of the functions in the Map module, allowing the type system to track how keys are added, updated, and removed across all possible key types.

For example, imagine we are calling the following Map functions with a variable map, which we don't know the exact shape of, and an atom key:

Map.put(map, :key, 123)
#=> returns type %{..., key: integer()}

Map.delete(map, :key)
#=> returns type %{..., key: not_set()}

As you can see, we track when keys are set and also when they are removed.

Some operations, like Map.replace/3, only replace the key if it exists, and that is also propagated by the type system:

Map.replace(map, :key, 123)
#=> returns type %{..., key: if_set(integer())}

In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the Map module and the given key is statically proven to never exist in the map, an error is emitted.

By combining full type inference with bang operations like Map.fetch!/2, Map.pop!/2, Map.replace!/3, and Map.update!/3, Elixir is able to propagate information about the desired keys. Take this module:

defmodule User do
  def name(map), do: Map.fetch!(map, :name)
end

defmodule CallsUser do
  def calls_name do
    User.name(%{})
  end
end

The code above has a type violation, which is now caught by the type system:

    warning: incompatible types given to User.name/1:

        User.name(%{})

    given types:

        %{name: not_set()}

    but expected one of:

        dynamic(%{..., name: term()})

    type warning found at:
    │
 16 │     User.name(%{})
    │         ~
    │
    └─ lib/calls_user.ex:7:5: CallsUser.calls_name/0

Acknowledgements

The type system was made possible thanks to a partnership between CNRS and Remote. The development work is currently sponsored by Fresha and Tidewave.

v1.20.0-rc.4

This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.

1. Enhancements

Elixir

  • [Code] Add :dbg_callback option to eval functions
  • [Code.Fragment] Allow preserving sigil metadata in container_cursor_to_quoted
  • [File] Add support for [:raw] opts in File.read/2
  • [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
  • [PartitionSupervisor] Support via tuples in count_children/1 and stop/3
  • [Process] Add Process.get_label/1

Mix

  • [mix deps] Allow overriding specific dependencies in :override

2. Bug fixes

Elixir

  • [Integer] Fix Integer.extended_gcd/2 returning negative GCD for zero base cases
  • [Integer] Raise when negative out-of-range digits are given to Integer.undigits/2
  • [Kernel] Protocols should not add compile-time dependencies on Any implementation
  • [Kernel] Ensure structs trigger recompilation for type checking purposes (regression)
  • [Kernel] Ensure type information propagate across hd/tl in guards (regression)
  • [Keyword] Raise ArgumentError in Keyword.from_keys/2 for non-atom keys
  • [URI] Fix URI.merge leaking :+ marker when base path is empty string

Mix

  • [mix deps] Use config files to pass project state to avoid argv limits on Windows when using MIX_OS_DEPS_COMPILE_PARTITION_COUNT
  • [mix compile] Fix compile env change triggering full recompilation of path dependencies
  • [mix compile] Add a build lock around protocol consolidation in umbrellas
  • [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
  • [mix test] Fix --warnings-as-errors not catching misnamed test file warnings

v1.20.0-rc.3 (2026-03-09)

1. Enhancements

IEx

  • [IEx] Optimize autocompleting modules

2. Bug fixes

Elixir

  • [Enum] Fix Enum.slice/2 for ranges with step > 1 sliced by step > 1
  • [File] Preserve directory permissions in File.cp_r/3
  • [File] Fix File.cp_r/3 infinite loop with symlink cycles
  • [File] Fix File.cp_r/3 infinite loop when copying into subdirectory of source
  • [File] Warn when defining @type record(), fixes CI on Erlang/OTP 29
  • [File] Fix File.Stream Enumerable.count for files without trailing newline
  • [Float] Fix Float.parse/1 inconsistent error handling for non-scientific notation overflow
  • [Kernel] Process fields even when structs are unknown (regression)
  • [Kernel] Improve performance on several corner cases in the type system (regression)
  • [Kernel] Fix regression when using Kernel.in/2 in defguard (regression)

v1.20.0-rc.2 (2026-03-04)

1. Enhancements

Elixir

  • [Code] Add module_definition: :interpreted option to Code which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated .beam file, which will have the same performance/behaviour as before
  • [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
  • [Integer] Add Integer.popcount/1
  • [Kernel] Add type inference across clauses. For example, if one clause says x when is_integer(x), then the next clause may no longer be an integer
  • [Kernel] Detect and warn on redundant clauses
  • [List] Add List.first!/1 and List.last!/1
  • Add Software Bill of Materials guide to the Documentation

Mix

  • [mix compile] Add module_definition: :interpreted option to Code which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated .beam file, which will have the same performance/behaviour as before
  • [mix deps] Parallelize dep lock status checks during deps.loadpaths, improving boot times in projects with many git dependencies

2. Potential breaking changes

Elixir

  • map.foo() (accessing a map field with parens) and mod.foo (invoking a function without parens) will now raise instead of emitting runtime warnings, aligning themselves with the type system behaviour

3. Bug fixes

IEx

  • [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
  • [IEx] Ensure pry works across remote nodes

Mix

  • [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution

v1.20.0-rc.1 (2026-01-13)

1. Bug fixes

Elixir

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

v1.20.0-rc.0 (2026-01-09)

1. Enhancements

Elixir

  • [Calendar] Optimize date_from_iso_days by using the Neri-Schneider algorithm
  • [Enum] Add Enum.min_max sorter
  • [Integer] Add Integer.ceil_div/2
  • [IO] Add IO.iodata_empty?/1
  • [File] Skip device, named pipes, etc in File.cp_r/3 instead of erroring with reason :eio
  • [Kernel] Print intermediate results of dbg for pipes
  • [Kernel] Warn on unused requires
  • [Regex] Add Regex.import/1 to import regexes defined with /E

ExUnit

  • [ExUnit.CaptureLog] Add :formatter option for custom log formatting

Mix

  • [mix deps] Support filtering mix deps output
  • [mix compile] Enforce :elixirc_paths to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
  • [mix test] Add mix test --dry-run

2. Potential breaking changes

Elixir

  • require SomeModule no longer expands to the given module at compile-time, but it still returns the module at runtime. Note that while Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as require(SomeMod).some_macro(), we are adding this note to the CHANGELOG

3. Hard deprecations

Elixir

  • [File] File.stream!(path, modes, lines_or_bytes) is deprecated in favor of File.stream!(path, lines_or_bytes, modes)
  • [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as <<x::size(^existing_var)>>
  • [Kernel.ParallelCompiler] Kernel.ParallelCompiler.async/1 is deprecated in favor of Kernel.ParallelCompiler.pmap/2, which is more performant and addresses known limitations

Logger

v1.19

The CHANGELOG for v1.19 releases can be found in the v1.19 branch.