Compare commits

...
8 Commits
23 changed files with 221 additions and 182 deletions
+49 -98
View File
@@ -122,9 +122,9 @@ While Elixir has always compiled the given files in project or a dependency in p
### Code loading bottlenecks
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing parallelization, especially on large projects.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server and the amount of work during compilation, with reports of more than two times faster compilation for large projects. The benefits depend on the codebase size and the number of CPU cores available.
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
@@ -140,7 +140,7 @@ defmodule MyLib.SomeModule do
end
```
Because the spawned module is not visible by the compiler, it won't be able to load `MyLib.SomeOtherModule`. You have two options, either use `Kernel.ParallelCompiler.pmap/2` or explicitly call `Code.ensure_loaded!(MyLib.SomeOtherModule)` before spawning the process that uses said module.
Because the spawned process is not visible to the compiler, it won't be able to load `MyLib.SomeOtherModule`. You have two options, either use `Kernel.ParallelCompiler.pmap/2` or explicitly call `Code.ensure_compiled!(MyLib.SomeOtherModule)` before spawning the process that uses said module.
The second one is related to `@on_load` callbacks (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invoke other modules defined within the same project. For example:
@@ -162,13 +162,13 @@ MyLib.SomeModule.something_else()
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
Both snippets above could actually lead to non-deterministic compilation in the past, and as a result of these changes, compilating these cases are now deterministic.
Both snippets above could actually lead to non-deterministic compilation failures in the past, and as a result of these changes, compiling these cases are now deterministic.
### Parallel compilation of dependencies
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, as outlined in the previous section, there were pathological cases where performance gains would be left on the table, such as when compiling dependencies with native code or dependencies where one or two large files would take most of the compilation time.
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
@@ -201,9 +201,9 @@ Given this may reduce the amount of data printed by default, the default limit h
## Erlang/OTP 28 support
Elixir v1.19 officially supports Erlang/OTP 28.1+ and later. In order to support the new Erlang/OTP 28 representation for regular expressions, structs can now control how they are escaped by defining a `__escape__/1` callback that must return AST.
Elixir v1.19 officially supports Erlang/OTP 28.1+ and later. In order to support the new Erlang/OTP 28 representation for regular expressions, structs can now control how they are escaped into abstract syntax trees by defining a `__escape__/1` callback.
On the other, the new representation for regular expressions implies they can no longer be used as default values for struct fields. Instead of this:
On the other hand, the new representation for regular expressions implies they can no longer be used as default values for struct fields. Instead of this:
```elixir
defmodule Foo do
@@ -232,92 +232,9 @@ Elixir v1.19 is also our first release following OpenChain compliance, [as previ
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
## v1.19.0-rc.2 (2025-10-07)
### 1. Enhancements
#### Elixir
* [Regex] Raise error message when regexes are used as default values in struct fields for compatibility with Erlang/OTP 28
* [Registry] Add key-based partitioning of duplicate registries
### 2. Bug fixes
#### Elixir
* [Kernel] Address issue with type checking not completing on protocol consolidation
#### ExUnit
* [ExUnit] Do not crash on empty test unit groups
#### Mix
* [mix help] Add `mix help app:APP`
* [mix test] Fix module preloading in `mix test --slowest-modules=N`
## v1.19.0-rc.1 (2025-10-05)
### 1. Enhancements
#### Elixir
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
* [OptionParser] Support the `:regex` type
* [OptionParser] Enhance parsing error to display available options
* [String] Update to Unicode 17.0.0
#### ExUnit
* [ExUnit] Set a process label for each test
#### Logger
* [Logger] Accept any enumerable in `Logger.metadata/1`
#### Mix
* [mix format] Add options to mix format to allow excluding of files
* [mix test] Add `--name-pattern` option to `mix test`
* [Mix.install/2] Support the `:compilers` option
### 2. Bug fixes
#### Elixir
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
* [Code] Properly handle column annotation for `in` in `not in` expressions
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
* [Inspect] Inspect ill-formed structs as maps
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
#### ExUnit
* [ExUnit.Assertions] Fix order in ExUnit results when listing pinned variables
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
#### IEx
* [IEx] Abort pipelines when there is an error in any step along the way
#### Mix
* [mix compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
* [mix compile] Avoid failures when locking compilation across different users
* [mix compile] Fix race condition when renaming files used by the compilation lock
* [mix test] Prevent `mix test` from overriding `:failures_manifest_path` option
### 3. Hard deprecations
#### Elixir
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
## v1.19.0-rc.0 (2025-06-09)
## v1.19.0 (2025-10-16)
### 1. Enhancements
@@ -335,18 +252,26 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
* [Inspect] Allow `optional: :all` when deriving Inspect
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
* [Kernel] Allow controlling which applications are used during inference
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
* [Kernel] Support `min/2` and `max/2` as guards
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
* [OptionParser] Support the `:regex` type
* [OptionParser] Enhance parsing error to display available options
* [Protocol] Type checking of protocols dispatch and implementations
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
* [Regex] Raise error message when regexes are used as default values in struct fields for compatibility with Erlang/OTP 28
* [Registry] Add key-based partitioning of duplicate registries
* [String] Add `String.count/2` to count occurrences of a pattern
* [String] Update to Unicode 17.0.0
#### ExUnit
* [ExUnit] Set a process label for each test
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
* [ExUnit.Case] Add `:test_group` to the test context
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
@@ -357,36 +282,61 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
#### Logger
* [Logger] Accept any enumerable in `Logger.metadata/1`
#### Mix
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun`, `mix help Mod.fun/arity`, and `mix help app:package`
* [mix format] Add options to mix format to allow excluding of files
* [mix test] Add `--name-pattern` option to `mix test`
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
* [mix xref graph] Add support for `--format json`
* [mix xref graph] Emit a warning if `--source` is part of a cycle
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
* [Mix] Support the `:compilers` option
* [Mix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
### 2. Bug fixes
#### Elixir
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
* [Code] Properly handle column annotation for `in` in `not in` expressions
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
* [Inspect] Inspect ill-formed structs as maps
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
* [Protocol] `defstruct/1` and `defexception/1` are now disabled inside `defprotocol` as to not allow defining structs/exceptions alongside a protocol
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
#### ExUnit
* [ExUnit.Assertions] Fix order of pinned variables in failure reports
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
* [ExUnit.Case] Do not crash on empty test unit groups
#### IEx
* [IEx] Abort pipelines when there is an error in any step along the way
#### Mix
* [mix cmd] Preserve argument quoting in subcommands
* [mix cmd] Preserve argument quoting in subcommands by no longer performing shell expansion. To revert to the previous behaviour, pass `--shell` before the command name
* [mix compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
* [mix compile] Avoid failures when locking compilation across different users
* [mix compile] Fix race condition when renaming files used by the compilation lock
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
* [mix test] Preserve files with no longer filter on `mix test`
* [mix test] Ensure modules are preloaded in `mix test --slowest-modules=N`
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
### 3. Soft deprecations (no warnings emitted)
@@ -399,12 +349,13 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
#### Mix
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and the task itself has no effect
### 4. Hard deprecations
#### Elixir
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
+1 -1
View File
@@ -1 +1 @@
1.19.0-rc.2
1.19.0
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.0-rc.2
ELIXIR_VERSION=1.19.0
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.19.0-rc.2
set ELIXIR_VERSION=1.19.0
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+1 -1
View File
@@ -1763,7 +1763,7 @@ defmodule DateTime do
Although the first example shows a difference of 2 hours when
comparing the wall clocks of the given datetime with the returned one,
due to the "spring forward" time jump, the actual ellapsed time is
due to the "spring forward" time jump, the actual elapsed time is
still exactly of 1 hour.
In case you don't want these changes to happen automatically or you
+1 -1
View File
@@ -317,7 +317,7 @@ defmodule File do
directories of `path`
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory
* `:eperm` - missed required permisions
* `:eperm` - missed required permissions
## Examples
+1 -1
View File
@@ -565,7 +565,7 @@ defmodule Module do
written to disk, and therefore raising does not effectively halt compilation
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
or other callback. Given modules have already been compiled, functions in
ths module, such as `get_attribute/2`, which expect modules to not have been
this module, such as `get_attribute/2`, which expect modules to not have been
yet compiled, do not work on `@after_verify` callback.
Accepts a module or a `{module, function_name}` tuple. The function
+20 -8
View File
@@ -49,8 +49,8 @@ defmodule Module.Types do
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause ->
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
{_, kind, _, _} = def ->
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
false ->
false
@@ -82,7 +82,7 @@ defmodule Module.Types do
{types, context} ->
# Optimized version of finder, since we already the definition
finder = fn _ ->
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
@@ -146,12 +146,24 @@ defmodule Module.Types do
end
end
defp default_domain({_, arity} = fun_arity, impl) do
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
with {for, callbacks} <- impl,
true <- fun_arity in callbacks do
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
args = [
Descr.dynamic(Module.Types.Of.impl(for))
| List.duplicate(Descr.dynamic(), arity - 1)
]
{fun_arity, kind, meta, clauses} = def
clauses =
for {meta, args, guards, body} <- clauses do
{[type_check: {:impl, for}] ++ meta, args, guards, body}
end
{mode, {fun_arity, kind, meta, clauses}, args}
else
_ -> List.duplicate(Descr.dynamic(), arity)
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
end
end
@@ -208,7 +220,7 @@ defmodule Module.Types do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
false -> false
end
end
@@ -219,7 +231,7 @@ defmodule Module.Types do
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already the definition
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
+14
View File
@@ -132,6 +132,20 @@ defmodule Module.Types.Helpers do
the union (which may be none)
"""
{:impl, for} ->
# Get the type without dynamic for better pretty printing
type =
for
|> Module.Types.Of.impl()
|> Module.Types.Descr.dynamic()
|> Map.fetch!(:dynamic)
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
"""
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
"""
:empty_domain ->
"""
+10 -4
View File
@@ -259,12 +259,12 @@ defmodule Module.Types.Pattern do
defp badpattern_error(expr, index, tag, stack, context) do
meta =
if meta = get_meta(expr) do
meta ++ Keyword.take(stack.meta, [:generated, :line])
meta ++ Keyword.take(stack.meta, [:generated, :line, :type_check])
else
stack.meta
end
error(__MODULE__, {:badpattern, expr, index, tag, context}, meta, stack, context)
error(__MODULE__, {:badpattern, meta, expr, index, tag, context}, meta, stack, context)
end
defp of_pattern_intersect(tree, expected, expr, index, tag, stack, context) do
@@ -802,13 +802,19 @@ defmodule Module.Types.Pattern do
#
# The match pattern ones have the whole expression instead
# of a single pattern.
def format_diagnostic({:badpattern, pattern_or_expr, index, tag, context}) do
def format_diagnostic({:badpattern, meta, pattern_or_expr, index, tag, context}) do
{to_trace, message} = badpattern(tag, pattern_or_expr, index)
traces = collect_traces(to_trace, context)
hints =
case Keyword.get(meta, :type_check) do
{:impl, _} = impl -> [impl]
_ -> []
end
%{
details: %{typing_traces: traces},
message: IO.iodata_to_binary([message, format_traces(traces)])
message: IO.iodata_to_binary([message, format_traces(traces), format_hints(hints)])
}
end
@@ -320,7 +320,7 @@ end
In the previous example, even though Elixir does not know which modules the function `example/0` was invoked on, it knows the modules `OtherModule.Foo` and `OtherModule.Bar` are referred outside of a function and therefore they become compile-time dependencies. If any of them change, Elixir will recompile `MyModule` itself.
However, you should not programatically generate the module names themselves, as that would make it impossible for Elixir to track them. More precisely, do not do this:
However, you should not programmatically generate the module names themselves, as that would make it impossible for Elixir to track them. More precisely, do not do this:
```elixir
defmodule MyModule do
@@ -348,7 +348,7 @@ end
#### Refactoring
To address this anti-pattern, you should avoid defining module names programatically. For example, if you need to dispatch to multiple modules, do so by using full module names.
To address this anti-pattern, you should avoid defining module names programmatically. For example, if you need to dispatch to multiple modules, do so by using full module names.
Instead of:
+1 -1
View File
@@ -207,4 +207,4 @@ end
When a long action is performed on the server, all other requests to that particular server will wait until the action is done, which may cause some clients to timeout.
Some APIs, such as GenServers, make a clearer distiction between client and server, and we will explore them in future chapters. Next let's talk about naming things, applications, and supervisors.
Some APIs, such as GenServers, make a clearer distinction between client and server, and we will explore them in future chapters. Next let's talk about naming things, applications, and supervisors.
@@ -181,7 +181,7 @@ end
Now run `mix test` again and our app should boot but we should see one failure. When we changed the `KV` module, we broke the boilerplate test case which tested the `KV.hello/0` function. You can simply remove that test case and we are back to a green suite.
We wrote very little code but we did something incredibly powerful. We now have a function, `KV.start/2` that is invoked whenever your application starts. This gives us the perfect place to start our key-value registry. The `Application` module also allows us to define a `stop/1` callback and other funtionality. You can check the `Application` and `Supervisor` modules for extensive documentation on their uses.
We wrote very little code but we did something incredibly powerful. We now have a function, `KV.start/2` that is invoked whenever your application starts. This gives us the perfect place to start our key-value registry. The `Application` module also allows us to define a `stop/1` callback and other functionality. You can check the `Application` and `Supervisor` modules for extensive documentation on their uses.
Let's finally start our registry.
@@ -142,11 +142,13 @@ Inferring type signatures comes with a series of trade-offs:
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies (in the future). Calls to modules within the same project are assumed to be `dynamic()` as to reduce cyclic dependencies and the need for recompilations. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, we are exploring "module type inference": our goal is to infer type signatures considering invocations of functions in the same module and of functions from *other applications* (such as Elixir itself and your dependencies). Once module types are inferred, your whole project is type checked considering all declared and inferred types.
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
We have successfully implemented these features as part of Elixir v1.19, by performing inference of all constructs (except guards), taking into account the signatures from calls to functions within the same module and in Elixir's standard library.
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
In future releases, we plan to perform type inference of guards and also consider the type signatures of your dependencies during inference. Overall, our goal with type inference is to find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
Keep in mind this only applies to *type inference*. Once we introduce type signatures and you explicitly annotate your functions, type inference and the trade-offs above no longer play a role. Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
## Roadmap
@@ -83,7 +83,7 @@ iex> _
** (CompileError) iex:3: invalid use of _
```
A pinned value represents the value itself and not its – even if syntatically equal – pattern. The right hand side is compared to be equal to the pinned value:
A pinned value represents the value itself and not its – even if syntactically equal – pattern. The right hand side is compared to be equal to the pinned value:
```iex
iex> x = %{}
+2 -1
View File
@@ -257,11 +257,12 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
{EContext, SC, EC} = expand(QContext, SP, EP),
Quoted = elixir_quote:quote(Exprs, Q),
{EQuoted, ES, EQ} = expand(Quoted, SC, EC),
BindingMeta = lists:keydelete(column, 1, Meta),
EBinding =
[{'{}', [],
['=', [], [
{'{}', [], [K, Meta, EContext]},
{'{}', [], [K, BindingMeta, EContext]},
V
]
]} || {K, V} <- Binding],
+16 -41
View File
@@ -141,16 +141,12 @@ do_tuple_linify(Fun, Meta, Left, Right, Var) ->
%% lines are kept and hygiene mechanisms are disabled.
escape(Expr, Op, Unquote) ->
try
Q = #elixir_quote{
do_quote(Expr, #elixir_quote{
line=true,
file=nil,
op=Op,
unquote=Unquote
},
case Unquote of
true -> do_quote(Expr, Q);
false -> do_escape(Expr, Q)
end
})
catch
Kind:Reason:Stacktrace ->
Pruned = lists:dropwhile(fun
@@ -162,14 +158,15 @@ escape(Expr, Op, Unquote) ->
do_escape({Left, Meta, Right}, #elixir_quote{op=escape_and_prune} = Q) when is_list(Meta) ->
TM = [{K, V} || {K, V} <- Meta, (K == no_parens) orelse (K == line) orelse (K == delimiter)],
TL = do_escape(Left, Q),
TR = do_escape(Right, Q),
TL = do_quote(Left, Q),
TR = do_quote(Right, Q),
{'{}', [], [TL, TM, TR]};
do_escape({Left, Right}, Q) ->
{do_escape(Left, Q), do_escape(Right, Q)};
{do_quote(Left, Q), do_quote(Right, Q)};
do_escape(Tuple, Q) when is_tuple(Tuple) ->
TT = do_escape(tuple_to_list(Tuple), Q),
TT = do_quote(tuple_to_list(Tuple), Q),
{'{}', [], TT};
do_escape(BitString, _) when is_bitstring(BitString) ->
@@ -206,7 +203,10 @@ do_escape(Map, Q) when is_map(Map) ->
end
else
_ ->
TT = [escape_map_key_value(K, V, Map, Q) || {K, V} <- lists:sort(maps:to_list(Map))],
TT = [
{do_quote(K, Q), do_quote(V, Q)}
|| {K, V} <- lists:sort(maps:to_list(Map))
],
{'%{}', [], TT}
end;
@@ -214,11 +214,7 @@ do_escape([], _) ->
[];
do_escape([H | T], #elixir_quote{unquote=false} = Q) ->
case is_list(T) of
true -> [do_escape(H, Q) | do_escape(T, Q)];
% improper list
false -> [{'|', [], [do_escape(H, Q), do_escape(T, Q)]}]
end;
do_quote_simple_list(T, do_escape(H, Q), Q);
do_escape([H | T], Q) ->
%% The improper case is inefficient, but improper lists are rare.
@@ -228,7 +224,7 @@ do_escape([H | T], Q) ->
_:_ ->
{L, R} = reverse_improper(T, [H]),
TL = do_quote_splice(L, Q, [], []),
TR = do_escape(R, Q),
TR = do_quote(R, Q),
update_last(TL, fun(X) -> {'|', [], [X, TR]} end)
end;
@@ -245,28 +241,6 @@ do_escape(Fun, _) when is_function(Fun) ->
do_escape(Other, _) ->
bad_escape(Other).
escape_map_key_value(K, V, Map, Q) ->
MaybeRef = if
is_reference(V) -> V;
is_tuple(V) -> find_tuple_ref(V, 1);
true -> nil
end,
if
is_reference(MaybeRef) ->
argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (",
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped",
(bad_escape_hint())/binary>>);
true ->
{do_escape(K, Q), do_escape(V, Q)}
end.
find_tuple_ref(Tuple, Index) when Index > tuple_size(Tuple) -> nil;
find_tuple_ref(Tuple, Index) ->
case element(Index, Tuple) of
Ref when is_reference(Ref) -> Ref;
_ -> find_tuple_ref(Tuple, Index + 1)
end.
bad_escape(Arg) ->
argument_error(<<"cannot escape ", ('Elixir.Kernel':inspect(Arg, []))/binary,
(bad_escape_hint())/binary>>).
@@ -383,7 +357,6 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
{'{}', [], [quote, meta(NewMeta, Q), [TOpts, TArg]]};
%
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, shallow_validate=Validate}) when is_list(Meta) ->
case Validate of
true -> {{'.', Meta, [?MODULE, shallow_validate_ast]}, Meta, [Expr]};
@@ -620,8 +593,10 @@ argument_error(Message) ->
%% Helpers
meta(Meta, #elixir_quote{op=quote} = Q) ->
generated(keep(keydelete(column, Meta), Q), Q);
meta(Meta, Q) ->
generated(keep(keydelete(column, Meta), Q), Q).
do_quote(Meta, Q).
generated(Meta, #elixir_quote{generated=true}) -> [{generated, true} | Meta];
generated(Meta, #elixir_quote{generated=false}) -> Meta.
+7 -6
View File
@@ -82,6 +82,9 @@ defmodule MacroTest do
contents = quote(unquote: false, do: unquote(x))
assert Macro.escape(contents, unquote: true) == {:x, [], MacroTest}
contents = %{foo: quote(unquote: false, do: unquote(1))}
assert Macro.escape(contents, unquote: true) == {:%{}, [], [foo: 1]}
end
test "with generated" do
@@ -97,6 +100,7 @@ defmodule MacroTest do
test "with remote unquote" do
contents = quote(unquote: false, do: Kernel.unquote(:is_atom)(:ok))
assert eval_escaped(contents) == quote(do: Kernel.is_atom(:ok))
assert eval_escaped(%{foo: contents}) == %{foo: quote(do: Kernel.is_atom(:ok))}
end
test "with nested unquote" do
@@ -140,6 +144,9 @@ defmodule MacroTest do
quote(unquote: false, do: [1, unquote_splicing([2]), 3, unquote_splicing([4]) | [5]])
assert eval_escaped(contents) == [1, 2, 3, 4, 5]
contents = %{foo: quote(unquote: false, do: [1, 2, unquote_splicing([3, 4, 5])])}
assert eval_escaped(contents) == %{foo: [1, 2, 3, 4, 5]}
end
test "does not add context to quote" do
@@ -154,12 +161,6 @@ defmodule MacroTest do
[:foo, {:{}, [], [:quote, [{:%{}, [], []}], [{:{}, [], []}]]}]
end
test "escape container when a reference cannot be escaped" do
assert_raise ArgumentError, ~r"contains a reference", fn ->
Macro.escape(%{re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})
end
end
@tag :re_import
test "escape regex will remove references and replace it by a call to :re.import/1" do
assert {
@@ -408,6 +408,8 @@ defmodule Module.Types.IntegrationTest do
dynamic(%Range{})
hint: defimpl for Range requires its callbacks to match exclusively on %Range{}
typing violation found at:
│
6 │ def itself(nil), do: nil
+3 -1
View File
@@ -80,7 +80,9 @@ defmodule IEx.Config do
end
def parser() do
Application.fetch_env!(:iex, :parser)
# Since the parser itself can be invoked from the remote node
# without IEx running, we cannot use fetch_env!
Application.get_env(:iex, :parser, {IEx.Evaluator, :parse, []})
end
def color(color) do
-1
View File
@@ -19,7 +19,6 @@ defmodule IEx.MixProject do
mod: {IEx.App, []},
env: [
colors: [],
parser: {IEx.Evaluator, :parse, []},
inspect: [pretty: true],
history_size: 20,
default_prompt: "%prefix(%counter)>",
+61 -8
View File
@@ -15,29 +15,66 @@ defmodule Mix.Tasks.Cmd do
$ mix cmd make
By passing the `--cd` flag before the command, you can also force
the command to run in a specific directory:
## Shell expansion
$ mix cmd --cd "third-party" make
When you invoke `mix cmd` from the command line, environment variables,
glob patterns, and similar are expanded by the current shell and then
passed to `mix cmd`. For example, if you invoke:
$ mix cmd echo lib/*
Your shell will expand "lib/*" and then pass multiple arguments to
`mix cmd`, which in turn passes them to `echo`. Note that, `mix cmd`
by itself, does not perform any shell expansion. This means that,
if you invoke `mix cmd` programatically, as in:
Mix.Task.run("cmd", ["echo", "lib/*"])
or through a `mix.exs` alias:
alias: "cmd echo lib/*"
It will literally print "lib/*".
This behaviour is the default from Elixir v1.19. If you want
`mix cmd` to behave like a shell, you can pass the `--shell`
option before the command name:
Mix.Task.run("cmd", ["--shell", "echo", "lib/*"])
Keep in mind however that, if `--shell` is given, quoted arguments
are no longer correctly propagated to the underlying command
(as they get expanded before hand).
This task is automatically re-enabled, so it can be called multiple times
with different arguments.
## Umbrella applications and directories
This task is also useful in umbrella applications to execute a command
on each child app:
$ mix cmd pwd
In such cases, Mix will change the current working directory to the root
of each umbrella application before executing the command.
You can limit which apps the cmd runs in by using `mix do` with the `--app`
option:
$ mix do --app app1 --app app2 cmd pwd
The tasks aborts whenever a command exits with a non-zero status.
Note the tasks aborts whenever a command exits with a non-zero status.
This task is automatically re-enabled, so it can be called multiple times
with different arguments.
Outside of umbrella projects, you can pass the `--cd` flag before the command,
to specify a directory:
$ mix cmd --cd "third-party" make
## Command line options
* `--cd` *(since v1.10.4)* - the directory to run the command in
* `--shell` *(since v1.19.0)* - perform shell expansion of the arguments
## Orphan operating system processes
@@ -54,7 +91,8 @@ defmodule Mix.Tasks.Cmd do
@switches [
app: :keep,
cd: :string
cd: :string,
shell: :boolean
]
@impl true
@@ -75,9 +113,24 @@ defmodule Mix.Tasks.Cmd do
end
if apps == [] or Mix.Project.config()[:app] in apps do
path = hd(args)
rest = tl(args)
arg =
cond do
Keyword.get(opts, :shell, false) ->
Enum.join([path | rest], " ")
String.contains?(path, ["/", "\\"]) and Path.type(path) != :absolute ->
{Path.expand(path, Keyword.get(opts, :cd, ".")), rest}
true ->
{path, rest}
end
cmd_opts = Keyword.take(opts, [:cd])
case Mix.shell().cmd({hd(args), tl(args)}, cmd_opts) do
case Mix.shell().cmd(arg, cmd_opts) do
0 -> :ok
status -> exit(status)
end
+21
View File
@@ -14,6 +14,27 @@ defmodule Mix.Tasks.CmdTest do
assert_received {:mix_shell, :run, ["hello world\n"]}
end
test "can be invoked as a shell" do
nl = os_newline()
Mix.Task.run("cmd", ["--shell", "echo", "hello"])
assert_received {:mix_shell, :run, ["hello" <> ^nl]}
end
@tag :unix
test "supports relative paths" do
in_tmp("cmd-relative", fn ->
File.mkdir_p!("priv")
File.write!("priv/world.sh", "#!/bin/sh\necho world")
File.chmod!("priv/world.sh", 0o755)
Mix.Task.run("cmd", ["priv/world.sh"])
assert_received {:mix_shell, :run, ["world\n"]}
Mix.Task.run("cmd", ["--cd", "priv", "./world.sh"])
assert_received {:mix_shell, :run, ["world\n"]}
end)
end
test "runs the command for each app" do
in_fixture("umbrella_dep/deps/umbrella", fn ->
Mix.Project.in_project(:umbrella, ".", fn _ ->