Compare commits

...
24 Commits
Author SHA1 Message Date
José Valim 7837cc5c59 Release v1.11.0 2020-10-06 15:43:09 +02:00
Daniil Fedotov b96fc39422 Restrict Logger.configure on config_change callback (#10388)
The `config_change` callback is called from `application_controller` process,
which is receiving env updates on releases. `Logger.configure` does a blocking
call to the `Logger` process, which calls `Application.put_env`

This causes a crash on logger config update.

The fix is to call `Logger.configure` only for the config that have not yet
been persisted.
2020-10-03 08:33:31 +02:00
Waldemar Rachwał e1cb898198 Take --dot-iex into account after shell respawn (#10384) 2020-10-01 20:36:14 +02:00
Dave Lucia 88e2894925 Enum.max_by/3 and Enum.min_by/3 dialyzer fixes (#10383) 2020-10-01 13:23:19 +02:00
José Valim f7cb5ae053 Fix inference of open maps with non-singleton keys
Closes #10371.
2020-09-29 13:11:22 +02:00
José Valim b4845e6854 Update iso.ex 2020-09-25 13:34:38 +02:00
José Valim 0f1200f99b Add a note on async_stream+take 2020-09-25 13:33:15 +02:00
José Valim cec1a7190b Make config/runtime.exs deep merge, closes #10365 2020-09-25 13:32:54 +02:00
Gabriel Alves 53d1b98a05 Raise instead of crashing on when trying to use list operators in guards (#10363) 2020-09-23 14:30:39 +02:00
José Valim 4828f11cd2 Keep invoking loadconfig on cli boot 2020-09-22 15:47:35 +02:00
José Valim 501271cad0 Make replace and replace! in Keyword consistent, closes #10345 2020-09-18 16:13:16 +02:00
José Valim fd4a6ad5cf Handle empty map in other places, closes #10343 2020-09-18 10:53:56 +02:00
José Valim 196aab87ca Align doc chunks return with EEP 48 2020-09-18 10:41:39 +02:00
José Valim 59b266506a Append space on middle and start expr if necessary, closes #10340 2020-09-17 17:11:28 +02:00
José Valim 1c8e625d77 Speed up compilation with multiple describe 2020-09-17 14:24:54 +02:00
José Valim f9d7adbfdf Introduce register_test/6 to speed up test definition 2020-09-17 10:57:00 +02:00
José Valim f1721c8a58 Reduce amount of injected code in case 2020-09-17 10:29:40 +02:00
José Valim 57abca4c16 Properly handle reduce in types, closes #10224 2020-09-14 23:02:07 +02:00
José Valim 8e9ece0d62 More typing improvements (#10333)
1. Move push_expr_stack to unify and keep helpers typing free

2. Add get_var! when accessing variables so it helps us find
   variables that have not been properly processed

3. Properly process all variables found missing on step 2
2020-09-14 22:45:34 +02:00
José Valim 54ba096d34 Remove boolean and number as builtin types (#10329) 2020-09-14 22:45:34 +02:00
José Valim 6042fd0bcc Infer -> Unify 2020-09-14 22:45:34 +02:00
Eksperimental 475b73b46f Use proper casing in CHANGELOG (#10331) 2020-09-14 15:24:51 +02:00
José Valim 3cf6069ad9 Fix typo in CHANGELOG 2020-09-14 09:43:19 +02:00
José Valim 48cee5aaa5 Release v1.11.0-rc.0 2020-09-14 09:16:34 +02:00
39 changed files with 467 additions and 303 deletions
+11 -7
View File
@@ -243,7 +243,7 @@ The Calendar module ships with a new `Calendar.strftime/3` function, which provi
Mix also includes two new tasks: `mix app.config`, for application runtime configuration, and `mix test.coverage`, which generates aggregated coverage reports for umbrella projects and for test suites partitioned across processes.
## v1.11.0-dev
## v1.11.0 (2020-10-06)
### 1. Enhancements
@@ -284,6 +284,7 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
* [ExUnit] Add `@tag :tmp_dir` support to ExUnit. The temporary directory is automatically created and pruned before each test
* [ExUnit] Add file and line to ExUnit's `--trace`
* [ExUnit.Assertion] Allow receive timeouts to be computed at runtime
* [ExUnit.Case] Add `register_test/6` to speed up compilation of custom tests
* [ExUnit.Doctest] Allow users to add tags to doctests
#### IEx
@@ -305,17 +306,17 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
* [mix] Allow requirements for a Mix task to be listed via the `@requirements` module attribute
* [mix] Allow optional dependencies to be defined in `:extra_applications` and `:applications`
* [mix app.config] Add new `mix app.config` task that compiles applications and loads runtime configuration
* [mix archive.install] Support `--repo` option on hex packages
* [mix archive.install] Support `--repo` option on Hex packages
* [mix compile] Support the `__mix_recompile__?/0` callback for custom behaviour on when Mix should recompile a given module
* [mix compile.elixir] Mark modules for path dependencies as "Export dependencies" if they changed but their public interface is the same
* [mix compile.elixir] Track application boundaries in the Elixir compiler. If you invoke code from Erlang or Elixir standard libraries and you don't depend on the proper applications, a warning will be emitted. A warning will also be emitted if you invoke code from an umbrella sibling that you don't depend on - effectively forbidding cyclic dependencies between apps
* [mix deps] Sort the dependencies alphabetically before printing
* [mix deps] Use `origin/HEAD` as the default git ref in dependencies
* [mix deps] Use `origin/HEAD` as the default Git ref in dependencies
* [mix deps] Redact Git `username`/`password` in output log
* [mix deps] Support rebar3's `git_subdir` resource type
* [mix deps.compile] Allow local deps to be skipped on `mix deps.compile`
* [mix deps.unlock] Print which dependencies get unlocked when using the `--unused` flag
* [mix escript.install] Support `--repo` option on hex packages
* [mix escript.install] Support `--repo` option on Hex packages
* [mix new] Add `@impl` to application generated by `mix new --sup`
* [mix release] Enable overriding `sys.config` location via `RELEASE_SYS_CONFIG` env var
* [mix release] Boot a release under configuration in interactive mode and then swap to embedded mode (if running on Erlang/OTP 23+)
@@ -343,9 +344,12 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
* [Kernel] Properly parse `&//2` (i.e. the capture of the division operator)
* [Kernel] Raise `CompileError` when trying to define reserved types
* [Kernel] Improve compiler error message when using `|` in a `def` signature
* [Kernel] Improve error message when trying to use invalid list operators in guards
* [Kernel.SpecialForms] Add `|/2` to the list of special forms to avoid inconsistent behaviour on overrides
* [Keyword] Enforce keys to be atoms in `Keyword.keys/1`
* [URI] `URI.decode_query/2` emits an empty string for parameters without values, according to https://url.spec.whatwg.org/#application/x-www-form-urlencoded
* [Record] Keep lexical ordering when creating records
* [Registry] Do not crash when a process with key-value has been registered using `:via` and it fails to start on `init`
* [URI] `URI.decode_query/2` emits an empty string for parameters without values, according to [URL's living standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded) - note this behaviour is not specified in the spec implemented by the URI module, so the living standard was chosen
* [Version] Add defaults and enforce keys in `Version` struct
#### ExUnit
@@ -357,7 +361,7 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
#### IEx
* [IEx] Fix tokenizer emitting repeated warnings in the REPL
* [IEx] Ensure `dot_iex_path` is preserved when restarting the evaluator
* [IEx] Ensure `--dot-iex` is preserved when restarting the evaluator and after shell respawn
* [IEx.Pry] Ensure `IEx.pry` can be triggered more than twice when invoked from the same process
#### Mix
@@ -395,7 +399,7 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
* [Supervisor] Deprecate `Supervisor.start_child/2` and `Supervisor.terminate_child/2` in favor of `DynamicSupervisor`
* [Supervisor.Spec] Deprecate `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` in favor of the new typespecs
* [System] Deprecate `System.stracktrace/0` in favor of `__STACKTRACE__`
* [System] Deprecate `System.stacktrace/0` in favor of `__STACKTRACE__`
#### Mix
+1 -2
View File
@@ -2,8 +2,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
#CANONICAL := vMAJOR.MINOR/
CANONICAL ?= master/
CANONICAL := v1.11/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
+2 -3
View File
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
| Elixir version | Support
| -------------- | ------------------------------
| 1.11 | Development
| 1.10 | Bug fixes and security patches
| 1.11 | Bug fixes and security patches
| 1.10 | Security patches only
| 1.9 | Security patches only
| 1.8 | Security patches only
| 1.7 | Security patches only
| 1.6 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.11.0-dev
1.11.0
+16 -12
View File
@@ -65,10 +65,10 @@ defmodule EEx.Tokenizer do
error
{:ok, expr, new_line, new_column, rest} ->
key =
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, tokens} -> token_key(tokens)
{:error, _, _, _} -> :expr
{:ok, tokens} -> token_key(tokens, expr)
{:error, _, _, _} -> {:expr, expr}
end
{rest, new_line, new_column, buffer} =
@@ -122,38 +122,42 @@ defmodule EEx.Tokenizer do
end
# Receives tokens and check if it is a start, middle or an end token.
defp token_key(tokens) do
defp token_key(tokens, expr) do
case {tokens, Enum.reverse(tokens)} do
{[{:end, _} | _], [{:do, _} | _]} ->
:middle_expr
{:middle_expr, expr}
{_, [{:do, _} | _]} ->
:start_expr
{:start_expr, maybe_append_space(expr)}
{_, [{:block_identifier, _, _} | _]} ->
:middle_expr
{:middle_expr, maybe_append_space(expr)}
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
:middle_expr
{:middle_expr, expr}
{_, [{:stab_op, _, _} | reverse_tokens]} ->
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
if end_index > fn_index do
:start_expr
{:start_expr, expr}
else
:middle_expr
{:middle_expr, expr}
end
{tokens, _} ->
case Enum.drop_while(tokens, &closing_bracket?/1) do
[{:end, _} | _] -> :end_expr
_ -> :expr
[{:end, _} | _] -> {:end_expr, expr}
_ -> {:expr, expr}
end
end
end
defp maybe_append_space([?\s]), do: [?\s]
defp maybe_append_space([h]), do: [h, ?\s]
defp maybe_append_space([h | t]), do: [h | maybe_append_space(t)]
defp closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
defp closing_bracket?(_), do: false
+11
View File
@@ -127,6 +127,17 @@ defmodule EExTest do
end
test "trim mode with no spaces" do
string = """
<%=if true do%>
this
<%else%>
that
<%end%>
"""
expected = "\n this\n"
assert_eval(expected, string, [], trim: true)
string = """
<%=cond do%>
<%false ->%>
+2 -2
View File
@@ -111,9 +111,9 @@ defmodule Behaviour do
end
end
defp __behaviour__doc_value(:none), do: nil
defp __behaviour__doc_value(:hidden), do: false
defp __behaviour__doc_value(%{"en" => doc}), do: doc
defp __behaviour__doc_value(:hidden), do: false
defp __behaviour__doc_value(_), do: nil
import unquote(__MODULE__)
end
+6
View File
@@ -8,6 +8,12 @@ defmodule Calendar.ISO do
applied for all time, consequently the dates give different results
before the year 1583 from when the Gregorian calendar was adopted.
Given this is the default calendar used by Elixir, it has one
difference compared to the ISO8601 specification in that it allows
a whitespace instead of `T` as a seperator between date and times
both when parsing and formatting. Strict formatting can be done
by using the `to_iso8601` found in `NaiveDateTime` and `DateTime`.
Note that while ISO 8601 allows times and datetimes to specify
24:00:00 as the zero hour of the next day, this notation is not
supported by Elixir.
+1 -1
View File
@@ -1348,7 +1348,7 @@ defmodule Code do
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
when annotation: :erl_anno.anno(),
beam_language: :elixir | :erlang | atom(),
doc_content: %{required(binary) => binary} | :none | :hidden,
doc_content: %{optional(binary) => binary} | :none | :hidden,
doc_element:
{{kind :: atom, function_name :: atom, arity}, annotation, signature, doc_content,
metadata},
+10 -14
View File
@@ -1593,7 +1593,11 @@ defmodule Enum do
defp max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :lt)
@doc false
@spec max_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
@spec max_by(
t,
(element -> any),
(() -> empty_result) | (element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
def max_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
@@ -1644,12 +1648,6 @@ defmodule Enum do
nil
"""
@spec max_by(
t,
(element -> any),
(element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
@spec max_by(
t,
(element -> any),
@@ -1767,7 +1765,11 @@ defmodule Enum do
defp min_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt)
@doc false
@spec min_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
@spec min_by(
t,
(element -> any),
(() -> empty_result) | (element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
def min_by(enumerable, fun, empty_fallback)
when is_function(fun, 1) and is_function(empty_fallback, 0) do
@@ -1818,12 +1820,6 @@ defmodule Enum do
nil
"""
@spec min_by(
t,
(element -> any),
(element, element -> boolean) | module()
) :: element | empty_result
when empty_result: any
@spec min_by(
t,
(element -> any),
+2 -3
View File
@@ -23,9 +23,8 @@ defmodule Kernel.Typespec do
for {{:type, name, arity}, _, _, doc, _} <- docs do
case doc do
%{"en" => doc_string} -> {{name, arity}, doc_string}
:none -> {{name, arity}, nil}
# Hidden or unknown format are ignored
_ -> {{name, arity}, false}
:hidden -> {{name, arity}, false}
_ -> {{name, arity}, nil}
end
end
+14 -5
View File
@@ -668,10 +668,19 @@ defmodule Keyword do
@doc since: "1.11.0"
@spec replace(t, key, value) :: t
def replace(keywords, key, value) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
{^key, _} -> [{key, value} | delete(keywords, key)]
false -> keywords
end
do_replace(keywords, key, value)
end
defp do_replace([{key, _} | keywords], key, value) do
[{key, value} | delete(keywords, key)]
end
defp do_replace([{_, _} = e | keywords], key, value) do
[e | do_replace(keywords, key, value)]
end
defp do_replace([], _key, _value) do
[]
end
@doc """
@@ -704,7 +713,7 @@ defmodule Keyword do
[e | replace!(keywords, key, value, original)]
end
defp replace!([], key, _value, original) when is_atom(key) do
defp replace!([], key, _value, original) do
raise(KeyError, key: key, term: original)
end
+6 -6
View File
@@ -6,7 +6,7 @@ defmodule Module.Types do
end
import Module.Types.Helpers
alias Module.Types.{Expr, Pattern, Infer}
alias Module.Types.{Expr, Pattern, Unify}
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
@@ -57,7 +57,7 @@ defmodule Module.Types do
end
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
head_stack = push_expr_stack(def_expr, stack)
head_stack = Unify.push_expr_stack(def_expr, stack)
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
@@ -300,9 +300,9 @@ defmodule Module.Types do
[
"incompatible types:\n\n ",
Infer.format_type(left, simplify_left?),
Unify.format_type(left, simplify_left?),
" !~ ",
Infer.format_type(right, simplify_right?),
Unify.format_type(right, simplify_right?),
"\n\n",
format_expr(expr, location),
traces,
@@ -352,7 +352,7 @@ defmodule Module.Types do
"where \"",
Macro.to_string(var),
"\" was given the type ",
Infer.format_type(type, simplify?),
Unify.format_type(type, simplify?),
hint,
" in:\n\n # ",
format_location(location),
@@ -518,8 +518,8 @@ defmodule Module.Types do
defp map_type?(_other), do: false
defp atom_type?(:atom), do: true
defp atom_type?(:boolean), do: true
defp atom_type?({:atom, _}), do: false
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
defp atom_type?(_other), do: false
defp integer_type?(:integer), do: true
+21 -17
View File
@@ -2,7 +2,7 @@ defmodule Module.Types.Expr do
@moduledoc false
alias Module.Types.{Of, Pattern}
import Module.Types.{Helpers, Infer}
import Module.Types.{Helpers, Unify}
def of_expr(expr, %{context: stack_context} = stack, context) when stack_context != :expr do
of_expr(expr, %{stack | context: :expr}, context)
@@ -109,8 +109,7 @@ defmodule Module.Types.Expr do
# var
def of_expr(var, _stack, context) when is_var(var) do
{type, context} = new_var(var, context)
{:ok, type, context}
{:ok, get_var!(var, context), context}
end
# {left, right}
@@ -278,10 +277,19 @@ defmodule Module.Types.Expr do
# for pat <- expr do expr end
def of_expr({:for, _meta, args} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
case reduce_ok(args, context, &for_clause(&1, stack, &2)) do
{:ok, _context} -> {:ok, :dynamic, context}
{:error, reason} -> {:error, reason}
with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)),
{:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do
if opts[:reduce] do
with :ok <- of_clauses(block, stack, context) do
{:ok, :dynamic, context}
end
else
with {:ok, _type, context} <- of_expr(block, stack, context) do
{:ok, :dynamic, context}
end
end
end
end
@@ -379,6 +387,13 @@ defmodule Module.Types.Expr do
do: {:ok, context}
end
defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do
# TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
do: {:ok, context}
end
defp for_clause(list, stack, context) when is_list(list) do
reduce_ok(list, context, &for_option(&1, stack, &2))
end
@@ -399,17 +414,6 @@ defmodule Module.Types.Expr do
{:ok, context}
end
defp for_option({:do, [{:->, _, [pattern, body]}]}, stack, context) do
case Pattern.of_pattern(pattern, stack, context) do
{:ok, _pattern_type, context} -> of_expr_context(body, stack, context)
{:error, reason} -> {:error, reason}
end
end
defp for_option({:do, body}, stack, context) do
of_expr_context(body, stack, context)
end
defp with_clause({:<-, _, [left, expr]}, stack, context) do
{pattern, guards} = extract_head([left])
-10
View File
@@ -25,16 +25,6 @@ defmodule Module.Types.Helpers do
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
"""
def push_expr_stack(expr, stack) do
%{stack | last_expr: expr}
end
@doc """
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
and stops on `{:error, reason}`.
+21 -16
View File
@@ -6,17 +6,27 @@ defmodule Module.Types.Of do
@prefix quote(do: ...)
@suffix quote(do: ...)
alias Module.Types.Infer
alias Module.ParallelChecker
import Module.Types.Helpers
import Module.Types.Unify
@doc """
Handles open maps (with dynamic => dynamic).
"""
def open_map(args, stack, context, fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
{:ok, {:map, pairs_to_unions(pairs, context) ++ [{:optional, :dynamic, :dynamic}]}, context}
pairs =
for {key, value} <- pairs, not has_unbound_var?(key, context) do
if singleton?(key, context) do
{key, value}
else
{key, to_union([value, :dynamic], context)}
end
end
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
{:ok, {:map, triplets}, context}
end
end
@@ -25,7 +35,7 @@ defmodule Module.Types.Of do
"""
def closed_map(args, stack, context, fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
{:ok, {:map, pairs_to_unions(pairs, context)}, context}
{:ok, {:map, closed_to_unions(pairs, context)}, context}
end
end
@@ -37,10 +47,10 @@ defmodule Module.Types.Of do
end)
end
defp pairs_to_unions([{key, value}], _context), do: [{:required, key, value}]
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
defp pairs_to_unions(pairs, context) do
case Enum.split_with(pairs, fn {key, _value} -> Infer.has_unbound_var?(key, context) end) do
defp closed_to_unions(pairs, context) do
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
{[], pairs} -> pairs_to_unions(pairs, [], context)
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
end
@@ -57,17 +67,17 @@ defmodule Module.Types.Of do
find_subtype_values(ahead, key, context) ++
find_subtype_values(behind, key, context)
pairs_to_unions(ahead, [{key, Infer.to_union(all_values, context)} | behind], context)
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
end
defp pairs_to_unions([], acc, context) do
acc
|> Enum.sort(&Infer.subtype?(elem(&1, 0), elem(&2, 0), context))
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|> Enum.map(fn {key, value} -> {:required, key, value} end)
end
defp find_subtype_values(pairs, key, context) do
for {pair_key, pair_value} <- pairs, Infer.subtype?(pair_key, key, context), do: pair_value
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
end
defp find_matching_values([{key, value} | ahead], key, acc, values) do
@@ -153,16 +163,11 @@ defmodule Module.Types.Of do
true ->
with {:ok, type, context} <- fun.(expr, stack, context),
{:ok, _type, context} <- Infer.unify(type, expected_type, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
# TODO: Remove this clause once we properly handle comprehensions
defp binary_segment({:<-, _, _}, _stack, context, _fun) do
{:ok, context}
end
# Collect binary type specifiers,
# from `<<pattern::integer-size(10)>>` collect `integer`
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
@@ -173,7 +178,7 @@ defmodule Module.Types.Of do
fun.(other)
end
defp binary_type(:expr, {:float, _, _}), do: :number
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
+53 -56
View File
@@ -2,7 +2,7 @@ defmodule Module.Types.Pattern do
@moduledoc false
alias Module.Types.Of
import Module.Types.{Helpers, Infer}
import Module.Types.{Helpers, Unify}
@doc """
Handles patterns and guards at once.
@@ -129,8 +129,8 @@ defmodule Module.Types.Pattern do
end
# ^var
def of_pattern({:^, _meta, [var]}, stack, context) do
of_pattern(var, stack, context)
def of_pattern({:^, _meta, [var]}, _stack, context) do
{:ok, get_var!(var, context), context}
end
# var
@@ -194,16 +194,11 @@ defmodule Module.Types.Pattern do
end
end
# %^var{...}
def of_pattern({:%, meta1, [{:^, _meta2, [var]}, args]}, stack, context) do
of_pattern({:%, meta1, [var, args]}, stack, context)
end
# %var{...}
# %var{...} and %^var{...}
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) do
stack = push_expr_stack(expr, stack)
with {var_type, context} = new_var(var, context),
with {:ok, var_type, context} = of_pattern(var, stack, context),
{:ok, _, context} <- unify(var_type, :atom, stack, context),
{:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
{:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context}
@@ -217,43 +212,45 @@ defmodule Module.Types.Pattern do
## GUARDS
# TODO: Some guards can be changed to intersection types or higher order types
@boolean {:union, [{:atom, true}, {:atom, false}]}
@number {:union, [:integer, :float]}
@guard_functions %{
{:is_atom, 1} => {[:atom], :boolean},
{:is_binary, 1} => {[:binary], :boolean},
{:is_bitstring, 1} => {[:binary], :boolean},
{:is_boolean, 1} => {[:boolean], :boolean},
{:is_float, 1} => {[:float], :boolean},
{:is_function, 1} => {[:fun], :boolean},
{:is_function, 2} => {[:fun, :integer], :boolean},
{:is_integer, 1} => {[:integer], :boolean},
{:is_list, 1} => {[{:list, :dynamic}], :boolean},
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], :boolean},
{:is_atom, 1} => {[:atom], @boolean},
{:is_binary, 1} => {[:binary], @boolean},
{:is_bitstring, 1} => {[:binary], @boolean},
{:is_boolean, 1} => {[@boolean], @boolean},
{:is_float, 1} => {[:float], @boolean},
{:is_function, 1} => {[:fun], @boolean},
{:is_function, 2} => {[:fun, :integer], @boolean},
{:is_integer, 1} => {[:integer], @boolean},
{:is_list, 1} => {[{:list, :dynamic}], @boolean},
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean},
{:is_map_key, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
{:is_number, 1} => {[:number], :boolean},
{:is_pid, 1} => {[:pid], :boolean},
{:is_port, 1} => {[:port], :boolean},
{:is_reference, 1} => {[:reference], :boolean},
{:is_tuple, 1} => {[:tuple], :boolean},
{:<, 2} => {[:dynamic, :dynamic], :boolean},
{:"=<", 2} => {[:dynamic, :dynamic], :boolean},
{:>, 2} => {[:dynamic, :dynamic], :boolean},
{:>=, 2} => {[:dynamic, :dynamic], :boolean},
{:"/=", 2} => {[:dynamic, :dynamic], :boolean},
{:"=/=", 2} => {[:dynamic, :dynamic], :boolean},
{:==, 2} => {[:dynamic, :dynamic], :boolean},
{:"=:=", 2} => {[:dynamic, :dynamic], :boolean},
{:*, 2} => {[:number, :number], :number},
{:+, 1} => {[:number], :number},
{:+, 2} => {[:number, :number], :number},
{:-, 1} => {[:number], :number},
{:-, 2} => {[:number, :number], :number},
{:/, 2} => {[:number, :number], :number},
{:abs, 1} => {[:number], :number},
{:ceil, 1} => {[:number], :integer},
{:floor, 1} => {[:number], :integer},
{:round, 1} => {[:number], :integer},
{:trunc, 1} => {[:number], :integer},
{:is_number, 1} => {[@number], @boolean},
{:is_pid, 1} => {[:pid], @boolean},
{:is_port, 1} => {[:port], @boolean},
{:is_reference, 1} => {[:reference], @boolean},
{:is_tuple, 1} => {[:tuple], @boolean},
{:<, 2} => {[:dynamic, :dynamic], @boolean},
{:"=<", 2} => {[:dynamic, :dynamic], @boolean},
{:>, 2} => {[:dynamic, :dynamic], @boolean},
{:>=, 2} => {[:dynamic, :dynamic], @boolean},
{:"/=", 2} => {[:dynamic, :dynamic], @boolean},
{:"=/=", 2} => {[:dynamic, :dynamic], @boolean},
{:==, 2} => {[:dynamic, :dynamic], @boolean},
{:"=:=", 2} => {[:dynamic, :dynamic], @boolean},
{:*, 2} => {[@number, @number], @number},
{:+, 1} => {[@number], @number},
{:+, 2} => {[@number, @number], @number},
{:-, 1} => {[@number], @number},
{:-, 2} => {[@number, @number], @number},
{:/, 2} => {[@number, @number], @number},
{:abs, 1} => {[@number], @number},
{:ceil, 1} => {[@number], :integer},
{:floor, 1} => {[@number], :integer},
{:round, 1} => {[@number], :integer},
{:trunc, 1} => {[@number], :integer},
{:element, 2} => {[:integer, :tuple], :dynamic},
{:hd, 1} => {[{:list, :dynamic}], :dynamic},
{:length, 1} => {[{:list, :dynamic}], :integer},
@@ -265,7 +262,7 @@ defmodule Module.Types.Pattern do
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
{:bit_size, 1} => {[:binary], :integer},
{:byte_size, 1} => {[:binary], :integer},
{:size, 1} => {[{:union, [:binary, :tuple]}], :boolean},
{:size, 1} => {[{:union, [:binary, :tuple]}], @boolean},
{:div, 2} => {[:integer, :integer], :integer},
{:rem, 2} => {[:integer, :integer], :integer},
{:node, 0} => {[], :atom},
@@ -276,15 +273,15 @@ defmodule Module.Types.Pattern do
{:bxor, 2} => {[:integer, :integer], :integer},
{:bsl, 2} => {[:integer, :integer], :integer},
{:bsr, 2} => {[:integer, :integer], :integer},
{:or, 2} => {[:boolean, :boolean], :boolean},
{:and, 2} => {[:boolean, :boolean], :boolean},
{:xor, 2} => {[:boolean, :boolean], :boolean},
{:not, 1} => {[:boolean], :boolean}
{:or, 2} => {[@boolean, @boolean], @boolean},
{:and, 2} => {[@boolean, @boolean], @boolean},
{:xor, 2} => {[@boolean, @boolean], @boolean},
{:not, 1} => {[@boolean], @boolean}
# Following guards are matched explicitly to handle
# type guard functions such as is_atom/1
# {:andalso, 2} => {[:boolean, :boolean], :boolean}
# {:orelse, 2} => {[:boolean, :boolean], :boolean}
# {:andalso, 2} => {[@boolean, @boolean], @boolean}
# {:orelse, 2} => {[@boolean, @boolean], @boolean}
}
@type_guards [
@@ -320,9 +317,9 @@ defmodule Module.Types.Pattern do
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
{:ok, context} <- merge_context_and(context, stack, left_context, right_context),
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
do: {:ok, :boolean, context}
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
{:ok, _, context} <- unify(right_type, @boolean, stack, context),
do: {:ok, @boolean, context}
end
def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, stack, context) do
@@ -332,8 +329,8 @@ defmodule Module.Types.Pattern do
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
do: {:ok, :boolean, context}
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
do: {:ok, @boolean, context}
end
# The unary operators + and - are special cased to avoid common warnings until
@@ -1,4 +1,4 @@
defmodule Module.Types.Infer do
defmodule Module.Types.Unify do
@moduledoc false
import Module.Types.Helpers
@@ -21,11 +21,6 @@ defmodule Module.Types.Infer do
# {:union, [type]}
# {:map, [{:required | :optional, key_type, value_type}]}
#
# TODO: Those types should be removed:
#
# :boolean
# :number
#
# Once new types are added, they should be considered in:
#
# * unify (all)
@@ -306,6 +301,23 @@ defmodule Module.Types.Infer do
defp error(type, reason, context), do: {:error, {type, reason, context}}
@doc """
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
"""
def push_expr_stack(expr, stack) do
%{stack | last_expr: expr}
end
@doc """
Gets a variable.
"""
def get_var!(var, context) do
Map.fetch!(context.vars, var_name(var))
end
@doc """
Adds a variable to the typing context and returns its type variable.
If the variable has already been added, return the existing type variable.
@@ -487,14 +499,33 @@ defmodule Module.Types.Infer do
def has_unbound_var?(_type, _context), do: false
@doc """
Returns true if it is a singleton type.
Only atoms are singleton types. Unbound vars are not
considered singleton types.
"""
def singleton?({:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> singleton?(type, context)
end
end
def singleton?({:atom, _}, _context), do: true
def singleton?(_type, _context), do: false
@doc """
Checks if the first argument is a subtype of the second argument.
This function assumes that:
* dynamic is not considered a subtype of all other types but the top type
* unbound variables are not subtype of anything
* dynamic is not considered a subtype of all other types but the top type.
This allows this function can be used for ordering, in other cases, you
may need to check for both sides
"""
def subtype?(type, type, _context), do: true
@@ -515,11 +546,6 @@ defmodule Module.Types.Infer do
def subtype?(_, :dynamic, _context), do: true
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
def subtype?({:atom, boolean}, :boolean, _context) when is_boolean(boolean), do: true
def subtype?(:boolean, :atom, _context), do: true
def subtype?(:float, :number, _context), do: true
def subtype?(:integer, :number, _context), do: true
# Composite
def subtype?({:tuple, _, _}, :tuple, _context), do: true
+32
View File
@@ -491,6 +491,38 @@ defmodule Task do
stream = Task.async_stream(collection, Mod, :expensive_fun, [], ordered: false)
Stream.run(stream)
## Attention: async + take
Given items in an async stream are processed concurrently, doing
`async_stream` followed by `Enum.take/2` may cause more items than
requested to be processed. Let's see an example:
1..100
|> Task.async_stream(fn i ->
Process.sleep(100)
IO.puts(to_string(i))
end)
|> Enum.take(10)
For a machine with 8 cores, the above will process 16 items instead
of 10. The reason is that `async_stream/5` always have 8 elements
processing at once. So by the time `Enum` says it got all elements
it needed, there are still 6 elements left to be processed.
The solution here is to use `Stream.take/2` instead of `Enum.take/2`
to filter elements before-hand:
1..100
|> Stream.take(10)
|> Task.async_stream(fn i ->
Process.sleep(100)
IO.puts(to_string(i))
end)
|> Enum.to_list()
If for some reason you cannot take the elements before hand,
you can use `:max_concurrency` to limit how many elements
may be over processed at the cost of reducing concurrency.
"""
@doc since: "1.4.0"
@spec async_stream(Enumerable.t(), module, atom, [term], keyword) :: Enumerable.t()
@@ -174,4 +174,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.8]: https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md#4-hard-deprecations
[v1.9]: https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md#4-hard-deprecations
[v1.10]: https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md#4-hard-deprecations
[v1.11]: https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#4-hard-deprecations
[v1.11]: https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.md#4-hard-deprecations
+1 -1
View File
@@ -166,8 +166,8 @@ bootstrap_files() ->
<<"lib/elixir/lib/module/locals_tracker.ex">>,
<<"lib/elixir/lib/module/parallel_checker.ex">>,
<<"lib/elixir/lib/module/types/helpers.ex">>,
<<"lib/elixir/lib/module/types/unify.ex">>,
<<"lib/elixir/lib/module/types/of.ex">>,
<<"lib/elixir/lib/module/types/infer.ex">>,
<<"lib/elixir/lib/module/types/pattern.ex">>,
<<"lib/elixir/lib/module/types/expr.ex">>,
<<"lib/elixir/lib/module/types.ex">>,
+1 -1
View File
@@ -27,9 +27,9 @@ guard_op('orelse', 2) ->
guard_op(Op, Arity) ->
try erl_internal:op_type(Op, Arity) of
arith -> true;
list -> true;
comp -> true;
bool -> true;
list -> false;
send -> false
catch
_:_ -> false
@@ -240,6 +240,19 @@ defmodule Kernel.ComprehensionTest do
assert acc == %{1 => 'olleh', 3 => 'olleh'}
end
test "for comprehensions with matched reduce" do
acc =
for entry <- [1, 2, 3], reduce: {:ok, nil} do
{:ok, _} ->
{:ok, entry}
{:error, _} = error ->
error
end
assert acc == {:ok, 3}
end
## List generators (inlined by the compiler)
test "list for comprehensions" do
@@ -310,6 +310,22 @@ defmodule Kernel.GuardTest do
end
end
assert_raise CompileError,
~r"cannot invoke remote function :erlang\.\+\+/2 inside guards",
fn ->
defmodule ListSubtractionUsage do
defguard foo(list) when list ++ []
end
end
assert_raise CompileError,
~r"cannot invoke remote function :erlang\.\-\-/2 inside guards",
fn ->
defmodule ListSubtractionUsage do
defguard foo(list) when list -- []
end
end
assert_raise CompileError, ~r"invalid expression in guard", fn ->
defmodule LocalCallUsage do
defguard foo(local, call) when local.(call)
@@ -297,7 +297,7 @@ defmodule Module.Types.MapTest do
end
end
test "with bound var keys" do
test "map creation with bound var keys" do
assert quoted_expr(
[atom, bool, true = var],
[is_atom(atom) and is_boolean(bool)],
@@ -307,7 +307,8 @@ defmodule Module.Types.MapTest do
{:map,
[
{:required, {:atom, true}, {:atom, true}},
{:required, :boolean, {:union, [{:atom, :bool}, {:atom, true}]}},
{:required, {:union, [atom: true, atom: false]},
{:union, [{:atom, :bool}, {:atom, true}]}},
{:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}}
]}}
@@ -320,7 +321,8 @@ defmodule Module.Types.MapTest do
{:map,
[
{:required, {:atom, true}, {:atom, true}},
{:required, :boolean, {:union, [{:atom, :bool}, {:atom, true}]}},
{:required, {:union, [atom: true, atom: false]},
{:union, [{:atom, :bool}, {:atom, true}]}},
{:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}}
]}}
@@ -333,12 +335,13 @@ defmodule Module.Types.MapTest do
{:map,
[
{:required, {:atom, true}, {:atom, true}},
{:required, :boolean, {:union, [{:atom, :bool}, {:atom, true}]}},
{:required, {:union, [atom: true, atom: false]},
{:union, [{:atom, :bool}, {:atom, true}]}},
{:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}}
]}}
end
test "with unbound var keys" do
test "map creation with unbound var keys" do
assert quoted_expr(
[var, struct],
(
@@ -118,13 +118,18 @@ defmodule Module.Types.PatternTest do
{:map, [{:required, {:atom, :a}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}}
assert quoted_pattern(%{123 => a}) ==
{:ok, {:map, [{:required, :integer, {:var, 0}}, {:optional, :dynamic, :dynamic}]}}
{:ok,
{:map,
[
{:required, :integer, {:union, [{:var, 0}, :dynamic]}},
{:optional, :dynamic, :dynamic}
]}}
assert quoted_pattern(%{123 => :foo, 456 => :bar}) ==
{:ok,
{:map,
[
{:required, :integer, {:union, [{:atom, :foo}, {:atom, :bar}]}},
{:required, :integer, :dynamic},
{:optional, :dynamic, :dynamic}
]}}
@@ -285,10 +290,10 @@ defmodule Module.Types.PatternTest do
{:ok, [{:union, [:tuple, :atom]}]}
assert quoted_head([x], [is_boolean(x) and is_atom(x)]) ==
{:ok, [:boolean]}
{:ok, [{:union, [atom: true, atom: false]}]}
assert quoted_head([x], [is_atom(x) and is_boolean(x)]) ==
{:ok, [:boolean]}
{:ok, [{:union, [atom: true, atom: false]}]}
assert quoted_head([x], [is_atom(x) > :foo]) == {:ok, [var: 0]}
@@ -307,7 +312,7 @@ defmodule Module.Types.PatternTest do
assert {:error, {:unable_unify, {:tuple, :atom, _}}} =
quoted_head([x], [is_tuple(x) and is_atom(x)])
assert {:error, {:unable_unify, {:boolean, :tuple, _}}} =
assert {:error, {:unable_unify, {{:union, [atom: true, atom: false]}, :tuple, _}}} =
quoted_head([x], [is_tuple(is_atom(x))])
end
@@ -322,13 +327,16 @@ defmodule Module.Types.PatternTest do
assert {:error, {:unable_unify, {{:atom, :foo}, {:list, :dynamic}, _}}} =
quoted_head([x], [length(:foo)])
assert {:error, {:unable_unify, {:boolean, {:list, :dynamic}, _}}} =
assert {:error,
{:unable_unify, {{:union, [atom: true, atom: false]}, {:list, :dynamic}, _}}} =
quoted_head([x], [length(is_tuple(x))])
assert {:error, {:unable_unify, {:boolean, :tuple, _}}} =
assert {:error, {:unable_unify, {{:union, [atom: true, atom: false]}, :tuple, _}}} =
quoted_head([x], [elem(is_tuple(x), 0)])
assert {:error, {:unable_unify, {:boolean, :number, _}}} =
assert {:error,
{:unable_unify,
{{:union, [atom: true, atom: false]}, {:union, [:integer, :float]}, _}}} =
quoted_head([x], [elem({}, is_tuple(x))])
assert quoted_head([x], [elem({}, 1)]) == {:ok, [var: 0]}
@@ -371,7 +379,11 @@ defmodule Module.Types.PatternTest do
assert quoted_head([%{true: bool}], [is_boolean(bool)]) ==
{:ok,
[
{:map, [{:required, {:atom, true}, :boolean}, {:optional, :dynamic, :dynamic}]}
{:map,
[
{:required, {:atom, true}, {:union, [atom: true, atom: false]}},
{:optional, :dynamic, :dynamic}
]}
]}
assert quoted_head([%{true: true} = foo, %{false: false} = foo]) ==
@@ -1,8 +1,8 @@
Code.require_file("../../test_helper.exs", __DIR__)
defmodule Module.Types.InferTest do
defmodule Module.Types.UnifyTest do
use ExUnit.Case, async: true
import Module.Types.Infer
import Module.Types.Unify
alias Module.Types
defp unify_lift(left, right, context \\ new_context()) do
@@ -53,27 +53,17 @@ defmodule Module.Types.InferTest do
assert unify_lift(:integer, :integer) == {:ok, :integer}
assert unify_lift(:binary, :binary) == {:ok, :binary}
assert unify_lift(:atom, :atom) == {:ok, :atom}
assert unify_lift(:boolean, :boolean) == {:ok, :boolean}
assert {:error, {:unable_unify, {:integer, :boolean, _}}} = unify_lift(:integer, :boolean)
assert {:error, {:unable_unify, {:integer, :atom, _}}} = unify_lift(:integer, :atom)
end
test "subtype undirected" do
assert unify_lift(:boolean, :atom) == {:ok, :boolean}
assert unify_lift(:atom, :boolean) == {:ok, :boolean}
assert unify_lift(:boolean, {:atom, true}) == {:ok, {:atom, true}}
assert unify_lift({:atom, true}, :boolean) == {:ok, {:atom, true}}
assert unify_lift(:atom, {:atom, true}) == {:ok, {:atom, true}}
assert unify_lift({:atom, true}, :atom) == {:ok, {:atom, true}}
end
test "subtype directed" do
assert unify_directed_lift(:boolean, :atom) == {:ok, :boolean}
assert unify_directed_lift({:atom, true}, :boolean) == {:ok, {:atom, true}}
assert unify_directed_lift({:atom, true}, :atom) == {:ok, {:atom, true}}
assert {:error, _} = unify_directed_lift(:atom, :boolean)
assert {:error, _} = unify_directed_lift(:boolean, {:atom, true})
assert {:error, _} = unify_directed_lift(:atom, {:atom, true})
end
@@ -83,8 +73,8 @@ defmodule Module.Types.InferTest do
assert unify_lift({:tuple, 1, [:integer]}, {:tuple, 1, [:integer]}) ==
{:ok, {:tuple, 1, [:integer]}}
assert unify_lift({:tuple, 1, [:boolean]}, {:tuple, 1, [:atom]}) ==
{:ok, {:tuple, 1, [:boolean]}}
assert unify_lift({:tuple, 1, [{:atom, :foo}]}, {:tuple, 1, [:atom]}) ==
{:ok, {:tuple, 1, [{:atom, :foo}]}}
assert {:error, {:unable_unify, {{:tuple, 1, [:integer]}, {:tuple, 0, []}, _}}} =
unify_lift({:tuple, 1, [:integer]}, {:tuple, 0, []})
@@ -134,10 +124,10 @@ defmodule Module.Types.InferTest do
{:ok, {:map, [{:required, :integer, :atom}]}}
assert unify_lift(
{:map, [{:required, {:atom, :foo}, :boolean}]},
{:map, [{:required, {:atom, :foo}, {:atom, :bar}}]},
{:map, [{:required, {:atom, :foo}, :atom}]}
) ==
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
assert {:error,
{:unable_unify,
@@ -166,28 +156,28 @@ defmodule Module.Types.InferTest do
test "map required/optional key" do
assert unify_lift(
{:map, [{:required, {:atom, :foo}, :boolean}]},
{:map, [{:required, {:atom, :foo}, {:atom, :bar}}]},
{:map, [{:required, {:atom, :foo}, :atom}]}
) ==
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
assert unify_lift(
{:map, [{:optional, {:atom, :foo}, :boolean}]},
{:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]},
{:map, [{:required, {:atom, :foo}, :atom}]}
) ==
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
assert unify_lift(
{:map, [{:required, {:atom, :foo}, :boolean}]},
{:map, [{:required, {:atom, :foo}, {:atom, :bar}}]},
{:map, [{:optional, {:atom, :foo}, :atom}]}
) ==
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
assert unify_lift(
{:map, [{:optional, {:atom, :foo}, :boolean}]},
{:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]},
{:map, [{:optional, {:atom, :foo}, :atom}]}
) ==
{:ok, {:map, [{:optional, {:atom, :foo}, :boolean}]}}
{:ok, {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}}
end
test "map with subtyped keys" do
@@ -277,8 +267,8 @@ defmodule Module.Types.InferTest do
assert unify_lift({:union, [:integer, :atom]}, {:union, [:atom, :integer]}) ==
{:ok, {:union, [:integer, :atom]}}
assert unify_lift({:union, [:atom]}, {:union, [:boolean]}) == {:ok, {:union, [:boolean]}}
assert unify_lift({:union, [:boolean]}, {:union, [:atom]}) == {:ok, {:union, [:boolean]}}
assert unify_lift({:union, [:atom]}, {:union, [{:atom, :bar}]}) ==
{:ok, {:union, [{:atom, :bar}]}}
assert {:error, {:unable_unify, {{:union, [:integer]}, {:union, [:atom]}, _}}} =
unify_lift({:union, [:integer]}, {:union, [:atom]})
@@ -425,15 +415,11 @@ defmodule Module.Types.InferTest do
describe "subtype?/3" do
test "with simple types" do
assert subtype?({:atom, :foo}, :atom, new_context())
assert subtype?({:atom, true}, :boolean, new_context())
assert subtype?({:atom, true}, :atom, new_context())
assert subtype?(:boolean, :atom, new_context())
refute subtype?(:integer, :binary, new_context())
refute subtype?(:atom, {:atom, :foo}, new_context())
refute subtype?(:boolean, {:atom, true}, new_context())
refute subtype?(:atom, {:atom, true}, new_context())
refute subtype?(:atom, :boolean, new_context())
end
test "with composite types" do
@@ -503,8 +489,7 @@ defmodule Module.Types.InferTest do
test "to_union/2" do
assert to_union([:atom], new_context()) == :atom
assert to_union([:integer, :integer], new_context()) == :integer
assert to_union([:boolean, :atom], new_context()) == :atom
assert to_union([{:atom, :foo}, :boolean, :atom], new_context()) == :atom
assert to_union([{:atom, :foo}, {:atom, :bar}, :atom], new_context()) == :atom
assert to_union([:binary, :atom], new_context()) == {:union, [:binary, :atom]}
assert to_union([:atom, :binary, :atom], new_context()) == {:union, [:atom, :binary]}
+83 -49
View File
@@ -259,41 +259,10 @@ defmodule ExUnit.Case do
end
quote do
unless Module.has_attribute?(__MODULE__, :ex_unit_tests) do
tag_check =
[:moduletag, :describetag, :tag]
|> Enum.any?(&Module.has_attribute?(__MODULE__, &1))
if tag_check do
raise "you must set @tag, @describetag, and @moduletag after the call to \"use ExUnit.Case\""
end
attributes = [
:ex_unit_tests,
:tag,
:describetag,
:moduletag,
:ex_unit_registered_test_attributes,
:ex_unit_registered_describe_attributes,
:ex_unit_registered_module_attributes,
:ex_unit_used_describes
]
Enum.each(attributes, &Module.register_attribute(__MODULE__, &1, accumulate: true))
@before_compile ExUnit.Case
@after_compile ExUnit.Case
@ex_unit_async false
@ex_unit_describe nil
unless ExUnit.Case.__register__(__MODULE__, unquote(opts)) do
use ExUnit.Callbacks
end
async = unquote(opts)[:async]
if is_boolean(async) do
@ex_unit_async async
end
import ExUnit.Callbacks
import ExUnit.Assertions
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
@@ -301,6 +270,49 @@ defmodule ExUnit.Case do
end
end
@doc false
def __register__(module, opts) do
registered? = Module.has_attribute?(module, :ex_unit_tests)
unless registered? do
tag_check = Enum.any?([:moduletag, :describetag, :tag], &Module.has_attribute?(module, &1))
if tag_check do
raise "you must set @tag, @describetag, and @moduletag after the call to \"use ExUnit.Case\""
end
attributes = [
:ex_unit_tests,
:tag,
:describetag,
:moduletag,
:ex_unit_registered_test_attributes,
:ex_unit_registered_describe_attributes,
:ex_unit_registered_module_attributes,
:ex_unit_used_describes
]
Enum.each(attributes, &Module.register_attribute(module, &1, accumulate: true))
attributes = [
before_compile: ExUnit.Case,
after_compile: ExUnit.Case,
ex_unit_async: false,
ex_unit_describe: nil
]
Enum.each(attributes, fn {k, v} -> Module.put_attribute(module, k, v) end)
end
async? = opts[:async]
if is_boolean(async?) do
Module.put_attribute(module, :ex_unit_async, async?)
end
registered?
end
@doc """
Defines a test with `message`.
@@ -333,9 +345,17 @@ defmodule ExUnit.Case do
var = Macro.escape(var)
contents = Macro.escape(contents, unquote: true)
%{module: mod, file: file, line: line} = __CALLER__
quote bind_quoted: [var: var, contents: contents, message: message] do
name = ExUnit.Case.register_test(__ENV__, :test, message, [])
quote bind_quoted: [
var: var,
contents: contents,
message: message,
mod: mod,
file: file,
line: line
] do
name = ExUnit.Case.register_test(mod, file, line, :test, message, [])
def unquote(name)(unquote(var)), do: unquote(contents)
end
end
@@ -354,8 +374,10 @@ defmodule ExUnit.Case do
"""
defmacro test(message) do
%{module: mod, file: file, line: line} = __CALLER__
quote bind_quoted: binding() do
name = ExUnit.Case.register_test(__ENV__, :test, message, [:not_implemented])
name = ExUnit.Case.register_test(mod, file, line, :test, message, [:not_implemented])
def unquote(name)(_), do: flunk("Not implemented")
end
end
@@ -423,23 +445,14 @@ defmodule ExUnit.Case do
"""
defmacro describe(message, do: block) do
quote do
ExUnit.Case.__describe__(__MODULE__, __ENV__.line, unquote(message))
try do
ExUnit.Case.__describe__(__MODULE__, __ENV__.line, unquote(message), fn ->
unquote(block)
after
@ex_unit_describe nil
Module.delete_attribute(__MODULE__, :describetag)
for attribute <- Module.get_attribute(__MODULE__, :ex_unit_registered_describe_attributes) do
Module.delete_attribute(__MODULE__, attribute)
end
end
end)
end
end
@doc false
def __describe__(module, line, message) do
def __describe__(module, line, message, fun) do
if Module.get_attribute(module, :ex_unit_describe) do
raise "cannot call \"describe\" inside another \"describe\". See the documentation " <>
"for ExUnit.Case.describe/2 on named setups and how to handle hierarchies"
@@ -463,7 +476,17 @@ defmodule ExUnit.Case do
Module.put_attribute(module, :ex_unit_describe, {line, message})
Module.put_attribute(module, :ex_unit_used_describes, message)
:ok
try do
fun.()
after
Module.put_attribute(module, :ex_unit_describe, nil)
Module.delete_attribute(module, :describetag)
for attribute <- Module.get_attribute(module, :ex_unit_registered_describe_attributes) do
Module.delete_attribute(module, attribute)
end
end
end
@doc false
@@ -496,7 +519,7 @@ defmodule ExUnit.Case do
display. You can use `ExUnit.plural_rule/2` to set a custom
pluralization.
"""
def register_test(%{module: mod, file: file, line: line}, test_type, name, tags) do
def register_test(mod, file, line, test_type, name, tags) do
unless Module.has_attribute?(mod, :ex_unit_tests) do
raise "cannot define #{test_type}. Please make sure you have invoked " <>
"\"use ExUnit.Case\" in the current module"
@@ -557,6 +580,17 @@ defmodule ExUnit.Case do
name
end
@doc """
Reigsters a test with the given environment.
This function is deprecated in favor of register_test/6 which performs
better under tight loops by avoiding `__ENV__`.
"""
@doc deprecated: "Use register_test/6 instead"
def register_test(%{module: mod, file: file, line: line}, test_type, name, tags) do
register_test(mod, file, line, test_type, name, tags)
end
@doc """
Registers a new attribute to be used during `ExUnit.Case` tests.
+2 -1
View File
@@ -778,9 +778,10 @@ defmodule IEx.Introspection do
doc && IO.ANSI.Docs.print(doc, format, opts)
end
defp translate_doc(%{"en" => doc}), do: doc
defp translate_doc(%{}), do: nil
defp translate_doc(:none), do: nil
defp translate_doc(:hidden), do: nil
defp translate_doc(%{"en" => doc}), do: doc
defp no_beam(module) do
case Code.ensure_loaded(module) do
+6 -6
View File
@@ -242,8 +242,8 @@ defmodule IEx.Server do
end
end
defp handle_take_over({:respawn, evaluator}, _state, evaluator, evaluator_ref, input, _callback) do
rerun([], evaluator, evaluator_ref, input)
defp handle_take_over({:respawn, evaluator}, state, evaluator, evaluator_ref, input, _callback) do
rerun(state.evaluator_options, evaluator, evaluator_ref, input)
end
defp handle_take_over({:continue, evaluator}, state, evaluator, evaluator_ref, input, _callback) do
@@ -254,18 +254,18 @@ defmodule IEx.Server do
defp handle_take_over(
{:DOWN, evaluator_ref, :process, evaluator, :normal},
_state,
state,
evaluator,
evaluator_ref,
input,
_callback
) do
rerun([], evaluator, evaluator_ref, input)
rerun(state.evaluator_options, evaluator, evaluator_ref, input)
end
defp handle_take_over(
{:DOWN, evaluator_ref, :process, evaluator, reason},
_state,
state,
evaluator,
evaluator_ref,
input,
@@ -281,7 +281,7 @@ defmodule IEx.Server do
io_error("** (IEx.Error) #{type} when printing EXIT message: #{inspect(detail)}")
end
rerun([], evaluator, evaluator_ref, input)
rerun(state.evaluator_options, evaluator, evaluator_ref, input)
end
defp handle_take_over(_, state, _evaluator, _evaluator_ref, _input, callback) do
+3 -1
View File
@@ -62,7 +62,9 @@ defmodule Logger.App do
@doc false
def config_change(changed, _new, _removed) do
Logger.configure(changed)
# All other config has already been persisted, we only need to
# update the level and reload the logger state.
Logger.configure(Keyword.take(changed, [:level]))
end
@doc """
+2 -1
View File
@@ -647,7 +647,8 @@ defmodule LoggerTest do
:ok = Logger.configure(level: :debug)
try do
assert Logger.App.config_change([level: :error], [], []) === :ok
assert Application.put_env(:logger, :level, :error) === :ok
assert :application_controller.config_change(logger: [level: :debug]) === :ok
assert Logger.level() === :error
after
Logger.configure(level: :debug)
+4 -2
View File
@@ -78,7 +78,9 @@ defmodule Mix.CLI do
defp run_task(name, args) do
try do
ensure_no_slashes(name)
Mix.Tasks.Loadconfig.load_default()
# We must go through the task instead of invoking the module directly
# because projects like Nerves use this to invoke it early.
Mix.Task.run("loadconfig")
Mix.Task.run(name, args)
rescue
# We only rescue exceptions in the Mix namespace, all
@@ -144,7 +146,7 @@ defmodule Mix.CLI do
path = Path.join(Mix.Utils.mix_config(), "config.exs")
if File.regular?(path) do
Mix.Tasks.Loadconfig.load_imports(path)
Mix.Tasks.Loadconfig.load_compile(path)
end
end
+1 -1
View File
@@ -34,7 +34,7 @@ defmodule Mix.Tasks.App.Config do
runtime = config[:config_path] |> Path.dirname() |> Path.join("runtime.exs")
if File.exists?(runtime) do
Mix.Tasks.Loadconfig.load_file(runtime)
Mix.Tasks.Loadconfig.load_runtime(runtime)
end
if opts[:preload_modules] do
+14 -7
View File
@@ -29,35 +29,42 @@ defmodule Mix.Tasks.Loadconfig do
case args do
[] -> load_default()
[file] -> load_imports(file)
[file] -> load_compile(file)
end
end
@doc false
def load_default do
defp load_default do
config = Mix.Project.config()
if File.regular?(config[:config_path]) or config[:config_path] != "config/config.exs" do
load_imports(config[:config_path])
load_compile(config[:config_path])
else
[]
end
end
@doc false
def load_imports(file) do
# Loads compile-time configuration, they support imports, and are not deep merged.
def load_compile(file) do
{config, files} = Config.Reader.read_imports!(file, env: Mix.env(), target: Mix.target())
Mix.ProjectStack.loaded_config(persist_apps(config, file), files)
config
end
@doc false
def load_file(file) do
# Loads runtime configuration, they do not support imports, and are deep merged.
def load_runtime(file) do
config = Config.Reader.read!(file, env: Mix.env(), target: Mix.target(), imports: :disabled)
Mix.ProjectStack.loaded_config(persist_apps(config, file), [])
Mix.ProjectStack.loaded_config(persist_apps(hydrate_apps(config), file), [])
config
end
defp hydrate_apps(config) do
for {app, pairs} <- config do
hd(Config.Reader.merge([{app, Application.get_all_env(app)}], [{app, pairs}]))
end
end
defp persist_apps(config, file) do
Application.put_all_env(config, persistent: true)
apps = Keyword.keys(config)
+7 -1
View File
@@ -154,7 +154,13 @@ defmodule Mix.Tasks.Run do
defp process_config(opts) do
for {:config, value} <- opts do
Mix.Tasks.Loadconfig.load_imports(value)
# TODO: Remove on v2.0.
IO.warn(
"the --config flag is deprecated. If you need to handle multiple configurations, " <>
"it is preferable to dynamically import them in your config files"
)
Mix.Tasks.Loadconfig.load_compile(value)
end
:ok
+5 -1
View File
@@ -17,11 +17,13 @@ defmodule Mix.Tasks.App.ConfigTest do
File.write!("config/config.exs", """
import Config
config :sample, from_compile: :compile, from_runtime: :compile
config :sample, :nested, from_compile: :compile, from_runtime: :compile
""")
File.write!("config/runtime.exs", """
import Config
config :sample, from_runtime: :runtime
config :sample, :nested, from_compile: :compile, from_runtime: :compile
""")
Mix.Task.run("loadconfig")
@@ -30,10 +32,12 @@ defmodule Mix.Tasks.App.ConfigTest do
assert Application.get_all_env(:sample) |> Enum.sort() == [
from_compile: :compile,
from_env: :env,
from_runtime: :runtime
from_runtime: :runtime,
nested: [from_compile: :compile, from_runtime: :compile]
]
end)
after
Application.delete_env(:sample, :nested, persistent: true)
Application.delete_env(:sample, :from_env, persistent: true)
Application.delete_env(:sample, :from_compile, persistent: true)
Application.delete_env(:sample, :from_runtime, persistent: true)
+13 -2
View File
@@ -359,6 +359,11 @@ defmodule Mix.Tasks.ReleaseTest do
config = [releases: [runtime_config: [reboot_system_after_config: true]]]
Mix.Project.in_project(:release_test, ".", config, fn _ ->
File.write!("config/config.exs", """
#{File.read!("config/config.exs")}
config :release_test, :runtime, keep: :static, override: :static
""")
File.write!("config/runtime.exs", """
import Config
@@ -366,7 +371,11 @@ defmodule Mix.Tasks.ReleaseTest do
raise "file should not be loaded while assembling release"
end
config :release_test, :runtime, {:was_set, config_env(), config_target()}
config :release_test, :runtime,
override: :runtime,
config_env: config_env(),
config_target: config_target()
config :release_test, :encoding, {:runtime, :time_μs, :"£", "£", '£'}
""")
@@ -401,7 +410,9 @@ defmodule Mix.Tasks.ReleaseTest do
release_mode: "embedded",
release_node: "runtime_config",
release_vsn: "0.1.0",
runtime_config: {:ok, {:was_set, :dev, :host}},
runtime_config:
{:ok,
[keep: :static, override: :runtime, config_env: :dev, config_target: :host]},
static_config: {:ok, :was_set},
sys_config_env: sys_config_env,
sys_config_init: sys_config_init
-14
View File
@@ -3,24 +3,10 @@ Code.require_file("../../test_helper.exs", __DIR__)
defmodule Mix.Tasks.RunTest do
use MixTest.Case
import ExUnit.CaptureIO
setup do
Mix.Project.push(MixTest.Case.Sample)
end
test "loads configuration", context do
in_tmp(context.test, fn ->
config = fixture_path("configs/good_config.exs")
expr = "IO.puts(Application.get_env(:my_app, :key))"
assert capture_io(fn -> Mix.Task.run("run", ["--config", config, "--eval", expr]) end) ==
"value\n"
end)
after
Application.delete_env(:my_app, :key)
end
test "run requires files before evaling commands", context do
git_repo = fixture_path("git_repo/lib/git_repo.ex")