Skip to content

Commit 2a7a37f

Browse files
committed
Add reverse arrows for min/max, closes #15210
1 parent 972df19 commit 2a7a37f

2 files changed

Lines changed: 82 additions & 46 deletions

File tree

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

Lines changed: 65 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ defmodule Module.Types.Apply do
389389
end
390390

391391
defp do_remote(:erlang, name, [left, right], expected, expr, stack, context, of_fun)
392-
when name in [:>=, :"=<", :>, :<, :min, :max] do
392+
when name in [:>=, :"=<", :>, :<] do
393393
case sized_order(name, left, right, expected) do
394394
{arg, expected, precise?, return} ->
395395
{actual, context} = of_fun.(arg, expected, expr, stack, context)
@@ -399,39 +399,41 @@ defmodule Module.Types.Apply do
399399
:none ->
400400
{left_type, context} = of_fun.(left, term(), expr, stack, context)
401401
{right_type, context} = of_fun.(right, term(), expr, stack, context)
402+
result = return(boolean(), [left_type, right_type], stack)
402403

403-
result =
404-
if name in [:min, :max] do
405-
union(left_type, right_type)
406-
else
407-
return(boolean(), [left_type, right_type], stack)
408-
end
409-
410-
if is_warning(stack) do
411-
common = intersection(left_type, right_type)
412-
413-
cond do
414-
# This check is incomplete. After all, we could have the number type nested
415-
# inside a tuple or a list and the comparison would still be valid.
416-
# However, nested comparison between distinct numbers is very uncommon,
417-
# so we only check the direct value here.
418-
empty?(common) and not (number_type?(left_type) and number_type?(right_type)) ->
419-
error = {:mismatched_comparison, left_type, right_type}
420-
remote_error(error, :erlang, name, 2, expr, stack, context)
421-
422-
match?({false, _}, map_fetch_key(dynamic(common), :__struct__)) ->
423-
error = {:struct_comparison, left_type, right_type}
424-
remote_error(error, :erlang, name, 2, expr, stack, context)
425-
426-
true ->
427-
{result, context}
428-
end
404+
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
405+
remote_error(error, :erlang, name, 2, expr, stack, context)
429406
else
430407
{result, context}
431408
end
432409
end
433410
end
434411

412+
defp do_remote(:erlang, name, [left, right], expected, expr, stack, context, of_fun)
413+
when name in [:min, :max] do
414+
# While comparison between distinct types are allowed,
415+
# we check for disjointedness, so we effectively require
416+
# left and right to have at least one type in common.
417+
# Overall, it behaves as if we had this signature:
418+
#
419+
# integer(), integer() -> integer()
420+
# float(), float() -> float()
421+
# float(), integer() -> number()
422+
# integer(), float() -> number()
423+
# a and not number(), b and not number() -> a and b
424+
#
425+
# However, during inference, we type it as `a, b -> a and b` only.
426+
{left_type, context} = of_fun.(left, expected, expr, stack, context)
427+
{right_type, context} = of_fun.(right, expected, expr, stack, context)
428+
result = union(left_type, right_type)
429+
430+
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
431+
remote_error(error, :erlang, name, 2, expr, stack, context)
432+
else
433+
{result, context}
434+
end
435+
end
436+
435437
defp do_remote(:erlang, :element, [index, tuple], expected, expr, stack, context, of_fun)
436438
when is_integer(index) do
437439
tuple_type = open_tuple(List.duplicate(term(), max(index - 1, 0)) ++ [expected])
@@ -684,28 +686,45 @@ defmodule Module.Types.Apply do
684686
end
685687
end
686688

687-
defp sized_order(name, left, right, expected) do
688-
if name in [:>=, :"=<", :>, :<] do
689-
case {left, right} do
690-
{{{:., _, [:erlang, fun]}, _, [arg]}, size} when is_data_size(fun, size) ->
691-
case booleaness(expected) do
692-
{true, _} -> sized_order(name, fun, size, arg, @atom_true)
693-
{false, _} -> sized_order(invert_order(name), fun, size, arg, @atom_false)
694-
_ -> :none
695-
end
689+
defp mismatched_ordered_comparison(left_type, right_type, stack) do
690+
if is_warning(stack) do
691+
common = intersection(left_type, right_type)
696692

697-
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
698-
case booleaness(expected) do
699-
{true, _} -> sized_order(invert_order(name), fun, size, arg, @atom_true)
700-
{false, _} -> sized_order(name, fun, size, arg, @atom_false)
701-
_ -> :none
702-
end
693+
cond do
694+
# This check is incomplete. After all, we could have the number type nested
695+
# inside a tuple or a list and the comparison would still be valid.
696+
# However, nested comparison between distinct numbers is very uncommon,
697+
# so we only check the direct value here.
698+
empty?(common) and not (number_type?(left_type) and number_type?(right_type)) ->
699+
{:mismatched_comparison, left_type, right_type}
703700

704-
_ ->
705-
:none
701+
match?({false, _}, map_fetch_key(dynamic(common), :__struct__)) ->
702+
{:struct_comparison, left_type, right_type}
703+
704+
true ->
705+
nil
706706
end
707-
else
708-
:none
707+
end
708+
end
709+
710+
defp sized_order(name, left, right, expected) do
711+
case {left, right} do
712+
{{{:., _, [:erlang, fun]}, _, [arg]}, size} when is_data_size(fun, size) ->
713+
case booleaness(expected) do
714+
{true, _} -> sized_order(name, fun, size, arg, @atom_true)
715+
{false, _} -> sized_order(invert_order(name), fun, size, arg, @atom_false)
716+
_ -> :none
717+
end
718+
719+
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
720+
case booleaness(expected) do
721+
{true, _} -> sized_order(invert_order(name), fun, size, arg, @atom_true)
722+
{false, _} -> sized_order(name, fun, size, arg, @atom_false)
723+
_ -> :none
724+
end
725+
726+
_ ->
727+
:none
709728
end
710729
end
711730

lib/elixir/test/elixir/module/types/pattern_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,23 @@ defmodule Module.Types.PatternTest do
790790
"""
791791
end
792792

793+
test "min/max" do
794+
assert typecheck!([x, y], is_integer(min(x, y)), min(x, y)) ==
795+
dynamic(integer())
796+
797+
assert typecheck!([x, y], is_number(min(x, y)), min(x, y)) ==
798+
dynamic(union(integer(), float()))
799+
800+
assert typecheck!([m], elem(m.pair, max(m.x, m.y)) > 0, m) ==
801+
dynamic(open_map(pair: open_tuple([]), x: integer(), y: integer()))
802+
803+
assert typeerror!(
804+
[x, y],
805+
is_integer(x) and is_binary(y) and is_integer(min(x, y)),
806+
min(x, y)
807+
) =~ "comparison between distinct types found"
808+
end
809+
793810
test "conditional checks (and/or)" do
794811
assert typecheck!([x], :erlang.or(is_binary(x), is_atom(x)), x) ==
795812
dynamic(union(binary(), atom()))

0 commit comments

Comments
 (0)