Skip to content
26 changes: 16 additions & 10 deletions lib/elixir/lib/inspect.ex
Original file line number Diff line number Diff line change
Expand Up @@ -571,20 +571,26 @@ defimpl Inspect, for: Any do
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))

optional? =
filtered_guard =
quote do
var!(field) in unquote(filtered_fields)
end

field_guard =
if optional == [] do
false
filtered_guard
else
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}

quote do
case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))

%{} ->
false
end
unquote(filtered_guard) and
not case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))

%{} ->
false
end
end
end

Expand All @@ -593,7 +599,7 @@ defimpl Inspect, for: Any do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
var!(field) in unquote(filtered_fields) and not unquote(optional?),
unquote(field_guard),
do: var!(info)

var!(name) = Macro.inspect_atom(:literal, unquote(module))
Expand Down
50 changes: 30 additions & 20 deletions lib/elixir/lib/kernel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1905,8 +1905,8 @@ defmodule Kernel do

## Implemented in Elixir

defp optimize_boolean({:case, meta, args}) do
{:case, [{:optimize_boolean, true} | meta], args}
defp annotate_case(extra, {:case, meta, args}) do
{:case, extra ++ meta, args}
end

@doc """
Expand Down Expand Up @@ -1973,7 +1973,8 @@ defmodule Kernel do
end

defp build_boolean_check(operator, check, true_clause, false_clause) do
optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(check) do
false -> unquote(false_clause)
Expand Down Expand Up @@ -2006,7 +2007,8 @@ defmodule Kernel do
defmacro !{:!, _, [value]} do
assert_no_match_or_guard_scope(__CALLER__.context, "!")

optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
Expand All @@ -2019,7 +2021,8 @@ defmodule Kernel do
defmacro !value do
assert_no_match_or_guard_scope(__CALLER__.context, "!")

optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
Expand Down Expand Up @@ -3910,7 +3913,8 @@ defmodule Kernel do
end

defp build_if(condition, do: do_clause, else: else_clause) do
optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(condition) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
Expand Down Expand Up @@ -4228,15 +4232,18 @@ defmodule Kernel do
defmacro left && right do
assert_no_match_or_guard_scope(__CALLER__.context, "&&")

quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x
annotate_case(
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x

_ ->
unquote(right)
_ ->
unquote(right)
end
end
end
)
end

@doc """
Expand Down Expand Up @@ -4268,15 +4275,18 @@ defmodule Kernel do
defmacro left || right do
assert_no_match_or_guard_scope(__CALLER__.context, "||")

quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right)
annotate_case(
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right)

x ->
x
x ->
x
end
end
end
)
end

