Skip to content

Commit bc8948f

Browse files
committed
Add more tests
1 parent 0a77a35 commit bc8948f

File tree

4 files changed

+103
-74
lines changed

4 files changed

+103
-74
lines changed

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,19 @@ defmodule Module.Types.Apply do
610610
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
611611
end
612612

613+
## Funs
614+
615+
def fun_apply(fun_type, args_types, call, stack, context) do
616+
case fun_apply(fun_type, args_types) do
617+
{:ok, res} ->
618+
{res, context}
619+
620+
reason ->
621+
error = {{:badapply, reason}, args_types, fun_type, call, context}
622+
{error_type(), error(__MODULE__, error, elem(call, 1), stack, context)}
623+
end
624+
end
625+
613626
## Local
614627

615628
def local_domain(fun, args, expected, meta, stack, context) do
@@ -805,6 +818,64 @@ defmodule Module.Types.Apply do
805818

806819
## Diagnostics
807820

821+
def format_diagnostic({{:badapply, reason}, args_types, fun_type, expr, context}) do
822+
traces =
823+
if reason == :badarg do
824+
collect_traces(expr, context)
825+
else
826+
# In case there the type itself is invalid,
827+
# we limit the trace.
828+
collect_traces(elem(expr, 0), context)
829+
end
830+
831+
message =
832+
case reason do
833+
:badarg ->
834+
"""
835+
expected a #{length(args_types)}-arity function on call:
836+
837+
#{expr_to_string(expr) |> indent(4)}
838+
839+
but got type:
840+
841+
#{to_quoted_string(fun_type) |> indent(4)}
842+
"""
843+
844+
{:badarity, arities} ->
845+
info =
846+
case arities do
847+
[arity] -> "function with arity #{arity}"
848+
_ -> "function with arities #{Enum.join(arities, ",")}"
849+
end
850+
851+
"""
852+
expected a #{length(args_types)}-arity function on call:
853+
854+
#{expr_to_string(expr) |> indent(4)}
855+
856+
but got #{info}:
857+
858+
#{to_quoted_string(fun_type) |> indent(4)}
859+
"""
860+
861+
:badfun ->
862+
"""
863+
expected a #{length(args_types)}-arity function on call:
864+
865+
#{expr_to_string(expr) |> indent(4)}
866+
867+
but got type:
868+
869+
#{to_quoted_string(fun_type) |> indent(4)}
870+
"""
871+
end
872+
873+
%{
874+
details: %{typing_traces: traces},
875+
message: IO.iodata_to_binary([message, format_traces(traces)])
876+
}
877+
end
878+
808879
def format_diagnostic({:badlocal, {_, domain, clauses}, args_types, expr, context}) do
809880
domain = domain(domain, clauses)
810881
traces = collect_traces(expr, context)

lib/elixir/lib/module/types/descr.ex

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -717,39 +717,6 @@ defmodule Module.Types.Descr do
717717
do: {type, [], []}
718718
end
719719

720-
## Funs
721-
722-
@doc """
723-
Checks if a function type with the specified arity exists in the descriptor.
724-
725-
1. If there is no dynamic component:
726-
- The static part must be a non-empty function type of the given arity
727-
728-
2. If there is a dynamic component:
729-
- Either the static part is a non-empty function type of the given arity, or
730-
- The static part is empty and the dynamic part contains functions of the given arity
731-
"""
732-
# TODO: REMOVE ME
733-
def fun_fetch(:term, _arity), do: :error
734-
735-
def fun_fetch(%{} = descr, arity) when is_integer(arity) do
736-
case :maps.take(:dynamic, descr) do
737-
:error ->
738-
if not empty?(descr) and fun_only?(descr, arity), do: :ok, else: :error
739-
740-
{dynamic, static} ->
741-
empty_static? = empty?(static)
742-
743-
cond do
744-
not empty_static? -> if fun_only?(static, arity), do: :ok, else: :error
745-
empty_static? and not empty?(intersection(dynamic, fun(arity))) -> :ok
746-
true -> :error
747-
end
748-
end
749-
end
750-
751-
defp fun_only?(descr, arity), do: empty?(difference(descr, fun(arity)))
752-
753720
## Atoms
754721

