main-latest
Automated release for latest main.
This is a preview β how elixir's changelog would look hosted on Wakelog (100 entries found, first 15 shown). Nothing was saved. To make it real: sign up (30 seconds, no email), create a project, and paste the same URL in the Import box β you'll also get RSS, an embeddable widget, a README badge, and an API. Try another.
changelog preview Β· powered by Wakelog
Automated release for latest main.
Automated release for latest v1.20.
++/2 operatorsend/2 return typeand/2 and or/2Map.put/3 on empty map typesreceive/after timeout expressions:maps.values/1 reference the correct function%_{} patterns as precise so subsequent redundant struct clauses are detected--profile time optionbreak!/1 with unknown expressionsprofile: :time:uniq or :into are usedquote with unquote is used inside a pattern or guardKernel.put_elem/3 to emit :erlang.setelement/3__info__(:struct) to include the :required keyAutomated release for latest v1.18.
Version module (CVE-2026-49762, GHSA-w2h8-8x3g-278p)Calendar.strftime/2 to 1024 charactersCode.require_file releases the file if compilation fails--no-compile option when loading pluginsAnnouncement: https://elixir-lang.org/blog/2026/06/03/elixir-v1-20-0-released/
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
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).
Elixir also performs inference based on the function body itself. 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. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
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 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. Elixir v1.20 also implements occurrence typing for cond, case, and with, providing more precise types within each clause.
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).
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
The type system was made possible thanks to a partnership between CNRS and Remote. The development work is currently sponsored by Fresha and Tidewave.
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
It also introduces a new compiler option called :module_definition, which if the module definition should be :compiled (the default) or :interpreted. Note this does not affect the .beam file written to disk, only how the contents inside defmodule are executed. Using the :interpreted mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
defmodule can have only up to 20 arguments. If this is an issue, you can use maps or tuples to group the data. Note the functions themselves inside defmodule, such as the ones defined inside def and friends, can still have up to 255 argumentsYou can enable it by setting elixirc_options: [module_definition: :interpreted] in your mix.exs.
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
date_from_iso_days by using the Neri-Schneider algorithm:dbg_callback option to eval functionsmodule_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 beforecontainer_cursor_to_quotedEnum.min_max sorter[:raw] opts in File.read/2File.cp_r/3 instead of erroring with reason :eioFloat.round/2 by avoiding big integersInteger.ceil_div/2Integer.popcount/1IO.iodata_empty?/1x when is_integer(x), then the next clause may no longer be an integercase, cond, and withdbg for pipesList.first!/1 and List.last!/1after_compile/2 callback failscount_children/1 and stop/3Process.get_label/1keys: {:duplicate, :key} to ordered_set with composite keysRegex.import/1 to import regexes defined with /EString.length/1 and String.slice/3--repeat-until-failure:formatter option for custom log formattingsource/1--output optionmodule_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:elixirc_paths to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)deps.loadpaths, improving boot times in projects with many git dependenciesmix deps output--output option--no-compile optionmix source MODULE to print or open a given module/function locationmix test --dry-run? for security reasonsrequire SomeModule no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as require(SomeMod).some_macro(), we are adding this note to the CHANGELOGEnum.slice/2 for ranges with step > 1 sliced by step > 1File.cp_r/3File.cp_r/3 infinite loop with symlink cyclesFile.cp_r/3 infinite loop when copying into subdirectory of sourceFile.Stream's Enumerable.count for files without trailing newline@type record() for Erlang/OTP 29Float.parse/1 inconsistent error handling for non-scientific notation overflowInteger.extended_gcd/2 returning negative GCD for zero base casesInteger.undigits/2only: :sigils option when the imported module exports non-sigil symbols with sigil_ prefixAny implementationto_timeout/1ArgumentError in Keyword.from_keys/2 for non-atom keysMacro.to_string/1 with escaped trailing newlinePath.relative_to_cwd/2Stream.cycle/1 when enumerable reduce call yields no elementsString.count/2URI.merge leaking :+ marker when base path is empty stringLogger.configure/1non_executable_binary_to_term on loopback pubsubMIX_OS_DEPS_COMPILE_PARTITION_COUNT--warnings-as-errors not catching misnamed test file warnings--raise when mix test --warnings-as-errors passes with warningsFile.stream!(path, modes, lines_or_bytes) is deprecated in favor of File.stream!(path, lines_or_bytes, modes)<<x::size(^existing_var)>>Kernel.ParallelCompiler.async/1 is deprecated in favor of Kernel.ParallelCompiler.pmap/2, which is more performant and addresses known limitationsLogger.*_backend functions are deprecated in favor of handlers. If you really want to keep on using backends, see the :logger_backends packageLogger.enable/1 and Logger.disable/1 have been deprecated in favor of Logger.put_process_level/2 and Logger.delete_process_level/1xref: [exclude: ...] in your mix.exs is deprecated in favor of elixirc_options: [no_warn_undefined: ...]This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
case inside a cond condition (regression):override due to conflicts with Hex (revert)xref: [exclude: ...] in your mix.exs is deprecated in favor of elixirc_options: [no_warn_undefined: ...]This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
Float.round/2 by avoiding big integerscase, cond, and with{:duplicate, :key} key_ets to ordered_set with composite keysString.length/1 and String.slice/3--repeat-until-failuresource/1--output option--output option--no-compile optionmix source MODULE to print or open a given module/function location? for security reasonsonly: :sigils option when the imported module exports non-sigil symbols with sigil_ prefixto_timeout/1Macro.to_string/1 with escaped trailing newlinePath.relative_to_cwd/2Stream.cycle/1 when enumerable reduce call yields no elementsString.count/2Logger.configure/1non_executable_binary_to_term on loopback pubsubThis release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
:dbg_callback option to eval functionscontainer_cursor_to_quoted[:raw] opts in File.read/2after_verify/2 callback failscount_children/1 and stop/3Process.get_label/1:overrideInteger.extended_gcd/2 returning negative GCD for zero base casesInteger.undigits/2Any implementationhd/tl in guards (regression)ArgumentError in Keyword.from_keys/2 for non-atom keysURI.merge leaking :+ marker when base path is empty stringMIX_OS_DEPS_COMPILE_PARTITION_COUNT--warnings-as-errors not catching misnamed test file warningsEnum.slice/2 for ranges with step > 1 sliced by step > 1File.cp_r/3File.cp_r/3 infinite loop with symlink cyclesFile.cp_r/3 infinite loop when copying into subdirectory of source@type record(), fixes CI on Erlang/OTP 29File.Stream Enumerable.count for files without trailing newlineFloat.parse/1 inconsistent error handling for non-scientific notation overflowKernel.in/2 in defguard (regression)Overall, the compiler finds more bugs, for free, and it has never been faster:
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 beforeInteger.popcount/1x when is_integer(x), then the next clause may no longer be an integerList.first!/1 and List.last!/1module_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 beforedeps.loadpaths, improving boot times in projects with many git dependenciesmap.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<<expr::bitstring>> will have type binary instead of bitstring if expr is a binaryThis release includes type inference of all constructs.
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.
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).
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).
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()})
typing violation found at:
β
16 β User.name(%{})
β ~
β
ββ lib/calls_user.ex:7:5: CallsUser.calls_name/0
The type system was made possible thanks to a partnership between CNRS and Remote. The development work is currently sponsored by Fresha and Tidewave.
date_from_iso_days by using the Neri-Schneider algorithmEnum.min_max sorterInteger.ceil_div/2IO.iodata_empty?/1File.cp_r/3 instead of erroring with reason :eiodbg for pipesRegex.import/1 to import regexes defined with /E:formatter option for custom log formattingmix deps outputmix test --dry-runFile.stream!(path, modes, lines_or_bytes) is deprecated in favor of File.stream!(path, lines_or_bytes, modes)<<x::size(^existing_var)>>Kernel.ParallelCompiler.async/1 is deprecated in favor of Kernel.ParallelCompiler.pmap/2, which is more performant and addresses known limitationsLogger.*_backend functions are deprecated in favor of handlers. If you really want to keep on using backends, see the :logger_backends packageLogger.enable/1 and Logger.disable/1 have been deprecated in favor of Logger.put_process_level/2 and Logger.delete_process_level/1dbg_callback is modified at runtimenot inStream.flat_map/2 to crash#iex:break as part of multi-line prompts