Compare commits

...
31 Commits
Author SHA1 Message Date
José Valim 759443e724 Release v1.20.4 2026-08-28 12:02:55 +02:00
José Valim 0bba588757 Fix recursion on charlist error path 2026-08-28 11:59:47 +02:00
Alexander Gubarev 04c31907c5 Fix Task.yield_many/2 waits too long or forever when :limit exceeds task count (#15737) 2026-08-27 09:45:00 +02:00
José Valim 153489f9a6 Update CHANGELOG 2026-08-27 09:40:31 +02:00
Hasan mohamed 3ecb083d5d Fix type checker crash on non-returning for ... into: expression (#15747) (#15770) 2026-08-25 13:05:04 +02:00
Daniel Kukula d2689c839e Fix base64 swar singleton false positive (#15773) 2026-08-25 13:04:54 +02:00
Alexander Gubarev 309623adf1 Fix final sigma handling in :greek (#15791) 2026-08-25 13:04:14 +02:00
Łukasz Samson ae432598e4 Fix inferred type of :erlang.split_binary/2 result when arg is bitstring (#15790) 2026-08-25 13:04:08 +02:00
José Valim 31288d2267 Update CHANGELOG 2026-08-05 09:20:30 +02:00
José Valim 6efe0fc7cb Release v1.20.3 2026-08-04 16:31:51 +02:00
José Valim b3f2e0def7 Update CHANGELOG 2026-08-04 15:55:18 +02:00
Alexander Gubarev fa6205cc20 Fix elixirc --profile time switch argv parsing (#15647) 2026-08-04 15:49:27 +02:00
Łukasz Samson ea1519bcc0 Do not return {:ok, _} on maps with empty lines in map_put_static_value (#15597) 2026-08-04 15:19:29 +02:00
Łukasz Samson bfd4dd35fa Fix tuple_insert_static crash on non-normalized empty type (#15542)
Closes #15541.
2026-08-04 15:18:22 +02:00
Łukasz Samson 0564a597c6 Improve send return type (#15644)
Teach the type checker that Kernel.send/2 returns the message operand.
2026-08-04 15:16:47 +02:00
José Valim eded6248eb Optimize tuple and map emptiness checks (#15619)
Short-circuit disjoint and subtype tuple elements
before exploring the general difference branches.

This is a backport 59e257d3.

Assisted-by: Codex:GPT-5.6
2026-07-28 15:36:24 +02:00
José Valim 6bb46c30fc Update CHANGELOG 2026-07-19 13:21:16 +02:00
José Valim aa3dbdb289 Ensure %_{} is precise in guards, closes #15635 2026-07-19 10:06:36 +02:00
José Valim a55c1f7b6a Improve coverage of defguards 2026-07-17 16:20:35 +02:00
Łukasz Samson 76df4c6f58 Split tuple_insert_static into two paths (#15518)
- bdd_map on pure positive disjunction
- expand to the exact negation-free positive DNF, insert and union on negations and/or implicit :bdd_top

Fixes #15517
2026-07-16 17:50:51 +02:00
Alexander Gubarev 96f28d4362 Fix IEx.__break__!/3 fallback clause arity (#15595) 2026-07-12 13:52:34 +02:00
José Valim f894bc480e Provide proper signature for ++
And deal with compatibility checks in to_existing_atom/2.

Closes #15588.
2026-07-11 11:16:46 +02:00
José Valim 51d6cfdb93 Batch cachers in parallel checker (#15545) 2026-07-06 17:19:32 +02:00
José Valim 83ae474c61 Clarify protocols come later, closes #15529 2026-06-26 16:56:00 +02:00
Łukasz Samson 2fbf952cad Add missing Of.reset_vars call in receive/after badtimeout error path (#15527)
Variable refinement should not leak outside after block
2026-06-25 12:54:51 +02:00
Łukasz Samson 67772adcb2 Refer to the right function in remote_apply error on :maps.values/1 (#15526) 2026-06-25 12:54:51 +02:00
Łukasz Samson be1234799d Enable doctests on Descr, fix stale examples (#15528) 2026-06-25 12:54:51 +02:00
Łukasz Samson 4918e2d955 Thread full descr through fun_denormalize reduce (#15524)
Previously inner representation was threaded and returned, which broke expectations of caller `non_term_type_to_quoted`

Closes #15523.
2026-06-24 13:24:08 +02:00
José Valim cbf3836e8b Refine types for andalso/orelse in expressions, see #15450 2026-06-24 11:41:18 +02:00
Łukasz Samson 989165cabb Improve error returned from fun_apply (#15522)
- return badfun on empty type
- exclude call arity on badarity tuple

Fixes #15521

(cherry picked from commit 9a2029245d)
2026-06-24 10:34:45 +02:00
Guillaume Duboc 05d03543f6 Add put_dynamic for proper dynamic insertion (#15520) 2026-06-23 17:25:48 +02:00
29 changed files with 858 additions and 227 deletions
+44
View File
@@ -192,6 +192,50 @@ 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.4 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
### 2. Bug fixes
#### Elixir
* [Base] Fix false positives in Base64 validation
* [Kernel] Fix inferred type of `:erlang.split_binary/2` when given a bitstring
* [Kernel] Fix type checker crash on comprehensions with a non-returning `:into` expression
* [String] Fix final sigma handling in Greek casing
* [Task] Fix `Task.yield_many/2` waiting indefinitely when `:limit` exceeds the task count
## v1.20.3 (2026-08-04)
### 1. Enhancements
#### Elixir
* [Kernel.ParallelCompiler] Batch type checker cache operations to improve compilation times
### 2. Bug fixes
#### Elixir
* [Kernel] Improve type precision of the `++/2` operator
* [Kernel] Improve type precision of the `send/2` return type
* [Kernel] Improve type refinement for `and/2` and `or/2`
* [Kernel] Fix type checking of `Map.put/3` on empty map types
* [Kernel] Fix type checking of tuple insertion on equivalent tuple types
* [Kernel] Fix type checking of tuple insertion on gradual types with empty static parts
* [Kernel] Fix variable refinements leaking from invalid `receive/after` timeout expressions
* [Kernel] Fix rendering inferred types containing unions of static and dynamic functions
* [Kernel] Ensure typing errors in `:maps.values/1` reference the correct function
* [Kernel] Mark `%_{}` patterns as precise so subsequent redundant struct clauses are detected
* [elixirc] Fix parsing of the `--profile time` option
#### IEx
* [IEx.Helpers] Fix `break!/1` with unknown expressions
## v1.20.2 (2026-06-23)
### 1. Enhancements
+1 -1
View File
@@ -1 +1 @@
1.20.2
1.20.4
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.2
ELIXIR_VERSION=1.20.4
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.2
set ELIXIR_VERSION=1.20.4
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+17 -17
View File
@@ -187,21 +187,21 @@ defmodule Base do
# For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we
# extend the digit range to [0x2F, 0x39], which absorbs '/' into one range
# check — saves one Mycroft singleton. Trick lifted from
# check — saves one singleton term. Trick lifted from
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
@swar_ge_slash 0x51515151515151
# Mycroft zero-byte detection for base64 singletons (+, -, _).
# Per lane: high bit set iff `bxor(w, K*ones) - 0x01..01` has its high bit
# set, i.e. that byte's V value was 0 → original byte was K. Simplified
# (no `bnot V` term) — for ASCII-gated `w`, borrow-propagation false
# positives only occur for adjacent bytes that happen to equal `K xor 0x01`,
# which is outside the base64 alphabet, so it never matters here.
# Pattern follows https://github.com/elixir-lang/elixir/pull/15255.
@swar_mask01 0x01010101010101
@swar_plus_x7 0x2B2B2B2B2B2B2B
@swar_dash_x7 0x2D2D2D2D2D2D2D
@swar_under_x7 0x5F5F5F5F5F5F5F
# base64's '+', '-' and '_' border no other range, so each is checked as the
# degenerate range [c, c]. Mycroft zero-byte detection is one operation
# cheaper per singleton but not exact per lane: subtracting 0x01..01 makes a
# zero lane borrow from the lane above it, and a lane holding 1 turns that
# borrow into 0xFF, so bytes equal to `c bxor 0x01` read as matches too.
@swar_ge_plus 0x55555555555555
@swar_gt_plus 0x54545454545454
@swar_ge_dash 0x53535353535353
@swar_gt_dash 0x52525252525252
@swar_ge_underscore 0x21212121212121
@swar_gt_underscore 0x20202020202020
# Per-byte validity checks (used in both the SWAR clauses for the 8th byte
# of each stride and in the body of the sub-8-byte tail clauses).
@@ -365,8 +365,8 @@ defmodule Base do
# base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons.
# For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as
# part of one range (Lemire merge), leaving only '+' as a Mycroft singleton.
# For url, the singletons '-' and '_' are detected via two Mycroft terms.
# part of one range (Lemire merge), leaving only '+' as a singleton. For url,
# '-' and '_' are two more singletons.
defp valid_word64base?(w) do
band(w, @swar_mask80) == 0 and
band(
@@ -378,7 +378,7 @@ defmodule Base do
),
bxor(w + @swar_ge_slash, w + @swar_gt_9)
),
bxor(w, @swar_plus_x7) - @swar_mask01
bxor(w + @swar_ge_plus, w + @swar_gt_plus)
),
@swar_mask80
) == @swar_mask80
@@ -396,8 +396,8 @@ defmodule Base do
bxor(w + @swar_ge_0, w + @swar_gt_9)
),
bor(
bxor(w, @swar_dash_x7) - @swar_mask01,
bxor(w, @swar_under_x7) - @swar_mask01
bxor(w + @swar_ge_dash, w + @swar_gt_dash),
bxor(w + @swar_ge_underscore, w + @swar_gt_underscore)
)
),
@swar_mask80
+25 -5
View File
@@ -387,13 +387,20 @@ defimpl Inspect, for: List do
close = color_doc("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
{split, tail} =
if is_integer(printable_limit) do
case Enum.split(term, printable_limit) do
{split, []} -> {split, []}
{split, _} -> {split, " ++ ..."}
end
else
{term, []}
end
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
inspected = [?~, ?c, ?", escaped, ?" | tail]
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
@@ -407,6 +414,19 @@ defimpl Inspect, for: List do
end
end
defp unicode_list?(_, 0), do: true
defp unicode_list?([char | rest], counter)
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
do: unicode_list?(rest, decrement(counter))
defp unicode_list?([], _counter), do: true
defp unicode_list?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc false
def keyword({key, value}, opts) do
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
+2 -2
View File
@@ -21,8 +21,8 @@ defmodule Inspect.Opts do
is `:decimal` and if it is printable, otherwise in bit syntax. See
`String.printable?/1` to learn when a string is printable.
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
non-printable elements will be escaped.
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
non-printable code points will be escaped. Other lists will be printed as lists.
When `:as_lists` all lists will be printed as lists.
+1 -1
View File
@@ -338,7 +338,7 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | verbose_compile: true})
end
defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do
parse_argv(t, %{config | profile: :time})
end
+2 -2
View File
@@ -1193,7 +1193,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_binary(result) ->
@@ -1245,7 +1245,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_list(result) ->
+112 -77
View File
@@ -68,17 +68,37 @@ defmodule Module.ParallelChecker do
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
# Protocols may have been consolidated. So if we know their beam location,
# we discard their module map on purpose and start from file.
info =
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
List.to_string(beam_location)
else
cache_from_module_map(table, module_map, signatures)
end
inner_spawn(pid, checker, table, module, info, log?)
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
spawn_and_register_cache(pid, checker, table, module, List.to_string(beam_location), log?)
else
{mode, module_tuple} = cache_from_module_map(table, module_map, signatures)
ref = make_ref()
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
register_cache_and_checker(checker, mode, module, spawned, ref)
:ok
end
end
defp inner_spawn(pid, checker, table, module, info, log?) do
defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
spawn(fn ->
mon_ref = Process.monitor(pid)
receive do
{^ref, :check, profile} ->
# Set the compiler info so we can collect warnings
Process.link(pid)
:erlang.put(:elixir_compiler_info, {pid, self()})
{warnings, errors} = check_module(module_tuple, {checker, table}, log?, profile)
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :checked, ref})
{:DOWN, ^mon_ref, _, _, _} ->
:ok
end
end)
end
defp spawn_and_register_cache(pid, checker, table, module, info, log?) do
ref = make_ref()
spawned =
@@ -89,48 +109,28 @@ defmodule Module.ParallelChecker do
{^ref, :cache} ->
Process.link(pid)
{mode, module_tuple} =
cond do
is_binary(info) ->
location =
case :code.which(module) do
[_ | _] = path -> path
_ -> info
end
with {:ok, binary} <- File.read(location),
{:ok,
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
else
_ -> {:uncached, nil}
end
is_tuple(info) ->
info
location =
case :code.which(module) do
[_ | _] = path -> path
_ -> info
end
# We only make the module available now, so they are not visible during inference
:ets.insert(table, {module, mode})
send(checker, {ref, :cached})
receive do
{^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?, profile)
else
{[], []}
end
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :done, module})
with {:ok, binary} <- File.read(location),
{:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", exck}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(exck) do
mode = cache_chunk(table, module, contents)
module_tuple = module_map_to_module_tuple(module_map)
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
:ets.insert(table, {module, mode})
send(checker, {__MODULE__, :cached, module, spawned, ref})
else
_ ->
# Nothing to check, so we notify everyone we are done
:ets.insert(table, {module, :uncached})
send(checker, {__MODULE__, :cached, module, nil, ref})
send(pid, {__MODULE__, module, [], []})
end
{:DOWN, ^mon_ref, _, _, _} ->
@@ -138,7 +138,7 @@ defmodule Module.ParallelChecker do
end
end)
register(checker, module, spawned, ref)
register_cache(checker, module, spawned, ref)
:ok
end
@@ -192,7 +192,7 @@ defmodule Module.ParallelChecker do
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
inner_spawn(self(), checker, table, module, file, log?)
spawn_and_register_cache(self(), checker, table, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
@@ -533,8 +533,12 @@ defmodule Module.ParallelChecker do
:gen_server.call(server, {:unlock, module, mode}, :infinity)
end
defp register(server, module, pid, ref) do
:gen_server.cast(server, {:register, module, pid, ref})
defp register_cache_and_checker(server, mode, module, pid, ref) do
:gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, ref})
end
defp register_cache(server, module, pid, ref) do
:gen_server.cast(server, {:register_cache, module, pid, ref})
end
## Server callbacks
@@ -577,7 +581,8 @@ defmodule Module.ParallelChecker do
state = %{
waiting: %{},
modules: [],
caches: [],
checkers: [],
spawned: %{},
schedulers: schedulers,
threshold: threshold,
@@ -590,20 +595,9 @@ defmodule Module.ParallelChecker do
:gen_server.enter_loop(__MODULE__, [], state)
end
def handle_call(:start, _from, %{modules: modules, protocols: protocols, table: table} = state) do
def handle_call(:start, _from, %{caches: caches, protocols: protocols, table: table} = state) do
:ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
for {_module, pid, ref} <- modules do
send(pid, {ref, :cache})
end
for {_module, _pid, ref} <- modules do
receive do
{^ref, :cached} -> :ok
end
end
{:reply, length(modules), run_checkers(%{state | protocols: []})}
{:reply, length(caches), run_caches(%{state | protocols: []})}
end
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
@@ -633,10 +627,21 @@ defmodule Module.ParallelChecker do
{:noreply, state}
end
def handle_info({__MODULE__, :done, module}, state) do
# Unfortunately we cannot assume uniqueness because the same module
# may be defined by mistake several times
{timer, spawned} = Map.pop(state.spawned, module)
def handle_info({__MODULE__, :cached, module, pid, ref}, state) do
{_nil, spawned} = Map.pop(state.spawned, ref)
state =
if pid do
%{state | spawned: spawned, checkers: [{module, pid, ref} | state.checkers]}
else
%{state | spawned: spawned}
end
{:noreply, run_caches(state)}
end
def handle_info({__MODULE__, :checked, ref}, state) do
{timer, spawned} = Map.pop(state.spawned, ref)
timer && Process.cancel_timer(timer)
{:noreply, run_checkers(%{state | spawned: spawned})}
end
@@ -645,11 +650,41 @@ defmodule Module.ParallelChecker do
{:stop, :normal, state}
end
def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
{:noreply, %{state | modules: [{module, pid, ref} | modules]}}
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do
{:noreply, %{state | caches: [{module, pid, ref} | caches]}}
end
defp run_checkers(%{modules: []} = state) do
def handle_cast(
{:register_cache_and_checker, mode, module, pid, ref},
%{caches: caches, checkers: checkers} = state
) do
{:noreply,
%{state | caches: [{module, mode} | caches], checkers: [{module, pid, ref} | checkers]}}
end
defp run_caches(%{caches: [], spawned: spawned} = state) do
if spawned == %{}, do: run_checkers(state), else: state
end
defp run_caches(%{spawned: spawned, schedulers: schedulers} = state)
when map_size(spawned) >= schedulers do
state
end
defp run_caches(%{caches: [cache | caches]} = state) do
case cache do
{_module, pid, ref} ->
send(pid, {ref, :cache})
spawned = Map.put(state.spawned, ref, nil)
run_caches(%{state | caches: caches, spawned: spawned})
{module, mode} ->
:ets.insert(state.table, {module, mode})
run_caches(%{state | caches: caches})
end
end
defp run_checkers(%{checkers: []} = state) do
state
end
@@ -658,11 +693,11 @@ defmodule Module.ParallelChecker do
state
end
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do
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})
spawned = Map.put(state.spawned, ref, timer)
run_checkers(%{state | checkers: checkers, spawned: spawned})
end
defp profile(module, :time, fun) do
+40 -2
View File
@@ -213,7 +213,11 @@ defmodule Module.Types.Apply do
{:erlang, :spawn_link, [{mfargs, pid()}]},
{:erlang, :spawn_monitor, [{[fun(0)], tuple([pid(), reference()])}]},
{:erlang, :spawn_monitor, [{mfargs, tuple([pid(), reference()])}]},
{:erlang, :split_binary, [{[binary(), integer()], tuple([binary(), binary()])}]},
{:erlang, :split_binary,
[
{[binary(), integer()], tuple([binary(), binary()])},
{[bitstring_no_binary(), integer()], tuple([binary(), bitstring_no_binary()])}
]},
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
@@ -1087,6 +1091,13 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(:erlang, :send, info, [_dest, message] = args_types, stack) do
case remote_apply(info, args_types, stack) do
{:ok, _type} -> {:ok, return(message, args_types, stack)}
other -> other
end
end
defp remote_apply(:erlang, :tl, _info, [list], stack) do
case list_tl(list) do
{:ok, value_type} -> {:ok, return(value_type, [list], stack)}
@@ -1094,6 +1105,33 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(:erlang, :++, _info, [left, right], stack) do
# TODO: remove once we add parametric types, this will just be:
# empty_list(), a -> a
# non_empty_list(elem), a -> non_empty_list(elem, a)
case list_of(left) do
{empty_list?, list_of} ->
left_result =
if empty_list? do
right
else
none()
end
right_result =
if list_of do
non_empty_list(list_of, right)
else
none()
end
{:ok, return(union(left_result, right_result), [left, right], stack)}
:badproperlist ->
{:error, badremote(:erlang, :++, [left, right])}
end
end
@struct_key atom([:__struct__])
@nil_atom atom([nil])
@@ -1409,7 +1447,7 @@ defmodule Module.Types.Apply do
defp remote_apply(:maps, :values, _info, [map], stack) do
case map_to_list(map, fn _key, value -> value end) do
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
:badmap -> {:error, badremote(:maps, :keys, [map])}
:badmap -> {:error, badremote(:maps, :values, [map])}
end
end
+127 -54
View File
@@ -409,6 +409,11 @@ defmodule Module.Types.Descr do
defp pop_dynamic(:term), do: {:term, :term}
defp pop_dynamic(descr), do: Map.pop(descr, :dynamic, descr)
defp put_dynamic(:term, dynamic), do: optional_to_term(%{dynamic: dynamic})
defp put_dynamic(static, dynamic) when static == dynamic, do: static
defp put_dynamic(_static, dynamic) when dynamic == @none, do: @none
defp put_dynamic(static, dynamic), do: Map.put(static, :dynamic, dynamic)
@compile {:inline, maybe_union: 2}
defp maybe_union(nil, _fun), do: nil
defp maybe_union(descr, fun), do: union(descr, fun.())
@@ -508,7 +513,8 @@ defmodule Module.Types.Descr do
{right_dynamic, right_static} = pop_dynamic(right)
dynamic_part = difference_static(left_dynamic, right_static)
Map.put(difference_static(left_static, right_dynamic), :dynamic, dynamic_part)
difference_static(left_static, right_dynamic)
|> put_dynamic(dynamic_part)
else
difference_static(left, right)
end
@@ -1407,10 +1413,10 @@ defmodule Module.Types.Descr do
{:ok, atom()}
iex> fun_apply(fun([integer()], atom()), [float()])
:badarg
{:badarg, [integer()], false}
iex> fun_apply(fun([dynamic()], atom()), [dynamic()])
{:ok, atom()}
iex> fun_apply(dynamic(fun([integer()], atom())), [integer()])
{:ok, dynamic(atom())}
"""
def fun_apply(:term, _arguments), do: :badfun
@@ -1626,7 +1632,7 @@ defmodule Module.Types.Descr do
defp fun_normalize(%{fun: {:union, bdds}}, arity) do
case :maps.take(arity, bdds) do
{bdd, _rest} ->
{bdd, rest} ->
{domain, arrows} =
Enum.reduce(fun_bdd_to_pos_dnf(arity, bdd), {term(), []}, fn pos_funs,
{domain, arrows} ->
@@ -1634,7 +1640,13 @@ defmodule Module.Types.Descr do
end)
if arrows == [] do
{:badarity, :maps.keys(bdds)}
# The function is empty at the requested arity. Report the *other*
# arities (never the called one, which would be self-contradictory),
# or :badfun when there are none, i.e. the function is empty.
case :maps.keys(rest) do
[] -> :badfun
other -> {:badarity, other}
end
else
{:ok, domain, arrows}
end
@@ -1952,25 +1964,33 @@ defmodule Module.Types.Descr do
# representation. The goal here is to do the opposite of fun_descr
# and put static and dynamic parts back together to improve
# pretty printing.
defp fun_denormalize(%{fun: {:union, static_repr}}, %{fun: {:union, dynamic_repr}}, opts) do
# Denormalize each arity
for {arity, static_bdd} <- static_repr,
{^arity, dynamic_bdd} <- dynamic_repr,
reduce: {static_repr, dynamic_repr, []} do
{statics, dynamics, acc} ->
with {:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do
{Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted | acc]}
else
_ -> {statics, dynamics, acc}
end
end
defp fun_denormalize(
%{fun: {:union, static_repr}} = static,
%{fun: {:union, dynamic_repr}} = dynamic,
opts
) do
{static_repr, dynamic_repr, acc} =
Enum.reduce(static_repr, {static_repr, dynamic_repr, []}, fn
{arity, static_bdd}, {statics, dynamics, acc} ->
with %{^arity => dynamic_bdd} <- dynamics,
{:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do
{Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted | acc]}
else
_ -> {statics, dynamics, acc}
end
end)
{fun_replace_arities(static, static_repr), fun_replace_arities(dynamic, dynamic_repr), acc}
end
# If not unions of functions, do not try to denormalize.
defp fun_denormalize(static_repr, dynamic_repr, _opts) do
{static_repr, dynamic_repr, []}
defp fun_denormalize(static, dynamic, _opts) do
{static, dynamic, []}
end
defp fun_replace_arities(descr, arities) when arities == %{}, do: Map.delete(descr, :fun)
defp fun_replace_arities(descr, arities), do: %{descr | fun: {:union, arities}}
defp fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do
static_pos = fun_bdd_to_pos_dnf(arity, static_bdd)
dynamic_pos = fun_bdd_to_pos_dnf(arity, dynamic_bdd)
@@ -2272,7 +2292,7 @@ defmodule Module.Types.Descr do
It returns a two-element tuple. The first element dictates the
empty list type. The second element returns the value type.
{boolean(), t() or nil}
{true, nil} or {boolean(), t()}
If the value is `nil`, it means that component is missing.
Note `{false, nil}` is not a valid return type, instead it
@@ -2284,10 +2304,10 @@ defmodule Module.Types.Descr do
case :maps.take(:dynamic, descr) do
:error ->
with {empty_list?, value} <- list_of_static(descr) do
if empty?(value) and empty_list? == false do
:badproperlist
else
{empty_list?, value}
cond do
not empty?(value) -> {empty_list?, value}
empty_list? -> {true, nil}
true -> :badproperlist
end
end
@@ -2307,7 +2327,7 @@ defmodule Module.Types.Descr do
%{list: bdd} ->
Enum.reduce(list_bdd_to_pos_dnf(bdd), none(), fn {list, last, _negs}, acc ->
if last == @empty_list or subtype?(last, @empty_list) do
if empty_list_type?(last) do
union(acc, list)
else
acc
@@ -4175,7 +4195,7 @@ defmodule Module.Types.Descr do
defp map_put_static_value(descr, split_keys, type) do
case :maps.take(:dynamic, descr) do
:error ->
if descr_key?(descr, :map) and map_only?(descr) do
if non_empty_map_only?(descr) do
{:ok, map_put_static(descr, split_keys, type)}
else
:badmap
@@ -4497,11 +4517,22 @@ defmodule Module.Types.Descr do
end
defp map_line_meet_empty?(key, type, neg_type, t1, t2, tag, neg_tag, acc_meet, negs) do
diff = difference(type, neg_type)
meet = intersection(type, neg_type)
(empty?(diff) or map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} | t1]), negs)) and
(empty?(meet) or map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} | acc_meet], negs))
if empty?(meet) do
# This negative map is disjoint from the current line at this field.
map_line_empty?(tag, Enum.reverse(acc_meet, [{key, type} | t1]), negs)
else
diff = difference(type, neg_type)
if empty?(diff) do
# The field is a subtype of the negative field, so their intersection is type.
map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, type} | acc_meet], negs)
else
map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} | t1]), negs) and
map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} | acc_meet], negs)
end
end
end
defp map_line_fields_empty?([{k1, v1} | t1], [{k2, _} | _] = l2, tag, neg_tag, fields, negs)
@@ -5203,13 +5234,22 @@ defmodule Module.Types.Descr do
defp tuple_elements_empty?(acc_meet, tag, elements, [neg_type | neg_elements], negs) do
# Handles the case where {tag, elements} is an open tuple, like {:open, []}
{ty, elements} = List.pop_at(elements, 0, term())
diff = difference(ty, neg_type)
meet = intersection(ty, neg_type)
# In this case, there is no intersection between the positive and this negative.
# So we should just "go next"
(empty?(diff) or tuple_line_empty?(tag, Enum.reverse(acc_meet, [diff | elements]), negs)) and
(empty?(meet) or tuple_elements_empty?([meet | acc_meet], tag, elements, neg_elements, negs))
if empty?(meet) do
# This negative tuple is disjoint from the current line at this element.
tuple_line_empty?(tag, Enum.reverse(acc_meet, [ty | elements]), negs)
else
diff = difference(ty, neg_type)
if empty?(diff) do
# The element is a subtype of the negative element, so their intersection is ty.
tuple_elements_empty?([ty | acc_meet], tag, elements, neg_elements, negs)
else
tuple_line_empty?(tag, Enum.reverse(acc_meet, [diff | elements]), negs) and
tuple_elements_empty?([meet | acc_meet], tag, elements, neg_elements, negs)
end
end
end
# Determines if the set difference is empty when:
@@ -5538,12 +5578,15 @@ defmodule Module.Types.Descr do
iex> tuple_fetch(tuple([integer(), atom()]), 0)
{false, integer()}
iex> tuple_fetch(union(tuple([integer()]), tuple([integer(), atom()])), 1)
{true, atom()}
iex> tuple_fetch(union(tuple([integer()]), tuple([atom()])), 0)
{false, union(integer(), atom())}
iex> tuple_fetch(dynamic(), 0)
{true, dynamic()}
iex> tuple_fetch(tuple([integer(), atom()]), 2)
:badindex
iex> tuple_fetch(integer(), 0)
:badtuple
@@ -5955,27 +5998,57 @@ defmodule Module.Types.Descr do
end
end
defp tuple_insert_static(descr, _, _) when descr == @none, do: none()
defp tuple_insert_static(descr, index, type) do
Map.update!(descr, :tuple, fn bdd ->
bdd_map(bdd, fn bdd_leaf(tag, elements) ->
# If the tuple is open, then we want List.insert_at to put the new element at the correct
# index, which requires filling the tuple with `term()` values first.
# Closed tuples of an incorrect size will be ignored (they are cancelled by the earlier
# intersection with `tuple_of_size_at_least`).
elements =
if tag == :open and length(elements) < index do
tuple_fill(elements, index)
defp tuple_insert_static(%{tuple: bdd} = descr, index, type) do
%{
descr
| tuple:
if tuple_bdd_positive?(bdd) do
# A pure disjunction of leaves: the insert distributes over the union, so
# we rewrite each leaf in place (preserving the structure callers assert on).
bdd_map(bdd, fn bdd_leaf(tag, elements) ->
tuple_insert_leaf(tag, elements, index, type)
end)
else
elements
# The bdd carries negations and/or implicit `:bdd_top` positive paths
# (e.g. from `tuple_difference(open_tuple([]), _) -> bdd_negation`).
# `bdd_map` rewrites only explicit leaves, so it would skip the implicit
# top (losing the insert) and wrongly transform negated leaves. Expand to
# the exact negation-free positive DNF first, then insert into each leaf.
bdd
|> tuple_bdd_to_dnf_no_negations()
|> Enum.reduce(:bdd_bot, fn {tag, elements}, acc ->
tuple_union(tuple_insert_leaf(tag, elements, index, type), acc)
end)
end
bdd_leaf_new(tag, List.insert_at(elements, index, type))
end)
end)
}
end
defp tuple_insert_static(_descr, _index, _type), do: none()
# Inserts `type` at `index` into a single tuple literal. If the tuple is open,
# `List.insert_at` needs the tuple filled with `term()` up to `index` first.
# Closed tuples of an incorrect size are cancelled before reaching here (the
# input is intersected/guarded with `tuple_of_size_at_least`).
defp tuple_insert_leaf(tag, elements, index, type) do
elements =
if tag == :open and length(elements) < index do
tuple_fill(elements, index)
else
elements
end
bdd_leaf_new(tag, List.insert_at(elements, index, type))
end
# `bdd_map` rewrites a tuple insert exactly only on a pure positive disjunction
# of leaves (every node keeps its literal on the constrained-top branch with no
# dual branch). Any dual branch (or implicit `:bdd_top` in negated position)
# would be transformed incorrectly or skipped.
defp tuple_bdd_positive?(:bdd_bot), do: true
defp tuple_bdd_positive?(bdd_leaf(_, _)), do: true
defp tuple_bdd_positive?({_, _lit, :bdd_top, u, :bdd_bot}), do: tuple_bdd_positive?(u)
defp tuple_bdd_positive?(_), do: false
@doc """
Replace an element in the tuple at the given (0-based) index.
+37 -1
View File
@@ -584,7 +584,8 @@ defmodule Module.Types.Expr do
{union(body_type, acc), Of.reset_vars(context, original)}
else
error = {:badtimeout, timeout_type, timeout, context}
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
context = error(__MODULE__, error, meta, stack, context)
{union(body_type, acc), Of.reset_vars(context, original)}
end
end)
|> dynamic_unless_static(stack)
@@ -687,6 +688,38 @@ defmodule Module.Types.Expr do
end
end
def of_expr(
{{:., _, [:erlang, fun]}, _meta, [left, right]} = call,
expected,
_expr,
stack,
context
)
when fun in [:andalso, :orelse] do
polarity = fun == :andalso
polarity_type = atom([polarity])
left_expected =
case booleaness(expected) do
{^polarity, _} -> polarity_type
_ -> boolean()
end
{left_type, context} = of_expr(left, left_expected, call, stack, context)
{right_type, right_context} = of_expr(right, expected, call, stack, context)
if subtype?(left_type, polarity_type) do
{right_type, right_context}
else
context =
right_context
|> Of.reset_vars(context)
|> reset_warnings(context)
{union(atom([not polarity]), right_type), context}
end
end
def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, stack, context) do
{remote_type, context} = of_expr(remote, atom(), call, stack, context)
{mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context)
@@ -855,6 +888,9 @@ defmodule Module.Types.Expr do
{true, false} ->
{type, :bitstring, context}
{false, false} ->
{type, :none, context}
end
else
{_type, context} =
+2 -1
View File
@@ -772,7 +772,8 @@ defmodule Module.Types.Pattern do
{refined, context} = of_match_var(var, atom(), expr, stack, context)
if compatible?(refined, atom()) do
of_open_map(args, singleton?(refined), [__struct__: refined], [], path, stack, context)
precise? = match?({:_, _, _}, var) or singleton?(refined)
of_open_map(args, precise?, [__struct__: refined], [], path, stack, context)
else
error = {:badstruct, refined, expr, context}
{error_type(), false, error(__MODULE__, error, meta, stack, context)}
+30 -6
View File
@@ -1008,6 +1008,10 @@ defmodule String do
IO.iodata_to_binary(downcase_ascii(string))
end
def downcase(string, :greek) when is_binary(string) do
String.Unicode.downcase_greek(string, [])
end
def downcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
String.Unicode.downcase(string, [], mode)
end
@@ -1058,19 +1062,39 @@ defmodule String do
@letter_I_dot_above <<0x0130::utf8>>
def capitalize(<<@letter_i, right::binary>>, mode) do
if(mode == :turkic, do: @letter_I_dot_above, else: @letter_I) <> downcase(right, mode)
capitalized = if mode == :turkic, do: @letter_I_dot_above, else: @letter_I
capitalized <> downcase_after_capitalize(right, mode, ?i)
end
def capitalize(string, mode) when is_binary(string) do
case :unicode_util.gc(string) do
[gc] -> grapheme_to_binary(:string.titlecase([gc]))
[gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
[gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
[] -> ""
{:error, <<byte, rest::bits>>} -> <<byte>> <> downcase(rest, mode)
[gc] ->
grapheme_to_binary(:string.titlecase([gc]))
[gc, rest] ->
grapheme_to_binary(:string.titlecase([gc])) <>
downcase_after_capitalize(rest, mode, gc)
[gc | rest] ->
grapheme_to_binary(:string.titlecase([gc])) <>
downcase_after_capitalize(rest, mode, gc)
[] ->
""
{:error, <<byte, rest::bits>>} ->
<<byte>> <> downcase(rest, mode)
end
end
defp downcase_after_capitalize(rest, :greek, gc) do
String.Unicode.downcase_greek(rest, [], grapheme_to_binary(gc))
end
defp downcase_after_capitalize(rest, mode, _gc) do
downcase(rest, mode)
end
@doc false
@deprecated "Use String.trim_trailing/1 instead"
defdelegate rstrip(binary), to: String.Break, as: :trim_trailing
+2 -1
View File
@@ -1248,9 +1248,10 @@ defmodule Task do
{ref, nil}
end)
ref_count = map_size(refs)
on_timeout = Keyword.get(opts, :on_timeout, :nothing)
timeout = Keyword.get(opts, :timeout, 5_000)
limit = Keyword.get(opts, :limit, map_size(refs))
limit = min(Keyword.get(opts, :limit, ref_count), ref_count)
timeout_ref = make_ref()
timer_ref =
+2 -3
View File
@@ -109,19 +109,18 @@ iex> john.__struct__
User
```
However, structs do not inherit any of the protocols that maps do. For example, you can neither enumerate nor access a struct:
However, structs do not inherit any of the built-in features that maps do. For example, you can neither enumerate nor access a struct:
```elixir
iex> john = %User{}
%User{age: 27, name: "John"}
iex> john[:name]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
User.fetch(%User{age: 27, name: "John"}, :name)
iex> Enum.each(john, fn {field, value} -> IO.puts(value) end)
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"} of type User (a struct)
```
Structs alongside protocols provide one of the most important features for Elixir developers: data polymorphism. That's what we will explore in the next chapter.
As we will learn in future chapters, Elixir does allow developers to attach custom behaviours to structs based on their names, enabling custom data enumeration, pretty printing, and more. This dispatch mechanism is called [protocols](protocols.md) and enables extensible data polymorphism in Elixir.
## Default values and required keys
+40 -9
View File
@@ -263,6 +263,14 @@ defmodule BaseTest do
refute valid64?("Zm9)")
end
test "valid64?/1 returns false on a non-alphabet character before a ?+" do
# ?* is ?+ with its low bit flipped
for pos <- 0..14, char <- ~c"*),^ \t" do
string = String.duplicate("A", pos) <> <<char, ?+>> <> String.duplicate("A", 14 - pos)
refute valid64?(string), "expected #{inspect(string)} to be invalid"
end
end
test "valid64?/1 returns false on whitespace unless there's ignore: :whitespace" do
refute valid64?("\nQWxhZGRp bjpvcGVu\sIHNlc2Ft\t")
@@ -446,6 +454,14 @@ defmodule BaseTest do
refute url_valid64?("Zm9)")
end
test "url_valid64?/1 returns false on a non-alphabet character before a ?- or ?_" do
# ?, and ?^ are ?- and ?_ with their low bits flipped
for pos <- 0..14, char <- ~c",^*) \t", next <- ~c"-_" do
string = String.duplicate("A", pos) <> <<char, next>> <> String.duplicate("A", 14 - pos)
refute url_valid64?(string), "expected #{inspect(string)} to be invalid"
end
end
test "url_valid64?/1 returns false on whitespace unless there's ignore: :whitespace" do
refute url_valid64?("\nQWxhZGRp bjpvcGVu\sIHNlc2Ft\t")
@@ -1054,14 +1070,13 @@ defmodule BaseTest do
assert hex_valid32?("cPNg====", case: :mixed, padding: false)
end
# TODO: add valid? tests
test "encode then decode is identity" do
test "encode then decode is identity, and valid? agrees with decode" do
for {encode, decode, valid?} <- [
{&encode16/2, &decode16!/2, &valid16?/2},
{&encode32/2, &decode32!/2, &valid32?/2},
{&hex_encode32/2, &hex_decode32!/2, &hex_valid32?/2},
{&encode64/2, &decode64!/2, &valid64?/2},
{&url_encode64/2, &url_decode64!/2, &url_valid64?/2}
{&encode16/2, &decode16/2, &valid16?/2},
{&encode32/2, &decode32/2, &valid32?/2},
{&hex_encode32/2, &hex_decode32/2, &hex_valid32?/2},
{&encode64/2, &decode64/2, &valid64?/2},
{&url_encode64/2, &url_decode64/2, &url_valid64?/2}
],
encode_case <- [:upper, :lower],
decode_case <- [:upper, :lower, :mixed],
@@ -1089,10 +1104,26 @@ defmodule BaseTest do
decode_opts = Keyword.take([case: decode_case, padding: pad?], allowed_opts)
assert valid?.(encoded, decode_opts)
expected = decode.(encoded, decode_opts)
assert data == expected,
assert decode.(encoded, decode_opts) == {:ok, data},
"identity did not match for #{inspect(data)} when #{inspect(encode)} (#{encode_case})"
size = byte_size(encoded)
if size > 0 do
pos = :rand.uniform(size) - 1
taken = min(2, size - pos)
corruption = for _ <- 1..taken, into: <<>>, do: <<:rand.uniform(256) - 1>>
corrupted =
binary_part(encoded, 0, pos) <>
corruption <>
binary_part(encoded, pos + taken, size - pos - taken)
assert valid?.(corrupted, decode_opts) ==
match?({:ok, _}, decode.(corrupted, decode_opts)),
"valid? and decode disagree on #{inspect(corrupted)}"
end
end
end
end
+16 -1
View File
@@ -312,10 +312,15 @@ defmodule Inspect.ListTest do
test "printable limit" do
assert inspect(~c"hello world", printable_limit: 4) == ~s(~c"hell" ++ ...)
assert inspect(~c"hello", printable_limit: :infinity) == ~s(~c"hello")
# Non printable characters after the limit don't matter
assert inspect(~c"hello world" ++ [0], printable_limit: 4) == ~s(~c"hell" ++ ...)
# Non printable strings aren't affected by printable limit
assert inspect([0, 1, 2, 3, 4], printable_limit: 3) == ~s([0, 1, 2, 3, 4])
# Ensure a non-printable value after limit does not crash
assert inspect(~c"hello world" ++ [nil], printable_limit: 4) == ~s(~c"hell" ++ ...)
# Ensure a non-printable value after default limit does not hang
assert String.ends_with?(inspect(List.duplicate(?a, 5000) ++ [nil]), "\" ++ ...")
end
test "keyword" do
@@ -340,10 +345,20 @@ defmodule Inspect.ListTest do
assert inspect([0], charlists: :infer) == "[0]"
end
test "opt as strings" do
test "opt as charlists" do
assert inspect(~c"john" ++ [0] ++ ~c"doe", charlists: :as_charlists) == ~s(~c"john\\0doe")
assert inspect(~c"john", charlists: :as_charlists) == ~s(~c"john")
assert inspect([0], charlists: :as_charlists) == ~s(~c"\\0")
assert inspect([nil], charlists: :as_charlists) == "[nil]"
assert inspect([0xD800], charlists: :as_charlists) == "[55296]"
assert inspect([1_114_112], charlists: :as_charlists) == "[1114112]"
assert inspect([[?a]], charlists: :as_charlists) == ~s([~c"a"])
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 4) ==
"[97, 98, 99 | nil]"
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 3) ==
~s(~c"abc" ++ ...)
end
test "opt as lists" do
+12
View File
@@ -359,6 +359,12 @@ defmodule ListTest do
assert_raise ArgumentError, ~r"cannot convert the given list to a string", fn ->
List.to_string([:a, :b])
end
invalid = List.duplicate(?a, 4096) ++ [nil]
assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
List.to_string(invalid)
end
end
test "to_charlist/1" do
@@ -377,6 +383,12 @@ defmodule ListTest do
assert_raise ArgumentError, ~r"cannot convert the given list to a charlist", fn ->
List.to_charlist([:a, :b])
end
invalid = List.duplicate(?a, 4096) ++ [nil]
assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
List.to_charlist(invalid)
end
end
describe "myers_difference/2" do
@@ -15,6 +15,7 @@ defmodule Module.Types.DescrTest do
use ExUnit.Case, async: true
import Module.Types.Descr
doctest Module.Types.Descr, import: true
defmacro domain_key(arg) when is_atom(arg), do: [arg]
defp number(), do: union(integer(), float())
@@ -590,7 +591,10 @@ defmodule Module.Types.DescrTest do
assert equal?(dynamic(), difference(term(), dynamic()))
assert empty?(difference(dynamic(), term()))
assert empty?(difference(none(), dynamic()))
assert empty?(difference(dynamic(integer()), integer()))
assert difference(dynamic(integer()), integer()) == none()
# Covers assembling a result with static :term and dynamic unfolded term.
assert difference(term(), %{dynamic: none(), optional: 1}) == term()
end
test "optional" do
@@ -1311,6 +1315,28 @@ defmodule Module.Types.DescrTest do
assert fun_apply(fun_mixed, [integer()]) == {:badarity, [1, 2]}
assert fun_apply(fun_mixed, [integer(), atom()]) == {:badarity, [2, 1]}
# A function that is empty at the called arity is :badfun, not a
# self-contradictory {:badarity, [called_arity]}.
empty_fun =
difference(fun([union(integer(), float())], atom()), fun([integer()], atom()))
assert empty?(empty_fun)
assert fun_apply(empty_fun, [integer()]) == :badfun
# When the function is empty at the called arity but usable at another, the
# badarity must list only the *other* arity, never the (empty) called one.
# `empty_fun` keeps a structured empty arity-1 entry, which union preserves,
# so `usable_at_2` is equal? to a plain 2-arity function yet carries it.
usable_at_2 = union(empty_fun, fun([integer(), atom()], boolean()))
refute empty?(usable_at_2)
assert equal?(usable_at_2, fun([integer(), atom()], boolean()))
# Congruent with the normalized form, and excludes the empty arity 1.
assert fun_apply(usable_at_2, [integer()]) == {:badarity, [2]}
assert fun_apply(fun([integer(), atom()], boolean()), [integer()]) == {:badarity, [2]}
assert fun_apply(usable_at_2, [integer(), atom()]) == {:ok, boolean()}
# Function intersection tests (no overlap)
fun0 = intersection(fun([integer()], atom()), fun([float()], binary()))
assert fun_apply(fun0, [integer()]) == {:ok, atom()}
@@ -1709,7 +1735,7 @@ defmodule Module.Types.DescrTest do
test "list_of" do
assert list_of(term()) == :badproperlist
assert list_of(none()) == :badproperlist
assert list_of(empty_list()) == {true, none()}
assert list_of(empty_list()) == {true, nil}
assert list_of(union(empty_list(), integer())) == :badproperlist
assert list_of(non_empty_list(integer())) == {false, integer()}
assert list_of(non_empty_list(integer(), atom())) == :badproperlist
@@ -1720,7 +1746,9 @@ defmodule Module.Types.DescrTest do
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(list(integer(), term()))) == {true, dynamic()}
assert list_of(dynamic(non_empty_list(integer(), atom()))) == :badproperlist
assert list_of(dynamic(non_empty_list(integer(), term()))) == {false, dynamic()}
assert list_of(dynamic(union(empty_list(), integer()))) == {true, nil}
# A list that the difference resolves to nothing
@@ -1955,6 +1983,18 @@ defmodule Module.Types.DescrTest do
|> tuple_insert_at(1, float())
|> equal?(tuple([integer(), float(), atom(), boolean()]))
# Inserting must be a congruence wrt equal?, even when the positive side is
# the implicit top (a negation with :bdd_top branches) rather than an
# explicit open leaf. Here t2 == open_tuple([term(), term()]).
t2 = difference(open_tuple([]), union(tuple([]), tuple([term()])))
assert equal?(t2, open_tuple([term(), term()]))
assert tuple_insert_at(t2, 2, float())
|> equal?(tuple_insert_at(open_tuple([term(), term()]), 2, float()))
# The inserted index is actually constrained to float().
refute subtype?(tuple([integer(), integer(), atom()]), tuple_insert_at(t2, 2, float()))
# Test inserting into a complex union involving dynamic
assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()])))
|> tuple_insert_at(1, boolean())
@@ -1974,6 +2014,20 @@ defmodule Module.Types.DescrTest do
# Errors must propagate even when the inserted value is dynamic
assert tuple_insert_at(integer(), 0, dynamic()) == :badtuple
assert tuple_insert_at(tuple([atom([:ok])]), 2, dynamic()) == :badindex
# Must not crash when a gradual descr's static part is a non-normalized
# empty (semantically empty but syntactically present) non-tuple component.
a1 = union(dynamic(), non_empty_list(integer(), none()))
assert equal?(a1, dynamic())
assert tuple_insert_at(a1, 1, atom([:x]))
|> equal?(tuple_insert_at(dynamic(), 1, atom([:x])))
a2 = union(tuple([dynamic()]), non_empty_list(none()))
assert equal?(a2, tuple([dynamic()]))
assert tuple_insert_at(a2, 1, atom([:x]))
|> equal?(tuple_insert_at(tuple([dynamic()]), 1, atom([:x])))
end
test "tuple_replace_at" do
@@ -3162,6 +3216,16 @@ defmodule Module.Types.DescrTest do
assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())}
end
test "is consistent across representations of an empty type" do
# An empty map component that survives syntactically (open_map(c: none())
# is a non-normalized empty, equal to none()) must report :badmap like
# none(), not {:ok, <inhabited>}.
a2 = open_map(c: none())
assert equal?(none(), a2)
assert map_put(none(), atom([:a]), integer()) == :badmap
assert map_put(a2, atom([:a]), integer()) == :badmap
end
end
describe "disjoint" do
@@ -3258,6 +3322,8 @@ defmodule Module.Types.DescrTest do
test "dynamic (negation)" do
assert dynamic(negation(integer())) |> to_quoted_string() == "dynamic(not integer())"
assert negation(dynamic(none())) == term()
assert negation(dynamic(integer())) |> to_quoted_string() == "dynamic() or not integer()"
assert union(atom(), dynamic(integer())) |> negation() |> to_quoted_string() ==
@@ -3534,6 +3600,34 @@ defmodule Module.Types.DescrTest do
"""
end
test "fun union of static and dynamic keeps non-fun components" do
# Denormalizing a static/dynamic fun union must not drop the other parts of
# the type, nor crash when an arity fails to denormalize.
# (a) must not raise when denormalization fails for an arity
assert difference(fun([term()], atom()), fun([integer()], dynamic(atom())))
|> to_quoted_string() ==
"dynamic((term() -> atom()) and (integer() -> atom()))"
# (b) non-fun components must be preserved alongside the denormalized fun
assert union(
integer(),
union(fun([integer()], atom()), dynamic(fun([integer()], atom())))
)
|> to_quoted_string() == "(integer() -> atom()) or integer()"
# several components, with a leftover non-denormalized dynamic arity
assert union(
atom([:tag]),
union(
fun([integer()], atom()),
dynamic(union(fun([integer()], atom()), fun([integer(), integer()], atom())))
)
)
|> to_quoted_string() ==
"dynamic((integer(), integer() -> atom())) or :tag or (integer() -> atom())"
end
test "fun (negation)" do
assert fun([integer()], atom()) |> negation() |> to_quoted_string() ==
"not (integer() -> atom())"
@@ -148,6 +148,30 @@ defmodule Module.Types.ExprTest do
non_empty_list(term(), term())
"""
end
test "++" do
assert typecheck!([x], [] ++ String.to_integer(x)) == integer()
assert typecheck!([x], [x] ++ []) == non_empty_list(dynamic())
assert typeerror!([x], String.to_integer(x) ++ []) |> strip_ansi() =~
~l"""
incompatible types given to Kernel.++/2:
String.to_integer(x) ++ []
given types:
integer(), empty_list()
but expected one of:
#1
empty_list(), term()
#2
non_empty_list(term()), term()
"""
end
end
describe "funs" do
@@ -419,6 +443,13 @@ defmodule Module.Types.ExprTest do
) == dynamic(tuple([integer(), integer(), binary()]))
end
test "send returns the message" do
assert typecheck!(send(self(), {:msg, 1})) == tuple([atom([:msg]), integer()])
assert typeerror!(send(123, {:msg, 1})) =~
"incompatible types given to Kernel.send/2"
end
test "undefined function warnings" do
assert typewarn!(URI.unknown("foo")) ==
{dynamic(), "URI.unknown/1 is undefined or private"}
@@ -2276,6 +2307,42 @@ defmodule Module.Types.ExprTest do
assert typecheck!(false or true) == atom([true])
end
test "andalso/orelse do not leak right-hand side refinements" do
assert typecheck!(
[x],
cond do
:erlang.andalso(is_tuple(x), tuple_size(x) == 3) -> {:tuple, x}
is_atom(x) -> {:atom, x}
true -> {:other, x}
end
)
|> equal?(
dynamic(
tuple([atom([:tuple]), tuple([term(), term(), term()])])
|> union(tuple([atom([:atom]), atom()]))
|> union(tuple([atom([:other]), negation(atom())]))
)
)
assert typecheck!(
[x],
cond do
:erlang.orelse(is_atom(x), is_tuple(x)) -> {:atom_or_tuple, x}
is_binary(x) -> {:binary, x}
true -> {:other, x}
end
)
|> equal?(
dynamic(
tuple([atom([:atom_or_tuple]), term()])
|> union(tuple([atom([:binary]), binary()]))
|> union(
tuple([atom([:other]), negation(union(binary(), union(atom(), tuple())))])
)
)
)
end
test "and reports violations" do
assert typeerror!([x = 123], x and true) =~ """
the following conditional expression will always fail:
@@ -3156,6 +3223,10 @@ defmodule Module.Types.ExprTest do
"""
end
test ":into with non-returning collectable" do
assert typecheck!([list], for(x <- list, into: raise("oops"), do: x)) == none()
end
test ":reduce checks" do
assert typecheck!(
[list],
@@ -320,7 +320,7 @@ defmodule Module.Types.InferTest do
]
end
test "from defguard (regression with large code generation)", config do
test "from single defguard (regression with large code generation)", config do
# As long as it type checks in time, we are fine,
# but it should infer Macro.t in the future.
infer config do
@@ -334,6 +334,32 @@ defmodule Module.Types.InferTest do
end
end
test "from multiple defguard (regression with large code generation)", config do
infer config do
defguardp is_a(meta) when meta.m != :ppppp or meta.t != :nnnnn
defguardp is_b(meta) when is_nil(meta.s) or not meta.config.s.tunable
defguardp is_c(meta) when is_nil(meta.k) or not meta.config.k.tunable
defguardp is_d(meta) when is_nil(meta.t) or not meta.config.t.tunable
defguardp is_e(meta) when not meta.config.m.tunable
defguardp is_f(meta) when not meta.config.v.tunable
defguardp is_all_1(meta)
when is_a(meta) and is_b(meta) and is_c(meta) and is_d(meta) and
is_e(meta) and is_f(meta)
defguardp is_all_2(meta)
when (meta.m != :ppppp or meta.t != :nnnnn) and
(is_nil(meta.s) or not meta.config.s.tunable) and
(is_nil(meta.k) or not meta.config.k.tunable) and
(is_nil(meta.t) or not meta.config.t.tunable) and
not meta.config.m.tunable and
not meta.config.v.tunable
def fun_1(%{meta: meta} = _data) when is_all_1(meta), do: :something
def fun_2(%{meta: meta} = _data) when is_all_2(meta), do: :something
end
end
test "from defaults (regression with multiple clauses)", config do
types =
infer config do
@@ -1386,7 +1386,11 @@ defmodule Module.Types.PatternTest do
end
test "maps in patterns" do
assert precise?([%{}])
assert precise?([%{ok: _}])
assert precise?([%_{}])
assert precise?([%x{}])
assert precise?([%URI{}])
assert precise?([%URI{path: _}])
refute precise?([%{ok: 123}])
+9
View File
@@ -232,6 +232,11 @@ defmodule StringTest do
assert String.downcase("Σ ΣΑΣ Σ", :greek) == "σ σας σ"
assert String.downcase("ΜΕΣ'ΑΠΟ", :greek) == "μεσ'απο"
assert String.downcase("ΑΣ'ΤΟΥΣ", :greek) == "ασ'τους"
assert String.downcase("AΣ", :greek) == "aς"
assert String.downcase("AΣA", :greek) == "aσa"
assert String.downcase("A'Σ", :greek) == "a'ς"
assert String.downcase("İΣ", :greek) == "i̇ς"
assert String.downcase("ⅠΣ", :greek) == "ⅰς"
end
test "downcase/1 with ascii" do
@@ -274,6 +279,10 @@ defmodule StringTest do
assert String.capitalize("İii", :turkic) == "İii"
assert String.capitalize("Iıı", :turkic) == "Iıı"
assert String.capitalize("AΣ", :greek) == "Aς"
assert String.capitalize("iΣ", :greek) == "Iς"
assert String.capitalize("1Σ", :greek) == "1σ"
assert String.capitalize(<<138, ?B, ?C>>) == <<138, ?b, ?c>>
assert String.capitalize(<<225, 158, 128, 225, 158, 185, 225>>) ==
+15
View File
@@ -630,6 +630,21 @@ defmodule TaskTest do
[{task2, nil}, {task3, {:exit, :normal}}]
end
test "returns once all tasks have replied below limit" do
parent = self()
pid =
spawn(fn ->
task = Task.async(fn -> :done end)
result = Task.yield_many([task], limit: 2, timeout: :infinity)
send(parent, {self(), result})
end)
on_exit(fn -> Process.exit(pid, :kill) end)
assert_receive {^pid, [{%Task{}, {:ok, :done}}]}, 500
end
test "returns results from multiple tasks with limit and on timeout" do
Process.flag(:trap_exit, true)
task1 = Task.async(fn -> Process.sleep(:infinity) end)
+115 -38
View File
@@ -46,6 +46,35 @@ rangify = fn [head | tail] ->
[{first, last} | acc]
end
expand_range = fn range ->
[range | _] = :binary.split(range, " ")
case :binary.split(range, "..") do
[first] ->
[String.to_integer(first, 16)]
[first, last] ->
Enum.to_list(String.to_integer(first, 16)..String.to_integer(last, 16))
end
end
other_cased_letters =
Path.join(__DIR__, "PropList.txt")
|> File.read!()
|> String.split(["\r\n", "\n"])
|> Enum.reduce([], fn line, acc ->
case :binary.split(line, ";") do
[range, <<" Other_Lowercase", _::binary>>] ->
expand_range.(range) ++ acc
[range, <<" Other_Uppercase", _::binary>>] ->
expand_range.(range) ++ acc
_ ->
acc
end
end)
# A character is case ignorable if:
#
# Word_Break(C) = MidLetter or MidNumLet or Single_Quote, or
@@ -152,6 +181,17 @@ case_ignorable_categories = :binary.compile_pattern(["Mn", "Me", "Cf", "Lm", "Sk
{cacc, lacc, iacc, wacc, dacc, kacc}
end)
cased_letters = Enum.sort(Enum.uniq(cased_letters ++ other_cased_letters), :desc)
case_ignorable = Enum.sort(Enum.uniq(case_ignorable), :desc)
case_ignorable_map =
Enum.reduce(case_ignorable, %{}, fn codepoint, acc ->
:maps.put(codepoint, true, acc)
end)
cased_non_ignorable = Enum.reject(cased_letters, &:maps.is_key(&1, case_ignorable_map))
defmodule String.Unicode do
@moduledoc false
def version, do: {17, 0, 0}
@@ -236,6 +276,19 @@ defmodule String.Unicode do
# Downcase
def downcase_greek(string, acc) do
string
|> greek_final_sigma([], false)
|> downcase(acc, :default)
end
# Used by capitalize/2 to preserve the context supplied by the first grapheme.
def downcase_greek(string, acc, before) when is_binary(before) do
string
|> greek_final_sigma([], cased_letter_context(before, false))
|> downcase(acc, :default)
end
# Turkic İ -> i
def downcase(<<unquote(@letter_I_dot_above), rest::bits>>, acc, mode) do
char = if mode == :turkic, do: @letter_i, else: <<@letter_i, @combining_dot_above>>
@@ -255,14 +308,7 @@ defmodule String.Unicode do
# Greek sigma
def downcase(<<@letter_sigma, rest::bits>>, acc, mode) do
downcased =
if mode == :greek and cased_letter_list?(acc) and not cased_letter_binary?(rest) do
@letter_small_sigma_final
else
@letter_small_sigma
end
downcase(rest, [downcased | acc], mode)
downcase(rest, [@letter_small_sigma | acc], mode)
end
conditional_downcase = [@letter_I, @letter_I_dot_above, @letter_sigma]
@@ -300,49 +346,80 @@ defmodule String.Unicode do
# Sigma handling
defp greek_final_sigma(string, acc, cased_before) do
case :binary.match(string, @letter_sigma) do
{index, _length} ->
<<before::binary-size(^index), @letter_sigma, rest::bits>> = string
cased_before = cased_letter_context(before, cased_before)
downcased =
if cased_before and not cased_letter_binary?(rest) do
@letter_small_sigma_final
else
@letter_small_sigma
end
greek_final_sigma(rest, [downcased, before | acc], true)
:nomatch ->
if acc == [] do
string
else
IO.iodata_to_binary(:lists.reverse([string | acc]))
end
end
end
defp cased_letter_binary?(<<codepoint::utf8, rest::bits>>) do
if case_ignorable?(codepoint) do
cased_letter_binary?(rest)
else
cased_letter?(codepoint)
case case_context(codepoint) do
:ignorable -> cased_letter_binary?(rest)
:cased -> true
:other -> false
end
end
defp cased_letter_binary?(_), do: false
defp cased_letter_list?([<<codepoint::utf8>> | rest]) do
if case_ignorable?(codepoint) do
cased_letter_list?(rest)
else
cased_letter?(codepoint)
end
end
defp cased_letter_list?(_), do: false
for {first, last} <- rangify.(cased_letters) do
if first == last do
defp cased_letter?(unquote(first)), do: true
else
defp cased_letter?(codepoint)
when codepoint >= unquote(first) and codepoint <= unquote(last),
do: true
end
end
defp cased_letter?(_), do: false
for {first, last} <- rangify.(case_ignorable) do
if first == last do
defp case_ignorable?(unquote(first)), do: true
defp case_context(unquote(first)), do: :ignorable
else
defp case_ignorable?(codepoint)
defp case_context(codepoint)
when codepoint >= unquote(first) and codepoint <= unquote(last),
do: true
do: :ignorable
end
end
defp case_ignorable?(_), do: false
for {first, last} <- rangify.(cased_non_ignorable) do
if first == last do
defp case_context(unquote(first)), do: :cased
else
defp case_context(codepoint)
when codepoint >= unquote(first) and codepoint <= unquote(last),
do: :cased
end
end
defp case_context(_codepoint), do: :other
defp cased_letter_context(<<codepoint::utf8, rest::bits>>, cased_before) do
cased_before = update_cased_context(codepoint, cased_before)
cased_letter_context(rest, cased_before)
end
defp cased_letter_context(<<_byte, rest::bits>>, _cased_before) do
cased_letter_context(rest, false)
end
defp cased_letter_context("", cased_before), do: cased_before
defp update_cased_context(codepoint, cased_before) when is_integer(codepoint) do
case case_context(codepoint) do
:ignorable -> cased_before
:cased -> true
:other -> false
end
end
# Upcase
+1 -1
View File
@@ -785,7 +785,7 @@ defmodule IEx do
__break__!(ast, module, fun, args, guards, stops, env)
end
def __break__!(ast, _stops) do
def __break__!(ast, _stops, _env) do
raise_unknown_break_ast!(ast)
end
+6
View File
@@ -102,6 +102,12 @@ defmodule IEx.HelpersTest do
fn -> break!(PryExampleModule, :unknown, 2) end
end
test "errors when setting up a break for unknown expression" do
assert_raise ArgumentError, ~r"unknown expression to break on", fn ->
break!(123)
end
end
test "errors for non-Elixir modules" do
assert_raise RuntimeError,
"could not set breakpoint, module :maps was not written in Elixir",