755722
# The atom component of a type consists of pairs `{tag, set}` where `set` is a
@@ -984,22 +951,26 @@ defmodule Module.Types.Descr do
984951
@doc """
985952
Applies a function type to a list of argument types.
986953
987-
Returns the result type if the application is valid, or `:badarg` if not.
954+
Returns `{:ok, result}` if the application is valid
955+
or one `:badarg`, `:badfun`, `{:badarity, arities}` if not.
988956
989957
Handles both static and dynamic function types:
958+
990959
1. For static functions: checks exact argument types
991960
2. For dynamic functions: computes result based on both static and dynamic parts
992961
3. For mixed static/dynamic: computes all valid combinations
993962
994-
# Function application formula for dynamic types:
995-
# τ◦τ′ = (lower_bound(τ) ◦ upper_bound(τ′)) ∨ (dynamic(upper_bound(τ) ◦ lower_bound(τ′)))
996-
#
997-
# Where:
998-
# - τ is a dynamic function type
999-
# - τ′ are the arguments
1000-
# - ◦ is function application
1001-
#
1002-
# For more details, see Definition 6.15 in https://vlanvin.fr/papers/thesis.pdf
963+
## Function application formula for dynamic types
964+
965+
τ◦τ′ = (lower_bound(τ) ◦ upper_bound(τ′)) ∨ (dynamic(upper_bound(τ) ◦ lower_bound(τ′)))
966+
967+
Where:
968+
969+
- τ is a dynamic function type
970+
- τ′ are the arguments
971+
- ◦ is function application
972+
973+
For more details, see Definition 6.15 in https://vlanvin.fr/papers/thesis.pdf
1003974
1004975
## Examples
1005976
@@ -1028,6 +999,13 @@ defmodule Module.Types.Descr do
1028999
:badfun
10291000
end
10301001

1002+
# Optimize the cases where dynamic closes over all function types
1003+
{:term, fun_static} when fun_static == %{} ->
1004+
{:ok, dynamic()}
1005+
1006+
{%{fun: @fun_top}, fun_static} when fun_static == %{} ->
1007+
{:ok, dynamic()}
1008+
10311009
{fun_dynamic, fun_static} ->
10321010
if fun_only?(fun_static) do
10331011
fun_apply_with_strategy(fun_static, fun_dynamic, arguments)

lib/elixir/lib/module/types/expr.ex

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -450,20 +450,13 @@ defmodule Module.Types.Expr do
450450
end
451451

452452
# TODO: fun.(args)
453-
def of_expr({{:., meta, [fun]}, _meta, args} = call, _expected, _expr, stack, context) do
453+
def of_expr({{:., _, [fun]}, _, args} = call, _expected, _expr, stack, context) do
454454
{fun_type, context} = of_expr(fun, fun(length(args)), call, stack, context)
455455

456-
{_args_types, context} =
456+
{args_types, context} =
457457
Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
458458

459-
case fun_fetch(fun_type, length(args)) do
460-
:ok ->
461-
{dynamic(), context}
462-
463-
:error ->
464-
error = {:badfun, length(args), fun_type, fun, call, context}
465-
{error_type(), error(__MODULE__, error, meta, stack, context)}
466-
end
459+
Apply.fun_apply(fun_type, args_types, call, stack, context)
467460
end
468461

469462
def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context)
@@ -863,27 +856,6 @@ defmodule Module.Types.Expr do
863856
}
864857
end
865858

866-
def format_diagnostic({:badfun, arity, type, fun_expr, call_expr, context}) do
867-
traces = collect_traces(fun_expr, context)
868-
869-
%{
870-
details: %{typing_traces: traces},
871-
message:
872-
IO.iodata_to_binary([
873-
"""
874-
expected a #{arity}-arity function on call:
875-
876-
#{expr_to_string(call_expr) |> indent(4)}
877-
878-
but got type:
879-
880-
#{to_quoted_string(type) |> indent(4)}
881-
""",
882-
format_traces(traces)
883-
])
884-
}
885-
end
886-
887859
def format_diagnostic({:badcond, explain, type, expr, context}) do
888860
traces = collect_traces(expr, context)
889861

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,10 @@ defmodule Module.Types.DescrTest do
758758
end
759759

760760
test "static" do
761+
# Full static
762+
assert fun_apply(fun(), [integer()]) == {:ok, term()}
763+
assert fun_apply(difference(fun(), fun(2)), [integer()]) == {:ok, term()}
764+
761765
# Basic function application scenarios
762766
assert fun_apply(fun([integer()], atom()), [integer()]) == {:ok, atom()}
763767
assert fun_apply(fun([integer()], atom()), [float()]) == :badarg
@@ -820,6 +824,10 @@ defmodule Module.Types.DescrTest do
820824
defp dynamic_fun(args, return), do: dynamic(fun(args, return))
821825

822826
test "dynamic" do
827+
# Full dynamic
828+
assert fun_apply(dynamic(), [integer()]) == {:ok, dynamic()}
829+
assert fun_apply(difference(dynamic(), integer()), [integer()]) == {:ok, dynamic()}
830+
823831
# Basic function application scenarios
824832
assert fun_apply(dynamic_fun([integer()], atom()), [integer()]) == {:ok, dynamic(atom())}
825833
assert fun_apply(dynamic_fun([integer()], atom()), [float()]) == :badarg

0 commit comments

Comments
 (0)