Skip to content

Commit 34a5000

Browse files
committed
Keep Kernel.in/2 as :lists.member/2 and optimize in type checker and Erlang pass
1 parent 38dcc45 commit 34a5000

9 files changed

Lines changed: 228 additions & 67 deletions

File tree

lib/elixir/lib/kernel.ex

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4707,9 +4707,9 @@ defmodule Kernel do
47074707
[head | tail] = list ->
47084708
# We only expand lists in the body if they are relatively
47094709
# short and it is made only of literal expressions.
4710-
case not in_body? or small_literal_list?(right) do
4711-
true -> in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
4712-
false -> quote(do: :lists.member(unquote(left), unquote(right)))
4710+
case in_body? do
4711+
false -> in_list(left, head, tail, expand, list, in_body?)
4712+
true -> quote(do: :lists.member(unquote(left), unquote(right)))
47134713
end
47144714

47154715
%{} = right ->
@@ -4756,12 +4756,6 @@ defmodule Kernel do
47564756
end
47574757
end
47584758

4759-
defp small_literal_list?(list) when is_list(list) and length(list) <= 32 do
4760-
:lists.all(fn x -> is_binary(x) or is_atom(x) or is_number(x) end, list)
4761-
end
4762-
4763-
defp small_literal_list?(_list), do: false
4764-
47654759
defp in_range(left, first, last, step) when is_integer(step) do
47664760
in_range_literal(left, first, last, step)
47674761
end

lib/elixir/lib/module/types/apply.ex

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ defmodule Module.Types.Apply do
236236
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
237237
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
238238

239+
## Lists
240+
{:lists, :member, [{[term(), list(term())], boolean()}]},
241+
239242
## Map
240243
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
241244
{Map, :get, [{[open_map(), term()], term()}]},
@@ -367,8 +370,8 @@ defmodule Module.Types.Apply do
367370
right_literal? = Macro.quoted_literal?(right)
368371

369372
case {left_literal?, right_literal?} do
370-
{true, false} -> literal_compare(name, right, left, expected, expr, stack, context, of_fun)
371-
{false, true} -> literal_compare(name, left, right, expected, expr, stack, context, of_fun)
373+
{true, false} -> custom_compare(name, right, left, expected, expr, stack, context, of_fun)
374+
{false, true} -> custom_compare(name, left, right, expected, expr, stack, context, of_fun)
372375
{literal?, _} -> compare(name, left, right, literal?, expr, stack, context, of_fun)
373376
end
374377
end
@@ -479,6 +482,54 @@ defmodule Module.Types.Apply do
479482
end
480483
end
481484

485+
defp do_remote(:lists, :member, [arg, list] = args, expected, expr, stack, context, of_fun)
486+
when is_list(list) and list != [] do
487+
case booleaness(expected) do
488+
{polarity, _maybe_or_always} ->
489+
{return, acc} =
490+
case polarity do
491+
true -> {@atom_true, none()}
492+
false -> {@atom_false, term()}
493+
end
494+
495+
{expected, singleton?, context} =
496+
Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} ->
497+
{type, context} = of_fun.(literal, term(), expr, stack, context)
498+
499+
if singleton?(type) do
500+
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
501+
{acc, all_singleton?, context}
502+
else
503+
acc = if polarity, do: union(acc, type), else: acc
504+
{acc, false, context}
505+
end
506+
end)
507+
508+
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
509+
510+
cond do
511+
# Return a precise result
512+
singleton? and subtype?(arg_type, expected) ->
513+
{return(return, [arg_type, expected], stack), context}
514+
515+
# Singleton types with reverse polarity are negated, so we don't check for disjoint
516+
(singleton? and not polarity) or not is_warning(stack) ->
517+
{return(boolean(), [arg_type, expected], stack), context}
518+
519+
# Nothing in common between left and right, emit a warning
520+
disjoint?(arg_type, expected) ->
521+
error = {:mismatched_comparison, arg_type, list(expected)}
522+
remote_error(error, :lists, :member, 2, expr, stack, context)
523+
524+
true ->
525+
{return(boolean(), [arg_type, expected], stack), context}
526+
end
527+
528+
_ ->
529+
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
530+
end
531+
end
532+
482533
defp do_remote(mod, fun, args, expected, expr, stack, context, _of_fun) do
483534
remote_domain(mod, fun, args, expected, elem(expr, 1), stack, context)
484535
end
@@ -495,7 +546,7 @@ defmodule Module.Types.Apply do
495546
when (fun in [:length, :map_size] and is_integer(literal) and literal >= 0) or
496547
(fun in [:tuple_size] and literal in 0..15)
497548

