Compare commits

...
27 Commits
Author SHA1 Message Date
José Valim 545dddf138 Release v1.20.2 2026-06-23 14:02:47 +02:00
Łukasz Samson bc30e7d696 Include bitstring in domain keys (#15516)
Fixes #15515
2026-06-23 10:44:27 +02:00
José Valim 73130012bb Better organize tests 2026-06-21 21:36:40 +02:00
Łukasz Samson f55abe5ff9 Correctly render list with negative leaves (#15514)
BDD leaves represent non empty lists

Fixes #15513.
2026-06-21 21:34:37 +02:00
Łukasz Samson 394ab4bead Return error from compatible_intersection with none (#15512)
Match fast path behaviour with `compatible?/2`
Fixes #15511
2026-06-21 21:33:45 +02:00
Łukasz Samson f491a7eee4 Report empty_list: true on dynamic() in list_of (#15506)
Fixes #15505
2026-06-19 21:43:35 +02:00
José Valim a64284921e Fix optimized map difference with open key 2026-06-19 21:42:31 +02:00
Łukasz Samson 6bef18d523 Preserve optional static part on difference (#15503)
Fixes #15502
2026-06-18 09:22:29 +02:00
José Valim 8516737ea0 Avoid unecessary umbrella recompilation when a path dependency's manifest is newer but unchanged, closes #15454 2026-06-17 15:56:22 +02:00
José Valim c52eb2a323 opt_union -> union 2026-06-17 08:56:07 +02:00
Łukasz Samson 527fb26bcd Fix definition of term_or_dynamic_optional type (#15498)
Fixes #15497
2026-06-17 08:47:50 +02:00
José Valim 5c5909c2a1 Preserve empty list in dynamic list type constructor
Closes #15488.
2026-06-15 20:30:22 +02:00
José Valim 3c7ee9ff20 Handle empty function type in fun_apply
Closes #15486.
2026-06-15 18:48:29 +02:00
José Valim 51dce31a54 Preserve file meta from each clause in type system warnings 2026-06-15 16:03:50 +02:00
José Valim 0dd4cb4e0b Update CHANGELOG 2026-06-15 13:58:58 +02:00
Łukasz Samson 4d1de1d971 Fix missed optimization of Kernel.put_elem/3 (#15480)
?reorder added in 87582af54 used wrong arity and wrong erlang call
2026-06-15 12:49:18 +02:00
Jean Klingler b098c0406c Fix bin comprehensions when option is used (#15473)
* Fix bin comprehensions - static size

* Fix bin comprehensions - pinned var size

* Fix bin comprehensions - operations

* Add tests for nested generators
2026-06-15 17:34:06 +09:00
José Valim 1ba39f3501 Raise better error messages when expected structs disappear, closes #15472 2026-06-14 13:46:51 +02:00
Jean Klingler 0f9072a1d4 Cleaner error message for unquote when quote is used in a pattern (#15469) 2026-06-13 21:16:40 +09:00
José Valim 6dcc4cd2dc Simplify precision tracking in bitstrings 2026-06-13 12:11:32 +02:00
José Valim 40b6f012fe Fix env outdated deps compilation
We should only avoid cleaning for path dependencies.
Fetch dependencies should go through the previous process.

Closes #15467.
2026-06-13 12:11:31 +02:00
José Valim 68267627dc Track reused variables in bitstring patterns 2026-06-13 11:17:37 +02:00
José Valim 57766dd29b Use cyclic instead of recursive, add more examples, closes #15462 2026-06-12 21:01:29 +02:00
José Valim 1938d8a957 Ignore additional callbacks in protocol implementation, closes #15456 2026-06-12 21:01:28 +02:00
José Valim 3bebe1599a Log times to type check modules when profiling 2026-06-12 13:57:20 +02:00
Łukasz Samson 2beac77406 Add missing required key to __info__(:struct) type (#15453) 2026-06-10 16:41:24 +02:00
José Valim 85a23a1244 Document false positives, closes #15451 2026-06-10 09:27:42 +02:00
31 changed files with 672 additions and 172 deletions
+30
View File
@@ -192,6 +192,36 @@ It also introduces a new compiler option called `:module_definition`, which if t
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
## v1.20.2 (2026-06-23)
### 1. Enhancements
#### Elixir
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
### 2. Bug fixes
#### Elixir
* [Kernel] Fix binary comprehensions with sizes when options such as `:uniq` or `:into` are used
* [Kernel] Improve compiler error messages when `quote` with `unquote` is used inside a pattern or guard
* [Kernel] Restore the compiler optimization of `Kernel.put_elem/3` to emit `:erlang.setelement/3`
* [Module] Fix type checking when applying an empty function type
* [Module] Fix type checking of bitstring patterns that reuse variables
* [Module] Fix type information for `__info__(:struct)` to include the `:required` key
* [Module] Fix type operations on map and optional keys during difference/intersection, including open keys and empty intersections
* [Module] Fix typing of list types involving dynamic or empty lists
* [Module] Include bitstrings as a possible domain key in the type system
* [Module] Preserve file metadata from each clause in type system warnings
* [Module] Fix type warnings for protocol implementations whose protocol module defines additional callbacks
* [Module] Raise clearer type checking errors when an expected struct is removed or redefined during compilation
#### Mix
* [mix compile] Avoid unnecessary umbrella recompilation when a path dependency's manifest is newer but unchanged
* [mix deps.compile] Recompile fetched dependencies when their compile-time environment changes
## v1.20.1 (2026-06-09)
### 1. Security
+1 -1
View File
@@ -1 +1 @@
1.20.1
1.20.2
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.1
ELIXIR_VERSION=1.20.2
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.20.1
set ELIXIR_VERSION=1.20.2
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+2 -2
View File
@@ -197,8 +197,8 @@ defmodule Kernel.ParallelCompiler do
deprecation warnings, and type checking should run. Defaults to `true`.
We recommend disabling it only for debugging purposes.
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
and group pass checker
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
each module type check, and group pass checker
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
after compilation (see `Code.purge_compiler_modules/0`)
+27 -6
View File
@@ -20,6 +20,7 @@ defmodule Module.ParallelChecker do
{:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {:profile, :time}
| {atom(), term()}
]
@@ -117,13 +118,13 @@ defmodule Module.ParallelChecker do
send(checker, {ref, :cached})
receive do
{^ref, :check} ->
{^ref, :check, profile} ->
# Set the compiler info so we can collect warnings
:erlang.put(:elixir_compiler_info, {pid, self()})
{warnings, errors} =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
check_module(module_tuple, {checker, table}, log?, profile)
else
{[], []}
end
@@ -262,7 +263,7 @@ defmodule Module.ParallelChecker do
## Module checking
defp check_module(module_tuple, cache, log?) do
defp check_module(module_tuple, cache, log?, profile) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
module_tuple
@@ -276,9 +277,13 @@ defmodule Module.ParallelChecker do
definitions
)
type_warnings =
profile(module, profile, fn ->
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
end)
{warnings, errors} =
module
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
type_warnings
|> Kernel.++(behaviour_warnings)
|> group_diagnostics()
|> emit_diagnostics(file, log?)
@@ -577,6 +582,7 @@ defmodule Module.ParallelChecker do
schedulers: schedulers,
threshold: threshold,
callback: callback,
profile: Keyword.get(options, :profile),
protocols: [],
table: table
}
@@ -653,9 +659,24 @@ defmodule Module.ParallelChecker do
end
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
send(pid, {ref, :check})
send(pid, {ref, :check, state.profile})
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
spawned = Map.put(state.spawned, module, timer)
run_checkers(%{state | modules: modules, spawned: spawned})
end
defp profile(module, :time, fun) do
{time, result} = :timer.tc(fun)
IO.puts(
:stderr,
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
)
result
end
defp profile(_module, _profile, fun) do
fun.()
end
end
+12 -9
View File
@@ -136,8 +136,8 @@ defmodule Module.Types do
defp impl_for(attrs) do
case List.keyfind(attrs, :__impl__, 0) do
{:__impl__, [protocol: protocol, for: for]} ->
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
{for, protocol.behaviour_info(:callbacks)}
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
{for, protocol.__protocol__(:functions)}
else
nil
end
@@ -266,7 +266,7 @@ defmodule Module.Types do
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
else
@@ -299,11 +299,11 @@ defmodule Module.Types do
local_sigs ->
case finder.(fun_arity) do
{mode, {fun_arity, kind, meta, clauses}, expected} ->
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -316,9 +316,10 @@ defmodule Module.Types do
end
end
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
context =
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
stack = with_file_meta(stack, meta)
Module.Types.Traverse.of_expr(body, stack, context)
end)
@@ -326,9 +327,9 @@ defmodule Module.Types do
{inferred, [{0, 0}], context}
end
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
{fun, _arity} = fun_arity
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
stack = fresh_stack(stack, mode, fun_arity)
base_info = {:def, kind, fun, expected}
case clauses do
@@ -341,6 +342,7 @@ defmodule Module.Types do
end
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
stack = with_file_meta(stack, meta)
guards = []
previous = Pattern.init_previous()
fresh_context = fresh_context(context)
@@ -400,6 +402,7 @@ defmodule Module.Types do
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
{meta, args, guards, body},
{index, total, previous, domain, mapping, inferred, acc_context} ->
stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
+3 -1
View File
@@ -38,7 +38,9 @@ defmodule Module.Types.Apply do
end
fas = list(tuple([atom(), integer()]))
struct_info = list(closed_map(default: if_set(term()), field: atom()))
struct_info =
list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
shared_info = [
attributes: list(tuple([atom(), list(term())])),
+42 -6
View File
@@ -41,7 +41,7 @@ defmodule Module.Types.Descr do
defguardp fields_size(fields) when length(fields)
@domain_key_types :lists.sort(
[:binary, :integer, :float, :pid, :port, :reference] ++
[:binary, :bitstring, :integer, :float, :pid, :port, :reference] ++
[:fun, :atom, :tuple, :map, :list]
)
@@ -77,7 +77,7 @@ defmodule Module.Types.Descr do
@not_set %{optional: 1}
@term_or_optional Map.put(@term, :optional, 1)
@term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1})
@term_or_dynamic_optional Map.put(@term, :dynamic, @term_or_optional)
@not_atom_or_optional Map.delete(@term_or_optional, :atom)
@empty_intersection [0, :bdd_bot]
@@ -334,6 +334,7 @@ defmodule Module.Types.Descr do
keep_optional: 1, remove_optional: 1, remove_optional_static: 1, optional_to_term: 1}
defp keep_optional(descr) do
case descr do
%{dynamic: %{optional: 1}, optional: 1} -> %{dynamic: %{optional: 1}, optional: 1}
%{dynamic: %{optional: 1}} -> %{dynamic: %{optional: 1}}
%{optional: 1} -> %{optional: 1}
_ -> @none
@@ -978,7 +979,13 @@ defmodule Module.Types.Descr do
domain of a function. It is used to refine dynamic types
as we traverse the program.
"""
def compatible_intersection(other, :term), do: {:ok, remove_optional(other)}
def compatible_intersection(other, :term) do
if empty?(other) do
{:error, other}
else
{:ok, remove_optional(other)}
end
end
def compatible_intersection(left, right) do
{left_dynamic, left_static} = pop_dynamic(left)
@@ -1560,6 +1567,9 @@ defmodule Module.Types.Descr do
{:badarity, [arity | other]}
end
:badfun when fun_dynamic == nil ->
:badfun
:badfun ->
# No static arrows: dynamic-only path. Mixed-arity in the dynamic
# component is fine — we pick the matching-arity arrows and the
@@ -2159,6 +2169,9 @@ defmodule Module.Types.Descr do
not dynamic? ->
dynamic_descr
static_empty? and empty? ->
%{bitmap: @bit_empty_list, dynamic: dynamic_descr}
static_empty? ->
%{dynamic: dynamic_descr}
@@ -2281,7 +2294,7 @@ defmodule Module.Types.Descr do
{dynamic, static} ->
with {empty_list?, static_value} <- list_of_static(static) do
empty_list? =
empty_list? or
empty_list? or dynamic == :term or
match?(
%{bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0,
dynamic
@@ -2572,7 +2585,7 @@ defmodule Module.Types.Descr do
[to_quoted(ty, opts), to_quoted(lst, opts)]
end
{name, [], args}
{:non_empty_list, [], args}
end)
|> Kernel.then(
&[
@@ -3091,7 +3104,30 @@ defmodule Module.Types.Descr do
defp map_difference(bdd1, bdd2),
do: bdd_difference(bdd1, bdd2, &map_leaf_difference/3)
defp map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type) do
# This clause optimizes differences with an open single-key right side:
#
# %{b: atom(), c: pid()} \ %{..., c: if_set(pid() | binary())}
#
# Depending on the surrounding BDD shape, the difference formulas ask the
# leaf comparison for a direct difference plus an optional intersection
# or optional union.
#
# Because the right-hand side is an open map with a single key, this branch
# assumes the result can be represented by updating the matching key
# on the left-hand side. For this pair, the direct difference is empty and
# the intersection is `%{b: atom(), c: pid()}`, which are both correct.
#
# However, when the formula needs the union, this shortcut would produce
# `%{b: atom(), c: if_set(pid() or binary())}` by preserving the left shape.
# This is not the semantic union, because the right side also includes maps
# without `:b`. In other words, we cannot always union them by updating only
# the matching key.
#
# Therefore, this clause is only used when `type` is `:intersection` or `:none`.
# `:union` falls through to the general clause below. The reason we have
# this long comment is because this was a regression in the past.
defp map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type)
when type != :union do
{found?, v1} =
case fields_find(key, fields) do
{:ok, value} -> {true, value}
-1
View File
@@ -641,7 +641,6 @@ defmodule Module.Types.Expr do
end)
end
# TODO: with pat <- expr do expr end
def of_expr({:with, meta, [_ | _] = clauses}, expected, _expr, stack, original) do
cache_result(meta, stack, original, fn ->
{clauses, [[do: do_block] ++ options]} = Enum.split(clauses, -1)
+35 -28
View File
@@ -467,32 +467,32 @@ defmodule Module.Types.Of do
# TODO: Type check the fields match the struct
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
when is_atom(struct) do
{info, context} = struct_info(struct, :expr, meta, stack, context)
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
if is_nil(info) do
raise "expected #{inspect(struct)} to return struct metadata, but got none"
{dynamic(), context}
else
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, kind, meta, stack, context) do
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
case stack.no_warn_undefined do
%Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
@@ -511,7 +511,7 @@ defmodule Module.Types.Of do
{info, context}
else
error = {:unknown_struct, kind, struct}
error = {:unknown_struct, kind, struct, must_exist?}
{nil, error(error, meta, stack, context)}
end
end
@@ -848,19 +848,26 @@ defmodule Module.Types.Of do
}
end
def format_diagnostic({:unknown_struct, kind, module}) do
message =
if Code.ensure_loaded?(module) do
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
else
"struct #{inspect(module)} is undefined " <>
"(module #{inspect(module)} is not available or is yet to be defined)"
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
detail =
case {Code.ensure_loaded?(module), must_exist?} do
{true, false} ->
"there is such module but it does not define a struct"
{false, false} ->
"module #{inspect(module)} is not available or is yet to be defined"
{true, true} ->
"the module may have been redefined as it no longer defines a struct"
{false, true} ->
"the module was also only available but may have been removed during compilation"
end
%{
message: message,
message: "struct #{inspect(module)} is undefined (#{detail})",
group: true,
severity: if(kind == :pattern, do: :error, else: :warning)
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
}
end
+12 -4
View File
@@ -786,8 +786,9 @@ defmodule Module.Types.Pattern do
# <<...>>>
defp of_pattern({:<<>>, _meta, args} = node, _path, stack, context) do
precise? = of_precise_bitstring?(node, context.vars)
{type, context} = Of.bitstring(args, :match, stack, context)
{type, of_precise_bitstring?(node), context}
{type, precise?, context}
end
# left ++ right
@@ -987,12 +988,19 @@ defmodule Module.Types.Pattern do
{type, precise?, put_in(context.subpatterns[key], of_pattern_tree(type, stack, context))}
end
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]})
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]}, vars)
when type in [:binary, :bitstring, :bytes, :bits] do
is_var(expr) or of_precise_bitstring?(expr)
new_var?(expr, vars) or of_precise_bitstring?(expr, vars)
end
defp of_precise_bitstring?(_), do: false
defp of_precise_bitstring?(_expr, _vars), do: false
defp new_var?({_, meta, _} = var, vars) when is_var(var) do
version = Keyword.fetch!(meta, :version)
not is_map_key(vars, version)
end
defp new_var?(_expr, _vars), do: false
## Guards
#
@@ -231,11 +231,47 @@ 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, while calls to modules within the same project are assumed to be `dynamic()`. 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, Elixir aims to provide type inference across dependencies: our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
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 and keeping the expressiveness of the language.
In the long term, Elixir developers who want static 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.
In the long term, Elixir developers who want static typing guarantees may 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.
### False positives
Elixir's type inference generally avoids emitting false positive type violations: which are warnings emitted by the type checker when there are no runtime errors. However, in some situations, those may happen and are documented below.
#### `for`-comprehensions assume they are executed at least once
For comprehensions in Elixir assume they are executed at least once. Take this code:
```elixir
def example(x, list) do
for _i <- list do
Atom.to_string(x)
end
x + 1
end
```
`x + 1` will fail because it assumes `x` is an atom from the `Atom.to_string(x)` call, even though the function may raise no runtime error if `list` is an empty list. This is intentional, as it helps find discrepancies inside and outside comprehensions. You can address this by explicitly wrapping the comprehension in a `if list != [] do` block (or similar condition).
#### Struct update syntax must be statically proven
Elixir will warn if you use the struct update syntax and it is not statically proven that the given value does not have said struct type. For example:
```elixir
user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
Even though it is guaranteed at runtime that user is always a `User` struct. If the type system cannot prove it, it will emit a typing violation. This is how stuct updates work by design. In such cases, you can address it by matching on the struct when the user variable is defined:
```elixir
%User{} = user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
## Roadmap
@@ -72,6 +72,15 @@ iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
```
However, a variable cannot be defined through itself in the same pattern. This includes cases where the variable appears on both sides of a nested match operator:
```elixir
{:ok, x} = {x, :ok}
%{a: value} = %{b: value}
```
Such patterns are rejected as cyclic variable definitions. You can rewrite them by matching once and then using guards or separate comparisons for the additional constraints.
The underscore variable (`_`) has a special meaning as it can never be bound to any value. It is especially useful when you don't care about certain value in a pattern:
```iex
+3 -3
View File
@@ -562,16 +562,16 @@ format_error({recursive, Vars, TypeExpr}) ->
Message =
case lists:map(fun({Name, Context}) -> elixir_utils:var_info(Name, Context) end, lists:sort(Vars)) of
[Var] ->
io_lib:format("the variable ~ts is defined in function of itself", [Var]);
io_lib:format("the variable ~ts depends on itself through the pattern", [Var]);
[Var1, Var2] ->
io_lib:format("the variable ~ts is defined recursively in function of ~ts", [Var1, Var2]);
io_lib:format("the variable ~ts depends on ~ts through the pattern", [Var1, Var2]);
[Head | Tail] ->
List = lists:foldl(fun(X, Acc) -> [Acc, $,, $\s, X] end, Head, Tail),
io_lib:format("the following variables form a cycle: ~ts", [List])
end,
io_lib:format(
"recursive variable definition in patterns:~n~n~ts~n~n~ts",
"cyclic variable definition in patterns:~n~n~ts~n~n~ts",
[Code, Message]
);
+50 -38
View File
@@ -14,12 +14,15 @@
translate(Meta, Args, S) ->
{Cases, [{do, Expr} | Opts]} = elixir_utils:split_last(Args),
% needs to be the original variables, excluding variables from generators
InitVars = S#elixir_erl.var_names,
case lists:keyfind(reduce, 1, Opts) of
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, S);
false -> translate_into(Meta, Cases, Expr, Opts, S)
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S);
false -> translate_into(Meta, Cases, Expr, Opts, InitVars, S)
end.
translate_reduce(Meta, Cases, Expr, Reduce, S) ->
translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S) ->
Ann = ?ann(Meta),
{TReduce, SR} = elixir_erl_pass:translate(Reduce, Ann, S),
{TCases, SC} = translate_gen(Meta, Cases, [], SR),
@@ -30,9 +33,9 @@ translate_reduce(Meta, Cases, Expr, Reduce, S) ->
({'case', CaseAnn, _, CaseBlock}, InnerAcc) -> {'case', CaseAnn, InnerAcc, CaseBlock}
end,
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, SE).
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, InitVars, SE).
translate_into(Meta, Cases, Expr, Opts, S) ->
translate_into(Meta, Cases, Expr, Opts, InitVars, S) ->
Ann = ?ann(Meta),
{TInto, SI} =
@@ -47,8 +50,8 @@ translate_into(Meta, Cases, Expr, Opts, S) ->
{TExpr, SE} = elixir_erl_pass:translate(wrap_expr_if_unused(Expr, TInto), Ann, SC),
case inline_or_into(TInto) of
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, SE);
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, SE)
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE);
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE)
end.
%% In case we have no return, we wrap the expression
@@ -115,29 +118,29 @@ collect_filters([H | T], Acc) ->
collect_filters([], Acc) ->
{Acc, []}.
build_inline(Ann, Clauses, Expr, Into, Uniq, S) ->
build_inline(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
case not Uniq and lists:all(fun(Clause) -> element(1, Clause) == bin end, Clauses) of
true -> {build_comprehension(Ann, Clauses, Expr, Into), S};
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, S)
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, InitVars, S)
end.
build_inline_each(Ann, Clauses, Expr, false, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, false, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, _InnerAcc) -> InnerExpr end,
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S);
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, S) ->
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S);
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, _InitVars, S) ->
Clauses = [{clause, Ann, [Left], [], [Expr]}],
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
{?remote(Ann, 'Elixir.Enum', map, Args), S};
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, S) ->
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, _InitVars, S) ->
Clauses = [{clause, Ann, [Left], [], [Expr]}],
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
List = ?remote(Ann, 'Elixir.Enum', map, Args),
{?remote(Ann, maps, from_list, [List]), S};
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, S),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, InitVars, S),
{?remote(Ann, lists, reverse, [ReduceExpr]), SR};
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, InitVars, S) ->
{InnerValue, SV} = build_var(Ann, S),
Generated = erl_anno:set_generated(true, Ann),
@@ -155,17 +158,17 @@ build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
]}
end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, SV),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, SV),
{?remote(Ann, erlang, list_to_bitstring, [ReduceExpr]), SR}.
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, S) ->
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, S),
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, InitVars, S) ->
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, InitVars, S),
{?remote(Ann, maps, from_list, [ReduceExpr]), SR};
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, S) ->
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S),
{?remote(Ann, 'Elixir.MapSet', new, [ReduceExpr]), SR};
build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
build_into(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
{Fun, SF} = build_var(Ann, S),
{Acc, SA} = build_var(Ann, SF),
{Kind, SK} = build_var(Ann, SA),
@@ -182,7 +185,7 @@ build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
?remote(Ann, 'Elixir.Collectable', into, [Into])
},
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, SD),
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, InitVars, SD),
TryExpr =
{'try', Ann,
@@ -205,10 +208,10 @@ stacktrace_clause(Ann, Fun, Acc, Kind, Reason, Stack) ->
%% Helpers
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, S) ->
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, InitVars, S) ->
{Acc, SA} = build_var(Ann, S),
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, SA), SA};
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, InitVars, SA), SA};
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, InitVars, S) ->
%% Those variables are used only inside the anonymous function
%% so we don't need to worry about returning the scope.
{Acc, SA} = build_var(Ann, S),
@@ -229,12 +232,12 @@ build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
]}
]},
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, SU),
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, InitVars, SU),
{?remote(Ann, erlang, element, [{integer, Ann, 1}, EnumReduceCall]), SU}.
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
Ann = ?ann(Meta),
True = build_reduce_each(T, Expr, Acc, Acc, S),
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, S),
False = Acc,
Generated = erl_anno:set_generated(true, Ann),
@@ -255,13 +258,13 @@ build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) -
Args = [Right, Arg, {'fun', Ann, {clauses, Clauses1}}],
?remote(Ann, 'Elixir.Enum', reduce, Args);
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
Ann = ?ann(Meta),
Generated = erl_anno:set_generated(true, Ann),
{Tail, ST} = build_var(Ann, S),
{Fun, SF} = build_var(Ann, ST),
True = build_reduce_each(T, Expr, Acc, Acc, SF),
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, SF),
False = Acc,
{bin, _, Elements} = Left,
TailElement = {bin_element, Ann, Tail, default, [bitstring]},
@@ -275,7 +278,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
[?remote(Ann, erlang, error, [pair(Ann, badarg, Tail)])]}],
NoVarClauses =
case no_var(Generated, Elements) of
case no_unbound_var(Generated, Elements, InitVars) of
error ->
Clauses;
@@ -294,7 +297,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
{named_fun, Ann, element(3, Fun), VarClauses},
[Right, Arg]};
build_reduce_each([], Expr, _Arg, _Acc, _S) ->
build_reduce_each([], Expr, _Arg, _Acc, _InitVars, _S) ->
Expr.
is_var({var, _, _}) -> true;
@@ -307,9 +310,10 @@ build_var(Ann, S) ->
{Name, ST} = elixir_erl_var:build('_', S),
{{var, Ann, Name}, ST}.
no_var(ParentAnn, Elements) ->
no_unbound_var(ParentAnn, Elements, InitVars) ->
Vars = #{V => K || K := V <- InitVars},
try
[{bin_element, Ann, NoVarExpr, no_var_size(Size), Types} ||
[{bin_element, Ann, NoVarExpr, no_unbound_var_size(Size, Vars), Types} ||
{bin_element, Ann, Expr, Size, Types} <- Elements,
NoVarExpr <- no_var_expr(ParentAnn, Expr)]
catch
@@ -319,9 +323,17 @@ no_var(ParentAnn, Elements) ->
no_var_expr(Ann, {string, _, String}) -> [{var, Ann, '_'} || _ <- String];
no_var_expr(Ann, _) -> [{var, Ann, '_'}].
no_var_size(default) -> default;
no_var_size(Size) when is_integer(Size) -> Size;
no_var_size(_) -> throw(unbound_size).
no_unbound_var_size(Size, Vars) ->
valid_var_size(Size, Vars) orelse throw(unbound_size),
Size.
valid_var_size({var, _, Var}, Vars) when is_map_key(Var, Vars) -> true;
valid_var_size(default, _Vars) -> true;
valid_var_size({integer, _, _}, _Vars) -> true;
valid_var_size(Size, _Vars) when is_integer(Size) -> true;
valid_var_size({op, _Ann, _Op, Left, Right}, Vars) ->
valid_var_size(Left, Vars) andalso valid_var_size(Right, Vars);
valid_var_size(_Size, _vars) -> false.
build_comprehension(Ann, Clauses, Expr, Into) ->
{comprehension_kind(Into), Ann, Expr, comprehension_clause(Clauses)}.
+1 -1
View File
@@ -717,7 +717,7 @@ rewrite_strategy(Left, Right, Args) ->
).
?reorder('Elixir.Kernel', elem, 2, [Tuple, Index], erlang, element, [increment(Ann, Index), Tuple]);
?reorder('Elixir.Kernel', put_elem, 2, [Tuple, Index, Term], erlang, element, [increment(Ann, Index), Tuple, Term]);
?reorder('Elixir.Kernel', put_elem, 3, [Tuple, Index, Term], erlang, setelement, [increment(Ann, Index), Tuple, Term]);
?reorder('Elixir.Kernel', is_map_key, 2, [Map, Key], erlang, is_map_key, [Key, Map]);
?reorder('Elixir.Map', delete, 2, [Map, Key], maps, remove, [Key, Map]);
?reorder('Elixir.Map', fetch, 2, [Map, Key], maps, find, [Key, Map]);
+5
View File
@@ -266,6 +266,9 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote),
Generated = proplists:get_value(generated, EOpts, false),
(map_get(context, E) /= nil) andalso Unquote andalso elixir_quote:has_unquotes(Exprs) andalso
file_error(Meta, E, ?MODULE, quote_in_pattern_with_unquote),
{Q, QContext, QPrelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated, ET),
{EPrelude, SP, EP} = expand(QPrelude, ST, ET),
{EContext, SC, EC} = expand(QContext, SP, EP),
@@ -1216,6 +1219,8 @@ format_error({expected_compile_time_module, Kind, GivenTerm}) ->
format_error({unquote_outside_quote, Unquote}) ->
%% Unquote can be "unquote" or "unquote_splicing".
io_lib:format("~p called outside quote", [Unquote]);
format_error(quote_in_pattern_with_unquote) ->
"unquote is not allowed when quote is used inside a pattern or guard";
format_error({invalid_bind_quoted_for_quote, BQ}) ->
io_lib:format("invalid :bind_quoted for quote, expected a keyword list of variable names, got: ~ts",
['Elixir.Macro':to_string(BQ)]);
@@ -520,6 +520,33 @@ defmodule Kernel.ComprehensionTest do
assert for(<<s, x::size(s * 8) <- bin>>, into: %{}, do: {s, x}) == %{1 => 1, 2 => 515}
end
test "binary for comprehensions with chunk matching" do
bin = <<0, 1, 255, 2, 0, 3, 0, 1>>
# static sizes
assert for(<<0::8, x::8 <- bin>>, do: x) == [1, 3, 1]
assert for(<<0::8, x::8 <- bin>>, uniq: true, do: x) == [1, 3]
assert for(<<0::8, x::8 <- bin>>, into: "", do: <<x>>) == <<1, 3, 1>>
assert for(<<0::8, x::8 <- bin>>, into: %{}, do: {x, x}) == %{1 => 1, 3 => 3}
# size from pinned variable
s = 8
assert for(<<0::size(^s), x::size(^s) <- bin>>, do: x) == [1, 3, 1]
assert for(<<0::size(^s), x::size(^s) <- bin>>, uniq: true, do: x) == [1, 3]
assert for(<<0::size(^s), x::size(^s) <- bin>>, into: "", do: <<x>>) == <<1, 3, 1>>
assert for(<<0::size(^s), x::size(^s) <- bin>>, into: %{}, do: {x, x}) == %{1 => 1, 3 => 3}
# operation using fixed integers and pinned variables
assert for(<<0::size(^s * 1), x::size(^s * 1) <- bin>>, do: x) == [1, 3, 1]
assert for(<<0::size(^s * 1), x::size(^s * 1) <- bin>>, uniq: true, do: x) == [1, 3]
assert for(<<0::size(^s * 1), x::size(^s * 1) <- bin>>, into: "", do: <<x>>) == <<1, 3, 1>>
# nested generators
assert for(b <- [bin], <<0::size(^s), x::size(^s) <- b>>, do: x) == [1, 3, 1]
assert for(b <- [bin], <<0::size(^s), x::size(^s) <- b>>, uniq: true, do: x) == [1, 3]
assert for(b <- [bin], <<0::size(^s), x::size(^s) <- b>>, into: "", do: <<x>>) == <<1, 3, 1>>
end
test "binary for comprehensions where value is not used" do
bin = <<1, 2, 3>>
+17 -2
View File
@@ -196,11 +196,11 @@ defmodule Kernel.ErrorsTest do
)
end
test "recursive variables on definition" do
test "cyclic variables on definition" do
assert_compile_error(
[
"nofile:2:7: ",
"recursive variable definition in patterns:",
"cyclic variable definition in patterns:",
"foo(x = y, y = z, z = x)",
"the following variables form a cycle: \"x\", \"y\", \"z\""
],
@@ -456,6 +456,21 @@ defmodule Kernel.ErrorsTest do
"""
end
test "invalid unquote when quote/1 is in a pattern" do
assert_compile_error(
["unquote is not allowed when quote is used inside a pattern or guard"],
~c"""
defmodule Kernel.ErrorsTest.InvalidUnquoteInQuotePattern do
def my_fun(ast) do
case ast do
quote(do: foo(unquote(x))) -> x
end
end
end
"""
)
end
test "invalid attribute" do
msg = ~r"cannot inject attribute @foo into function/macro because cannot escape "
@@ -199,43 +199,43 @@ defmodule Kernel.ExpansionTest do
assert Macro.Env.vars(env) == []
end
test "errors on directly recursive definitions" do
test "errors on directly cyclic definitions" do
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{x = \{:ok, x\}\}
the variable "x" \(context Kernel.ExpansionTest\) is defined in function of itself
the variable "x" \(context Kernel.ExpansionTest\) depends on itself through the pattern
""",
fn -> expand(quote(do: {x = {:ok, x}} = :ok)) end
)
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{\{x, y\} = \{y, x\}\}
the variable "x" \(context Kernel.ExpansionTest\) is defined in function of itself
the variable "x" \(context Kernel.ExpansionTest\) depends on itself through the pattern
""",
fn -> expand(quote(do: {{x, y} = {y, x}} = :ok)) end
)
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{\{:x, y\} = \{x, :y\}, x = y\}
the variable "x" \(context Kernel.ExpansionTest\) is defined recursively in function of "y" \(context Kernel.ExpansionTest\)
the variable "x" \(context Kernel.ExpansionTest\) depends on "y" \(context Kernel.ExpansionTest\) through the pattern
""",
fn -> expand(quote(do: {{:x, y} = {x, :y}, x = y} = :ok)) end
)
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{x = y, y = z, z = x\}
@@ -245,7 +245,7 @@ defmodule Kernel.ExpansionTest do
)
end
test "complex recursive variable definitions" do
test "complex cyclic variable definitions" do
assert expand(
quote do:
{%{type: type, client_id: client_id} = message,
@@ -253,7 +253,7 @@ defmodule Kernel.ExpansionTest do
)
assert_compile_error(
~r"recursive variable definition in patterns",
~r"cyclic variable definition in patterns",
fn ->
expand(
quote do:
@@ -57,6 +57,7 @@ defmodule Kernel.ParallelCompilerTest do
~r"\[profile\] [\s\d]{6}ms compiling \+ 0ms waiting while compiling .*tmp/profile_time/bar.ex"
assert profile =~ ~r"\[profile\] Finished compilation cycle of 1 modules in \d+ms"
assert profile =~ ~r"\[profile\] Type checked HelloWorld in \d+ms"
assert profile =~ ~r"\[profile\] Finished group pass check of 1 modules in \d+ms"
after
purge([HelloWorld])
@@ -104,6 +104,14 @@ defmodule Module.Types.DescrTest do
refute equal?(union(term(), dynamic(if_set(integer()))), dynamic(union(term(), not_set())))
end
test "optional" do
# Test that union preserves optional properties
t = union(term(), dynamic(not_set()))
assert subtype?(term(), t)
assert subtype?(dynamic(), t)
assert subtype?(dynamic(integer()), t)
end
test "tuple" do
assert equal?(union(tuple(), tuple()), tuple())
@@ -585,6 +593,14 @@ defmodule Module.Types.DescrTest do
assert empty?(difference(dynamic(integer()), integer()))
end
test "optional" do
# Test that difference preserves optional properties
x = if_set(dynamic(integer()))
term_map = union(integer(), difference(term(), integer()))
assert equal?(term_map, term())
assert equal?(difference(x, term()), difference(x, term_map))
end
test "tuple" do
assert empty?(difference(open_tuple([atom()]), open_tuple([term()])))
refute empty?(difference(tuple(), empty_tuple()))
@@ -674,6 +690,27 @@ defmodule Module.Types.DescrTest do
assert difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none()
end
test "map difference of subtype" do
for {k1, k2} <- [{:a, :b}, {:b, :a}, {:b, :c}, {:c, :a}, {:c, :b}, {:b, :d}] do
a = closed_map([{k1, atom([:x, :y])}, {k2, pid()}])
b =
closed_map([
{k1, if_set(union(atom([:y]), float()))},
{k2, union(pid(), union(binary(), integer()))}
])
c = open_map([{k2, if_set(union(pid(), binary()))}])
# a <= c, while b is unrelated to both a and c.
# Therefore (a \ b) <= a <= c and (a \ b) \ c is empty.
ab = difference(a, b)
assert empty?(difference(ab, c)),
"difference failed for keys #{inspect({k1, k2})}"
end
end
test "map double negation with redundant empty map" do
type = closed_map(a: atom()) |> union(open_map(a: if_set(integer()))) |> union(empty_map())
@@ -893,6 +930,7 @@ defmodule Module.Types.DescrTest do
{closed_map([{domain_key(:integer), gradual}]),
closed_map([{domain_key(:integer), static}]),
closed_map([{domain_key(:integer), upper_bound}])},
{list(dynamic()), empty_list(), list(term())},
{non_empty_list(gradual), non_empty_list(static), non_empty_list(upper_bound)},
{non_empty_list(head, gradual), non_empty_list(head, static),
non_empty_list(head, upper_bound)}
@@ -963,6 +1001,13 @@ defmodule Module.Types.DescrTest do
assert subtype?(t2, t1)
# An open map is open in every domain key, including :bitstring.
open_pid = open_map([{domain_key(:pid), pid()}])
for d <- [:bitstring, :binary, :integer, :float, :atom, :tuple, :map, :list] do
assert subtype?(closed_map([{[d], atom([:x])}]), open_pid)
end
t1_minus_t2 = difference(t1, t2)
refute empty?(t1_minus_t2)
@@ -1092,6 +1137,21 @@ defmodule Module.Types.DescrTest do
end
end
describe "compatible_intersection" do
test "none" do
refute compatible?(none(), term())
assert compatible_intersection(none(), term()) == {:error, none()}
refute compatible?(dynamic(none()), term())
assert compatible_intersection(dynamic(none()), term()) == {:error, dynamic(none())}
end
test "dynamic" do
assert compatible_intersection(integer(), term()) == {:ok, integer()}
assert compatible_intersection(dynamic(integer()), term()) == {:ok, dynamic(integer())}
end
end
describe "empty?" do
test "tuple" do
assert tuple([none()]) |> empty?()
@@ -1197,6 +1257,7 @@ defmodule Module.Types.DescrTest do
test "non funs" do
assert fun_apply(term(), [integer()]) == :badfun
assert fun_apply(integer(), [integer()]) == :badfun
assert fun_apply(none(), [integer()]) == :badfun
assert fun_apply(union(integer(), none_fun(1)), [integer()]) == :badfun
assert fun_apply(union(integer(), fun([integer()], atom())), [integer()]) == :badfun
assert fun_apply(union(integer(), dynamic()), [integer()]) == :badfun
@@ -1656,6 +1717,7 @@ defmodule Module.Types.DescrTest do
assert list_of(non_empty_list(integer(), list(term()))) == {false, term()}
assert list_of(list(integer()) |> union(list(integer(), integer()))) == :badproperlist
assert list_of(list(integer()) |> union(integer())) == :badproperlist
assert list_of(dynamic()) == {true, dynamic()}
assert list_of(dynamic(list(integer()))) == {true, dynamic(integer())}
assert list_of(dynamic(list(integer(), atom()))) == {true, nil}
assert list_of(dynamic(non_empty_list(integer(), atom()))) == :badproperlist
@@ -2114,7 +2176,7 @@ defmodule Module.Types.DescrTest do
assert list(
Enum.reduce(
[binary(), float(), pid(), port(), reference()] ++
[binary(), bitstring_no_binary(), float(), pid(), port(), reference()] ++
[fun(), atom(), tuple(), open_map(), list(term(), term())],
tuple([integer(), binary()]),
fn domain, acc -> union(acc, tuple([domain, term()])) end
@@ -3219,6 +3281,9 @@ defmodule Module.Types.DescrTest do
assert list(term()) |> difference(list(integer())) |> to_quoted_string() ==
"non_empty_list(term()) and not non_empty_list(integer())"
assert difference(list(term()), non_empty_list(integer())) |> to_quoted_string() ==
"list(term()) and not non_empty_list(integer())"
assert list(term())
|> difference(list(integer()))
|> difference(list(atom()))
@@ -3318,10 +3318,15 @@ defmodule Module.Types.ExprTest do
assert typecheck!(GenServer.__info__(:struct)) == atom([nil])
assert typecheck!(URI.__info__(:struct)) ==
list(closed_map(default: if_set(term()), field: atom()))
list(
closed_map(default: if_set(term()), field: atom(), required: if_set(boolean()))
)
assert typecheck!([x], x.__info__(:struct)) ==
list(closed_map(default: if_set(term()), field: atom())) |> union(atom([nil]))
list(
closed_map(default: if_set(term()), field: atom(), required: if_set(boolean()))
)
|> union(atom([nil]))
end
test "behaviour_info/1" do
@@ -154,6 +154,50 @@ defmodule Module.Types.IntegrationTest do
assert itself_arg.(Itself.Unknown) == dynamic(open_map(__struct__: atom([Unknown])))
end
test "ignores additional callbacks on implementations" do
files = %{
"p.ex" => """
defmodule InjectCallback do
defmacro __before_compile__(_env) do
quote do
@callback extra() :: term()
end
end
end
defprotocol Injected do
@before_compile InjectCallback
def f(x)
end
defimpl Injected, for: Atom do
def f(_), do: :ok
def extra(), do: :extra
end
defprotocol Explicit do
@callback extra() :: term()
def f(x)
end
defimpl Explicit, for: Atom do
def f(_), do: :ok
def extra(), do: :extra
end
"""
}
assert capture_compile_warnings(files, []) == """
warning: cannot define @callback extra/0 inside protocol, use def/1 to outline your protocol definition
│
20 │ @callback extra() :: term()
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
│
└─ p.ex:20: Explicit (module)
"""
end
end
describe "type checking" do
@@ -289,6 +333,39 @@ defmodule Module.Types.IntegrationTest do
assert_warnings(files, warnings)
end
test "warning location respects file metadata in clauses" do
files = %{
"bug.ex" => """
defmodule Bug do
def render(x)
@file "template.heex"
def render(x) do
label = describe(x)
_ = label == :only
x
end
defp describe(_), do: "page"
end
"""
}
warnings = [
"""
warning: comparison between distinct types found:
label == :only
""",
"binary() == :only",
"# type: binary()",
"# from: template.heex:6:11",
"└─ template.heex:7:15: Bug.render/1"
]
assert_warnings(files, warnings)
end
test "unused private clauses" do
files = %{
"a.ex" => """
@@ -705,6 +782,44 @@ defmodule Module.Types.IntegrationTest do
purge(A)
end
test "does not crash on redefined module with newly added struct" do
Code.compile_string("""
defmodule RedefinedNestedStruct.Inner do
def foo(), do: :nothing
end
""")
files = %{
"redefined.ex" => """
defmodule RedefinedNestedStruct.Inner do
defstruct [:value]
def foo(), do: %__MODULE__{value: 1}
end
"""
}
in_tmp(fn ->
paths = generate_files(files)
{result, _stderr} =
with_io(:stderr, fn ->
Kernel.ParallelCompiler.compile_to_path(paths, ".", return_diagnostics: true)
end)
assert {:error, errors, %{compile_warnings: warnings, runtime_warnings: []}} = result
assert [%{message: "struct RedefinedNestedStruct.Inner is undefined " <> _}] = errors
assert Enum.any?(
warnings,
&(&1.message =~ "redefining module RedefinedNestedStruct.Inner")
)
end)
after
purge(RedefinedNestedStruct.Inner)
end
@tag :require_ast
test "regressions" do
files = %{
@@ -1370,6 +1370,14 @@ defmodule Module.Types.PatternTest do
refute precise?([<<_::binary-size(8)>>])
refute precise?([<<_::bitstring-size(8)>>])
refute precise?([<<(<<123>>)::bits>>])
refute precise?([x, <<x::binary>>])
refute precise?([x, <<x>>])
refute precise?([x, <<y, x>>])
refute precise?([x, <<(<<x::binary>>)::binary>>])
refute precise?([<<x::binary>>, x])
refute precise?([<<x>>, x])
refute precise?([<<y, x>>, x])
refute precise?([<<(<<x::binary>>)::binary>>, x])
end
test "tuples in patterns" do
+37 -38
View File
@@ -658,6 +658,20 @@ defmodule Mix.Compilers.Elixir do
reduce: {stale_modules, stale_modules, deps_exports, protocols_and_impls()} do
{modules, exports, deps_exports, protocols_and_impls} ->
{manifest_modules, manifest_sources} = read_manifest(manifest)
all_modules = Enum.map(manifest_modules, fn {mod, _} -> mod end)
dep_exports = Map.get(deps_exports, app, %{})
# Update modules, exports, and dep exports based on removed modules
{modules, exports, dep_exports} =
Enum.reduce(dep_exports, {modules, exports, dep_exports}, fn
{mod, _}, {modules, exports, dep_exports} ->
if mod in all_modules do
{modules, exports, dep_exports}
else
{Map.put(modules, mod, true), Map.put(exports, mod, true),
Map.delete(dep_exports, mod)}
end
end)
dep_modules =
for {module, module(timestamp: timestamp)} <- manifest_modules,
@@ -671,47 +685,32 @@ defmodule Mix.Compilers.Elixir do
dep_modules =
fixpoint_non_compile_modules(manifest_sources, Map.from_keys(dep_modules, true))
old_exports = Map.get(deps_exports, app, %{})
{exports, dep_exports} =
Enum.reduce(dep_modules, {exports, dep_exports}, fn {mod, _}, {exports, dep_exports} ->
export =
if Code.ensure_loaded?(mod) and function_exported?(mod, :__info__, 1) do
mod.__info__(:exports_md5)
end
# Update exports
{exports, new_exports} =
for {module, _} <- dep_modules, reduce: {exports, []} do
{exports, new_exports} ->
export =
if Code.ensure_loaded?(module) and function_exported?(module, :__info__, 1) do
module.__info__(:exports_md5)
end
# If the exports are the same, then the API did not change,
# so we do not mark the export as stale. Note this has to
# be very conservative. If the module is not loaded or if
# the exports were not there, we need to consider it a stale export.
case dep_exports do
# They still match, nothing to change
%{^mod => ^export} ->
{exports, dep_exports}
# If the exports are the same, then the API did not change,
# so we do not mark the export as stale. Note this has to
# be very conservative. If the module is not loaded or if
# the exports were not there, we need to consider it a stale
# export.
exports =
if export && old_exports[module] == export,
do: exports,
else: Map.put(exports, module, true)
# Now, it either matched and they are different, which means there was
# an export at some point OR it didn't exist before, which means adding
# it as an export is likely a no-op anyway
_ ->
{Map.put(exports, mod, true), Map.put(dep_exports, mod, export)}
end
end)
# Then we store the new export if any
new_exports =
if export,
do: [{module, export} | new_exports],
else: new_exports
{exports, new_exports}
end
new_exports = Map.new(new_exports)
removed =
for {module, _} <- old_exports,
not is_map_key(new_exports, module),
do: {module, true},
into: %{}
modules = modules |> Map.merge(dep_modules) |> Map.merge(removed)
exports = Map.merge(exports, removed)
deps_exports = Map.put(deps_exports, app, new_exports)
modules = Map.merge(modules, dep_modules)
deps_exports = Map.put(deps_exports, app, dep_exports)
protocols_and_impls =
protocols_and_impls_from_modules(manifest_modules, protocols_and_impls)
+5 -13
View File
@@ -486,20 +486,12 @@ defmodule Mix.Dep do
@doc """
Returns `true` if the dependency is compilable.
"""
def compilable?(%Mix.Dep{status: {:vsnlock, _}}), do: true
def compilable?(%Mix.Dep{status: {:noappfile, {_, _}}}), do: true
def compilable?(%Mix.Dep{status: {:scmlock, _}}), do: true
def compilable?(%Mix.Dep{status: :compile}), do: true
def compilable?(%Mix.Dep{status: :envoutdated}), do: true
def compilable?(dep), do: force_compilable?(dep)
@doc """
Returns `true` if the dependency is force compilable.
This is a subset of compilable. This is used in `deps.compile` to
clean the build path before compiling.
"""
def force_compilable?(%Mix.Dep{status: {:vsnlock, _}}), do: true
def force_compilable?(%Mix.Dep{status: {:noappfile, {_, _}}}), do: true
def force_compilable?(%Mix.Dep{status: {:scmlock, _}}), do: true
def force_compilable?(%Mix.Dep{status: :compile}), do: true
def force_compilable?(_), do: false
def compilable?(_), do: false
@doc """
Formats a dependency for printing.
+15 -1
View File
@@ -125,7 +125,7 @@ defmodule Mix.Tasks.Deps.Compile do
# If a dependency was marked as fetched or with an out of date lock
# or missing the app file, we always compile it from scratch.
if force? or Mix.Dep.force_compilable?(dep) do
if force? or clean_before_compile?(dep) do
File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)]))
end
@@ -402,6 +402,20 @@ defmodule Mix.Tasks.Deps.Compile do
end
end
# Most compilable statuses mean the dependency source, lock, app file, or build
# metadata changed in a way that requires removing the old build before
# compiling. :envoutdated is different: path dependencies can be incrementally
# recompiled from their existing build, but fetched dependencies must be cleaned
# first so stale BEAM files do not validate against the new compile environment
# before the dependency has a chance to rebuild.
defp clean_before_compile?(%Mix.Dep{status: :envoutdated, scm: scm}) do
scm.fetchable?()
end
defp clean_before_compile?(dep) do
Mix.Dep.compilable?(dep)
end
defp deps_compile_feedback(app) do
if Mix.install?() do
"Errors may have been logged above. You may run Mix.install/2 to try again or " <>
+57 -1
View File
@@ -73,6 +73,18 @@ defmodule Mix.Tasks.DepsTest do
end
end
defmodule FetchableGitRepoDepApp do
def project do
[
app: :git_sample,
version: "0.1.0",
deps: [
{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
]
]
end
end
## deps
test "prints list of dependencies and their status alphabetically" do
@@ -879,7 +891,51 @@ defmodule Mix.Tasks.DepsTest do
assert Application.spec(:raw_repo, :vsn)
end)
after
Application.delete_env(:raw_repo, :compile_env, persistent: true)
Application.delete_env(:anyapp, :anything, persistent: true)
end
test "recompiles fetchable dependencies when compile env changed" do
in_fixture("deps_status", fn ->
File.mkdir_p!("config")
File.write!("config/config.exs", "import Config\n")
Mix.Project.push(FetchableGitRepoDepApp)
Mix.Tasks.Deps.Get.run([])
File.write!("deps/git_repo/lib/git_repo.ex", """
Application.compile_env(:anyapp, :anything)
defmodule GitRepo do
def hello do
"World"
end
end
""")
Mix.Tasks.Loadconfig.load_compile("config/config.exs")
Mix.Task.run("compile", [])
assert Application.spec(:git_repo, :vsn)
File.write!("config/config.exs", """
import Config
config :anyapp, :anything, :anyvalue
""")
Application.unload(:git_repo)
Mix.ProjectStack.pop()
Mix.Task.clear()
Mix.Project.push(FetchableGitRepoDepApp)
purge([GitRepo])
Mix.Tasks.Loadconfig.load_compile("config/config.exs")
Mix.Task.run("compile", [])
assert_receive {:mix_shell, :info, ["Generated git_repo app"]}
assert Application.spec(:git_repo, :vsn)
end)
after
Application.delete_env(:anyapp, :anything, persistent: true)
purge([GitRepo, GitRepo.MixProject])
end
test "does not compile deps that have explicit option" do
+35
View File
@@ -479,6 +479,41 @@ defmodule Mix.UmbrellaTest do
end)
end
test "does not recompile after unchanged struct dependency and touched project files" do
in_fixture("umbrella_dep/deps/umbrella", fn ->
Mix.Project.in_project(:umbrella, ".", fn _ ->
File.mkdir_p!("config")
File.write!("config/config.exs", "import Config\n")
File.write!("apps/foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar]")
File.write!("apps/bar/lib/bar.ex", """
defmodule Bar do
def foo_bar(), do: %Foo{bar: true}
end
""")
Mix.Task.run("compile", ["--verbose"])
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
File.write!("apps/foo/lib/foo.ex", File.read!("apps/foo/lib/foo.ex") <> "\n")
ensure_touched("apps/foo/lib/foo.ex", "_build/dev/lib/bar/.mix/compile.elixir")
Mix.Task.clear()
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
ensure_touched("mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
ensure_touched("apps/foo/mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
ensure_touched("apps/bar/mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
ensure_touched("config/config.exs", "_build/dev/lib/bar/.mix/compile.elixir")
Mix.Task.clear()
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
end)
end)
end
test "recompiles after compile through runtime path dependency changes" do
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
Mix.Project.in_project(:bar, "bar", fn _ ->