@doc """
Expand Down
4 changes: 3 additions & 1 deletion lib/elixir/lib/module/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ defmodule Module.Types do
end

defp warnings_from_clause(meta, args, guards, body, stack, context) do
{_types, context} = Pattern.of_head(args, guards, meta, stack, context)
dynamic = Module.Types.Descr.dynamic()
expected = Enum.map(args, fn _ -> dynamic end)
{_types, context} = Pattern.of_head(args, guards, expected, :default, meta, stack, context)
{_type, context} = Expr.of_expr(body, stack, context)
context.warnings
end
Expand Down
9 changes: 9 additions & 0 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,15 @@ defmodule Module.Types.Descr do
def number_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_number) != 0, do: true
def number_type?(_), do: false

@doc """
Optimized version of `not empty?(intersection(atom(), type))`.
"""
def atom_type?(:term), do: true
def atom_type?(%{dynamic: :term}), do: true
def atom_type?(%{dynamic: %{atom: _}}), do: true
def atom_type?(%{atom: _}), do: true
def atom_type?(_), do: false

## Bitmaps

defp bitmap_to_quoted(val) do
Expand Down
84 changes: 53 additions & 31 deletions lib/elixir/lib/module/types/expr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ defmodule Module.Types.Expr do
functions_and_macros = list(tuple([atom(), list(tuple([atom(), integer()]))]))
list_of_modules = list(atom())

@try_catch atom([:error, :exit, :throw])

@caller closed_map(
__struct__: atom([Macro.Env]),
aliases: aliases,
context: atom([:match, :guard, nil]),
context_modules: list_of_modules,
file: binary(),
function: union(tuple(), atom([nil])),
function: union(tuple([atom(), integer()]), atom([nil])),
functions: functions_and_macros,
lexical_tracker: union(pid(), atom([nil])),
line: integer(),
Expand Down Expand Up @@ -108,13 +110,13 @@ defmodule Module.Types.Expr do
end

# left = right
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
def of_expr({:=, _, [left_expr, right_expr]} = expr, stack, context) do
{right_type, context} = of_expr(right_expr, stack, context)

# We do not raise on underscore in case someone writes _ = raise "omg"
case left_expr do
{:_, _, ctx} when is_atom(ctx) -> {right_type, context}
_ -> Pattern.of_match(left_expr, right_type, expr, stack, context)
_ -> Pattern.of_match(left_expr, right_type, expr, {:match, right_type}, stack, context)
end
end

Expand Down Expand Up @@ -241,19 +243,26 @@ defmodule Module.Types.Expr do
|> dynamic_unless_static(stack)
end

def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
{expr_type, context} = of_expr(case_expr, stack, context)
def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, stack, context) do
{case_type, context} = of_expr(case_expr, stack, context)

clauses
|> of_clauses(stack, [expr_type], {none(), context})
# If we are only type checking the expression and the expression is a literal,
# let's mark it as generated, as it is most likely a macro code.
if is_atom(case_expr) and {:type_check, :expr} in meta do
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
else
clauses
end
|> of_clauses([case_type], {:case, meta, case_type, case_expr}, stack, {none(), context})
|> dynamic_unless_static(stack)
end

# TODO: fn pat -> expr end
def of_expr({:fn, _meta, clauses}, stack, context) do
[{:->, _, [args, _]} | _] = clauses
expected = Enum.map(args, fn _ -> dynamic() end)
{_acc, context} = of_clauses(clauses, stack, expected, {none(), context})
[{:->, _, [head, _]} | _] = clauses
{patterns, _guards} = extract_head(head)
expected = Enum.map(patterns, fn _ -> dynamic() end)
{_acc, context} = of_clauses(clauses, expected, :fn, stack, {none(), context})
{fun(), context}
end

Expand All @@ -280,22 +289,22 @@ defmodule Module.Types.Expr do
{acc, context}

{:catch, clauses}, acc_context ->
of_clauses(clauses, stack, [atom([:error, :exit, :throw]), dynamic()], acc_context)
of_clauses(clauses, [@try_catch, dynamic()], :try_catch, stack, acc_context)

{:else, clauses}, acc_context ->
of_clauses(clauses, stack, [body_type], acc_context)
of_clauses(clauses, [body_type], {:try_else, body_type}, stack, acc_context)
end)
|> dynamic_unless_static(stack)
end

def of_expr({:receive, _meta, [blocks]}, stack, context) do
blocks
|> Enum.reduce({none(), context}, fn
{:do, {:__block__, _, []}}, {acc, context} ->
{acc, context}
{:do, {:__block__, _, []}}, acc_context ->
acc_context

{:do, clauses}, {acc, context} ->
of_clauses(clauses, stack, [dynamic()], {acc, context})
{:do, clauses}, acc_context ->
of_clauses(clauses, [dynamic()], :receive, stack, acc_context)

{:after, [{:->, meta, [[timeout], body]}]}, {acc, context} ->
{timeout_type, context} = of_expr(timeout, stack, context)
Expand All @@ -318,7 +327,7 @@ defmodule Module.Types.Expr do
context = Enum.reduce(opts, context, &for_option(&1, stack, &2))

if Keyword.has_key?(opts, :reduce) do
{_, context} = of_clauses(block, stack, [dynamic()], {none(), context})
{_, context} = of_clauses(block, [dynamic()], :for_reduce, stack, {none(), context})
{dynamic(), context}
else
{_type, context} = of_expr(block, stack, context)
Expand Down Expand Up @@ -441,16 +450,21 @@ defmodule Module.Types.Expr do

## Comprehensions

defp for_clause({:<-, meta, [left, expr]}, stack, context) do
defp for_clause({:<-, _, [left, right]} = expr, stack, context) do
{pattern, guards} = extract_head([left])
{_expr_type, context} = of_expr(expr, stack, context)
{[_type], context} = Pattern.of_head([pattern], guards, meta, stack, context)
{_, context} = of_expr(right, stack, context)

{_type, context} =
Pattern.of_match(pattern, guards, dynamic(), expr, :for, stack, context)

context
end

defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]}, stack, context) do
defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do
{right_type, context} = of_expr(right, stack, context)
{_pattern_type, context} = Pattern.of_match(left, binary(), left, stack, context)

{_pattern_type, context} =
Pattern.of_match(left, binary(), expr, :for, stack, context)

if binary_type?(right_type) do
context
Expand Down Expand Up @@ -481,11 +495,10 @@ defmodule Module.Types.Expr do

## With

defp with_clause({:<-, meta, [left, expr]}, stack, context) do
defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do
{pattern, guards} = extract_head([left])

{[_type], context} = Pattern.of_head([pattern], guards, meta, stack, context)
{_expr_type, context} = of_expr(expr, stack, context)
{_type, context} = Pattern.of_match(pattern, guards, dynamic(), :with, expr, stack, context)
{_, context} = of_expr(right, stack, context)
context
end

Expand All @@ -500,7 +513,7 @@ defmodule Module.Types.Expr do
end

defp with_option({:else, clauses}, stack, context) do
{_, context} = of_clauses(clauses, stack, [dynamic()], {none(), context})
{_, context} = of_clauses(clauses, [dynamic()], :with_else, stack, {none(), context})
context
end

Expand Down Expand Up @@ -532,15 +545,24 @@ defmodule Module.Types.Expr do
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}

defp of_clauses(clauses, stack, _expected, acc_context) do
Enum.reduce(clauses, acc_context, fn {:->, meta, [head, body]}, {acc, context} ->
defp of_clauses(clauses, expected, info, stack, {acc, context}) do
%{failed: failed?} = context

Enum.reduce(clauses, {acc, context}, fn {:->, meta, [head, body]}, {acc, context} ->
{failed?, context} = reset_failed(context, failed?)
{patterns, guards} = extract_head(head)
{_types, context} = Pattern.of_head(patterns, guards, meta, stack, context)
{_types, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context)
{body, context} = of_expr(body, stack, context)
{union(acc, body), context}
{union(acc, body), set_failed(context, failed?)}
end)
end

defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}}
defp reset_failed(context, _), do: {false, context}

defp set_failed(%{failed: false} = context, true), do: %{context | failed: true}
defp set_failed(context, _bool), do: context

defp extract_head([{:when, _meta, args}]) do
case Enum.split(args, -1) do
{patterns, [guards]} -> {patterns, flatten_when(guards)}
Expand Down
21 changes: 16 additions & 5 deletions lib/elixir/lib/module/types/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -223,21 +223,32 @@ defmodule Module.Types.Helpers do
if Keyword.get(meta, :generated, false) do
context
else
{fun, arity} = stack.function
location = {stack.file, meta, {stack.module, fun, arity}}
%{context | warnings: [{module, warning, location} | context.warnings]}
effectively_warn(module, warning, meta, stack, context)
end
end

defp effectively_warn(module, warning, meta, stack, context) do
{fun, arity} = stack.function
location = {stack.file, meta, {stack.module, fun, arity}}
%{context | warnings: [{module, warning, location} | context.warnings]}
end

@doc """
Emits an error.

In practice an error is a warning that halts other errors from being collected.
"""
def error(module, warning, meta, stack, context) do
case context do
%{failed: true} -> context
%{failed: false} -> warn(module, warning, meta, stack, %{context | failed: true})
%{failed: true} ->
context

%{failed: false} ->
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, %{context | failed: true})
end
end
end

Expand Down
Loading
Loading