498-
defp literal_compare(
549+
defp custom_compare(
499550
name,
500551
{{:., _, [:erlang, fun]}, _, [arg]} = left,
501552
literal,
@@ -547,18 +598,14 @@ defmodule Module.Types.Apply do
547598
end
548599
end
549600

550-
defp literal_compare(name, arg, literal, expected, expr, stack, context, of_fun) do
551-
{type, context} = of_fun.(literal, term(), expr, stack, context)
552-
literal_compare(name, arg, type, singleton?(type), expected, expr, stack, context, of_fun)
553-
end
554-
555-
def literal_compare(name, arg, type, singleton?, expected, expr, stack, context, of_fun) do
601+
defp custom_compare(name, arg, literal, expected, expr, stack, context, of_fun) do
556602
case booleaness(expected) do
557603
booleaness when booleaness in [:maybe_both, :none] ->
558-
{arg_type, context} = of_fun.(arg, term(), expr, stack, context)
559-
return_compare(name, arg_type, type, boolean(), false, expr, stack, context)
604+
compare(name, arg, literal, false, expr, stack, context, of_fun)
560605

561606
{boolean, _maybe_or_always} ->
607+
{type, context} = of_fun.(literal, term(), expr, stack, context)
608+
562609
{polarity, return} =
563610
case boolean do
564611
true -> {name in [:==, :"=:="], @atom_true}
@@ -567,7 +614,7 @@ defmodule Module.Types.Apply do
567614

568615
# This logic mirrors the code in `Pattern.of_pattern_tree`
569616
# If it is a singleton, we can always be precise
570-
if singleton? do
617+
if singleton?(type) do
571618
expected = if polarity, do: type, else: negation(type)
572619
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
573620
result = if subtype?(arg_type, expected), do: return, else: boolean()
@@ -1849,7 +1896,7 @@ defmodule Module.Types.Apply do
18491896
end
18501897

18511898
def format_diagnostic({{:mismatched_comparison, left, right}, mfac, expr, context}) do
1852-
{_, name, _, _} = mfac
1899+
{mod, name, _, _} = mfac
18531900
traces = collect_traces(expr, context)
18541901

18551902
%{
@@ -1863,7 +1910,7 @@ defmodule Module.Types.Apply do
18631910
18641911
given types:
18651912
1866-
#{type_comparison_to_string(name, left, right) |> indent(4)}
1913+
#{type_comparison_to_string(mod, name, left, right) |> indent(4)}
18671914
""",
18681915
format_traces(traces),
18691916
"""
@@ -1877,7 +1924,7 @@ defmodule Module.Types.Apply do
18771924
end
18781925

18791926
def format_diagnostic({{:struct_comparison, left, right}, mfac, expr, context}) do
1880-
{_, name, _, _} = mfac
1927+
{mod, name, _, _} = mfac
18811928
traces = collect_traces(expr, context)
18821929

18831930
%{
@@ -1891,7 +1938,7 @@ defmodule Module.Types.Apply do
18911938
18921939
given types:
18931940
1894-
#{type_comparison_to_string(name, left, right) |> indent(4)}
1941+
#{type_comparison_to_string(mod, name, left, right) |> indent(4)}
18951942
""",
18961943
format_traces(traces),
18971944
"""
@@ -1989,8 +2036,8 @@ defmodule Module.Types.Apply do
19892036

19902037
alias Inspect.Algebra, as: IA
19912038

1992-
defp type_comparison_to_string(fun, left, right) do
1993-
{_, fun, _, _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right])
2039+
defp type_comparison_to_string(mod, fun, left, right) do
2040+
{_, fun, _, _} = :elixir_rewrite.erl_to_ex(mod, fun, [left, right])
19942041

19952042
{fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]}
19962043
|> Code.Formatter.to_algebra()

lib/elixir/lib/module/types/pattern.ex

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,24 +1046,10 @@ defmodule Module.Types.Pattern do
10461046
of_remote(fun, args, call, expected, stack, context)
10471047
end
10481048

1049-
# This is reconstructed as part of orelse
1050-
def of_guard({{:., _, [Kernel, :in]}, _meta, [left, right]} = call, expected, _, stack, context) do
1051-
{right, context} = Enum.map_reduce(right, context, &of_guard(&1, term(), call, stack, &2))
1052-
{singleton, non_singleton} = Enum.split_with(right, &singleton?/1)
1053-
1054-
fun = fn
1055-
[], _singleton?, context ->
1056-
{none(), context}
1057-
1058-
types, singleton?, context ->
1059-
type = Enum.reduce(types, &union/2)
1060-
fun = &of_guard/5
1061-
Apply.literal_compare(:"=:=", left, type, singleton?, expected, call, stack, context, fun)
1062-
end
1063-
1064-
{singleton_type, context} = fun.(singleton, true, context)
1065-
{non_singleton_type, context} = fun.(non_singleton, false, context)
1066-
{union(singleton_type, non_singleton_type), context}
1049+
# The only possible case right now is the rewritten :lists.member/2 checks
1050+
def of_guard({{:., _, [mod, fun]}, _meta, args} = call, expected, _, stack, context)
1051+
when is_atom(mod) and is_atom(fun) do
1052+
Apply.remote(mod, fun, args, expected, call, stack, context, &of_guard/5)
10671053
end
10681054

10691055
# var
@@ -1158,7 +1144,7 @@ defmodule Module.Types.Pattern do
11581144
# building nested conditional environments.
11591145
[left | right] =
11601146
case unpack_op(call, fun, []) do
1161-
entries when fun == :orelse -> reconstruct_kernel_in(entries)
1147+
entries when fun == :orelse -> reconstruct_lists_member(entries)
11621148
entries -> entries
11631149
end
11641150

@@ -1216,24 +1202,27 @@ defmodule Module.Types.Pattern do
12161202
[other | acc]
12171203
end
12181204

1219-
defp reconstruct_kernel_in([head | tail]) do
1205+
# Reconstruct left in right operations but only when the right-side is a literal.
1206+
# When the right-side is not a literal, we need to track dependencies between
1207+
# left and right-side, which is currently not done for the `:lists.member/2` handling.
1208+
defp reconstruct_lists_member([head | tail]) do
12201209
with {{:., dot_meta, [:erlang, :"=:="]}, meta, [left, right]} <- head,
12211210
true <- Macro.quoted_literal?(right),
12221211
false <- data_size_op?(left),
1223-
{[_ | _] = entries, tail} <- reconstruct_kernel_in(tail, left, []) do
1212+
{[_ | _] = entries, tail} <- reconstruct_lists_member(tail, left, []) do
12241213
in_args = [left, [right | entries]]
1225-
[{{:., dot_meta, [Kernel, :in]}, meta, in_args} | reconstruct_kernel_in(tail)]
1214+
[{{:., dot_meta, [:lists, :member]}, meta, in_args} | reconstruct_lists_member(tail)]
12261215
else
1227-
_ -> [head | reconstruct_kernel_in(tail)]
1216+
_ -> [head | reconstruct_lists_member(tail)]
12281217
end
12291218
end
12301219

1231-
defp reconstruct_kernel_in([]), do: []
1220+
defp reconstruct_lists_member([]), do: []
12321221

1233-
defp reconstruct_kernel_in(list, left, acc) do
1222+
defp reconstruct_lists_member(list, left, acc) do
12341223
with [{{:., _, [:erlang, :"=:="]}, _, [^left, right]} | tail] <- list,
12351224
true <- Macro.quoted_literal?(right) do
1236-
reconstruct_kernel_in(tail, left, [right | acc])
1225+
reconstruct_lists_member(tail, left, [right | acc])
12371226
else
12381227
_ -> {Enum.reverse(acc), list}
12391228
end

lib/elixir/src/elixir_erl_pass.erl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,44 @@ translate_remote(maps, put, Meta, [Key, Value, Map], S) ->
589589
{[TKey, TValue, TMap], TS} ->
590590
{{map, Ann, TMap, [{map_field_assoc, Ann, TKey, TValue}]}, TS}
591591
end;
592+
translate_remote(lists, member, Meta, [Expr, [Head | Tail] = List], S) ->
593+
Ann = ?ann(Meta),
594+
595+
case optimize_list_membership(List, 0) of
596+
true ->
597+
{TExpr, S1} = translate(Expr, Ann, S),
598+
599+
{TVar, TFun, S3} =
600+
case TExpr of
601+
{var, _, _} ->
602+
{TExpr, fun(Orelse) -> Orelse end, S1};
603+
604+
_ ->
605+
{VarName, S2} = elixir_erl_var:build('_', S1),
606+
Generated = erl_anno:set_generated(true, Ann),
607+
Var = {var, Generated, VarName},
608+
Fun = fun(Orelse) -> {block, Generated, [{match, Generated, Var, TExpr}, Orelse]} end,
609+
{Var, Fun, S2}
610+
end,
611+
612+
{THead, _} = translate(Head, Ann, S),
613+
614+
TOrelse =
615+
lists:foldl(
616+
fun(X, Acc) ->
617+
{TX, _} = translate(X, Ann, S),
618+
{op, Ann, 'orelse', Acc, {op, Ann, '=:=', TVar, TX}}
619+
end,
620+
{op, Ann, '=:=', TVar, THead},
621+
Tail
622+
),
623+
624+
{TFun(TOrelse), S3};
625+
626+
false ->
627+
{TArgs, SA} = translate_args([Expr, List], Ann, S),
628+
{{call, Ann, {remote, Ann, {atom, Ann, lists}, {atom, Ann, member}}, TArgs}, SA}
629+
end;
592630
translate_remote(maps, merge, Meta, [Map1, Map2], S) ->
593631
Ann = ?ann(Meta),
594632

@@ -632,6 +670,14 @@ translate_remote(Left, Right, Meta, Args, S) ->
632670
{{call, Ann, {remote, Ann, TLeft, TRight}, TArgs}, SA}
633671
end.
634672

673+
optimize_list_membership([Head | Tail], Count)
674+
when Count =< 32, is_number(Head) orelse is_atom(Head) orelse is_binary(Head) ->
675+
optimize_list_membership(Tail, Count + 1);
676+
optimize_list_membership([], _Count) ->
677+
true;
678+
optimize_list_membership(_, _Count) ->
679+
false.
680+
635681
rewrite_strategy(erlang, Right, Args) ->
636682
Arity = length(Args),
637683
case elixir_utils:guard_op(Right, Arity) of

lib/elixir/src/elixir_rewrite.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ inline(Mod, Fun, Arity) -> inner_inline(ex_to_erl, Mod, Fun, Arity).
109109
?inline(?kernel, floor, 1, erlang, floor);
110110
?inline(?kernel, 'function_exported?', 3, erlang, function_exported);
111111
?inline(?kernel, hd, 1, erlang, hd);
112+
?inline(?kernel, in, 2, lists, member);
112113
?inline(?kernel, is_atom, 1, erlang, is_atom);
113114
?inline(?kernel, is_binary, 1, erlang, is_binary);
114115
?inline(?kernel, is_bitstring, 1, erlang, is_bitstring);

lib/elixir/test/elixir/kernel_test.exs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -696,16 +696,7 @@ defmodule KernelTest do
696696

697697
# Lists
698698
result = expand_to_string(quote(do: rand() in [1, 2]))
699-
assert result =~ "var = rand()"
700-
701-
result = expand_to_string(quote(do: var in [1, 2]), :guard)
702-
assert result =~ ":erlang.orelse(:erlang.\"=:=\"(var, 1), :erlang.\"=:=\"(var, 2))"
703-
704-
result = expand_to_string(quote(do: rand() in [1 | [2]]))
705-
assert result =~ ":lists.member(rand(), [1 | [2]]"
706-
707-
result = expand_to_string(quote(do: rand() in [1 | some_call()]))
708-
assert result =~ ":lists.member(rand(), [1 | some_call()]"
699+
assert result =~ ":lists.member(rand(), [1, 2]"
709700
end
710701

711702
test "is optimized" do

lib/elixir/test/elixir/macro_test.exs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -596,15 +596,17 @@ defmodule MacroTest do
596596
end
597597

598598
test "left-associative operators" do
599-
{result, formatted} = dbg_format(List.first([]) || "yes" || raise("foo"))
599+
{result, formatted} =
600+
dbg_format(List.first([]) || Process.get(:unknown, "yes") || raise("foo"))
601+
600602
assert result == "yes"
601603

602604
assert formatted =~ "macro_test.exs"
603605

604606
assert formatted =~ """
605607
List.first([]) #=> nil
606-
List.first([]) || "yes" #=> "yes"
607-
List.first([]) || "yes" || raise "foo" #=> "yes"
608+
List.first([]) || Process.get(:unknown, "yes") #=> "yes"
609+
List.first([]) || Process.get(:unknown, "yes") || raise "foo" #=> "yes"
608610
"""
609611
end
610612

0 commit comments

Comments
 (0)