Skip to content

Commit 41b4b00

Browse files
committed
is_function/2
1 parent 7ab2145 commit 41b4b00

5 files changed

Lines changed: 168 additions & 32 deletions

File tree

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ defmodule Module.Types.Apply do
119119
|> union(tuple([fun(), args_or_arity]))
120120
)
121121

122+
not_signature =
123+
for bool <- [true, false] do
124+
{[atom([bool])], atom([not bool])}
125+
end
126+
122127
and_signature =
123128
for left <- [true, false], right <- [true, false] do
124129
{[atom([left]), atom([right])], atom([left and right])}
@@ -206,7 +211,7 @@ defmodule Module.Types.Apply do
206211
{:erlang, :map_size, [{[open_map()], integer()}]},
207212
{:erlang, :node, [{[], atom()}]},
208213
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
209-
{:erlang, :not, [{[atom([false])], atom([true])}, {[atom([true])], atom([false])}]},
214+
{:erlang, :not, not_signature},
210215
{:erlang, :or, or_signature},
211216
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
212217
{:erlang, :rem, [{[integer(), integer()], integer()}]},
@@ -263,14 +268,14 @@ defmodule Module.Types.Apply do
263268
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
264269
{:maps, :get, [{[term(), open_map()], term()}]},
265270
{:maps, :is_key, [{[term(), open_map()], boolean()}]},
266-
{:maps, :keys, [{[open_map()], dynamic(list(term()))}]},
271+
{:maps, :keys, [{[open_map()], list(term())}]},
267272
{:maps, :put, [{[term(), term(), open_map()], open_map()}]},
268273
{:maps, :remove, [{[term(), open_map()], open_map()}]},
269274
{:maps, :take,
270275
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
271-
{:maps, :to_list, [{[open_map()], dynamic(list(tuple([term(), term()])))}]},
276+
{:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]},
272277
{:maps, :update, [{[term(), term(), open_map()], open_map()}]},
273-
{:maps, :values, [{[open_map()], dynamic(list(term()))}]}
278+
{:maps, :values, [{[open_map()], list(term())}]}
274279
] do
275280
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
276281

@@ -320,6 +325,20 @@ defmodule Module.Types.Apply do
320325
{:none, Enum.map(args, fn _ -> term() end), context}
321326
end
322327

328+
@is_function_info {:strong, nil, [{[term(), integer()], boolean()}]}
329+
330+
def remote_domain(:erlang, :is_function, [_, arity], expected, _meta, _stack, context)
331+
when is_integer(arity) and arity >= 0 do
332+
arg =
333+
case booleaness(expected) do
334+
:always_true -> fun(arity)
335+
:always_false -> negation(fun(arity))
336+
:undefined -> term()
337+
end
338+
339+
{@is_function_info, [arg, integer()], context}
340+
end
341+
323342
def remote_domain(:erlang, :element, [index, _], expected, _meta, _stack, context)
324343
when is_integer(index) do
325344
tuple = open_tuple(List.duplicate(term(), max(index - 1, 0)) ++ [expected])

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,40 @@ defmodule Module.Types.Descr do
902902
:sets.from_list([false], version: 2)
903903
]
904904

905+
@doc """
906+
Compute the booleaness of an element.
907+
908+
It is either :undefined, :always_true, or :always_false.
909+
"""
910+
def booleaness(:term), do: :undefined
911+
912+
def booleaness(%{} = descr) do
913+
descr = Map.get(descr, :dynamic, descr)
914+
915+
case descr do
916+
%{atom: {:union, %{true => _, false => _}}} ->
917+
:undefined
918+
919+
%{atom: {:union, %{true => _}}} ->
920+
:always_true
921+
922+
%{atom: {:union, %{false => _}}} ->
923+
:always_false
924+
925+
%{atom: {:negation, %{true => _, false => _}}} ->
926+
:undefined
927+
928+
%{atom: {:negation, %{true => _}}} ->
929+
:always_false
930+
931+
%{atom: {:negation, %{false => _}}} ->
932+
:always_true
933+
934+
_ ->
935+
:undefined
936+
end
937+
end
938+
905939
@doc """
906940
Compute the truthiness of an element.
907941
@@ -1687,7 +1721,16 @@ defmodule Module.Types.Descr do
16871721
defp pivot([], _acc, _fun), do: :error
16881722

16891723
# Converts a function BDD (Binary Decision Diagram) to its quoted representation
1690-
defp fun_to_quoted({:negation, _bdds}, _opts), do: [{:fun, [], []}]
1724+
defp fun_to_quoted({:negation, bdds}, opts) do
1725+
case fun_to_quoted({:union, bdds}, opts) do
1726+
[] ->
1727+
[{:fun, [], []}]
1728+
1729+
parts ->
1730+
ors = Enum.reduce(parts, &{:or, [], [&2, &1]})
1731+
[{:and, [], [{:fun, [], []}, {:not, [], [ors]}]}]
1732+
end
1733+
end
16911734

16921735
defp fun_to_quoted({:union, bdds}, opts) do
16931736
for {arity, bdd} <- bdds,

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ defmodule Module.Types.Pattern do
3434

3535
def of_head(patterns, guards, expected, tag, meta, stack, context) do
3636
stack = %{stack | meta: meta}
37-
3837
{trees, context} = of_pattern_args(patterns, expected, tag, stack, context)
3938
{_, context} = of_guards(guards, stack, context)
4039
{trees, context}
@@ -762,6 +761,7 @@ defmodule Module.Types.Pattern do
762761
# the environment vars.
763762

764763
@guard atom([true, false, :fail])
764+
@atom_true atom([true])
765765

766766
defp of_guards([], _stack, context) do
767767
{[], context}
@@ -913,7 +913,16 @@ defmodule Module.Types.Pattern do
913913
Apply.remote_apply(info, :erlang, fun, args_types, call, stack, context)
914914
end
915915

916-
defp of_remote(fun, meta, args, call, {_root, expected}, stack, context) do
916+
defp of_remote(fun, meta, args, call, {root, expected}, stack, context) do
917+
# If we are the root, we are only interested in positive results,
918+
# except for the operations that can return :fail.
919+
expected =
920+
if root and fun not in [:element, :hd, :map_get, :max, :min, :tl] do
921+
@atom_true
922+
else
923+
expected
924+
end
925+
917926
{info, domain, context} =
918927
Apply.remote_domain(:erlang, fun, args, expected, meta, stack, context)
919928

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ end
1414
defmodule Module.Types.DescrTest do
1515
use ExUnit.Case, async: true
1616

17-
import Module.Types.Descr, except: [fun: 1]
17+
import Module.Types.Descr
1818
defmacro domain_key(arg) when is_atom(arg), do: [arg]
1919

2020
defp number(), do: union(integer(), float())
@@ -1225,6 +1225,23 @@ defmodule Module.Types.DescrTest do
12251225
end
12261226

12271227
describe "projections" do
1228+
test "booleaness" do
1229+
for type <- [term(), none(), atom(), boolean(), integer()] do
1230+
assert booleaness(type) == :undefined
1231+
assert booleaness(dynamic(type)) == :undefined
1232+
end
1233+
1234+
for type <- [atom([false]), atom([:other, false]), negation(atom([true]))] do
1235+
assert booleaness(type) == :always_false
1236+
assert booleaness(dynamic(type)) == :always_false
1237+
end
1238+
1239+
for type <- [atom([true]), atom([:other, true]), negation(atom([false]))] do
1240+
assert booleaness(type) == :always_true
1241+
assert booleaness(dynamic(type)) == :always_true
1242+
end
1243+
end
1244+
12281245
test "truthiness" do
12291246
for type <- [term(), none(), atom(), boolean(), union(atom([false]), integer())] do
12301247
assert truthiness(type) == :undefined
@@ -2710,6 +2727,13 @@ defmodule Module.Types.DescrTest do
27102727
|> union(fun([pid()], pid()))
27112728
|> to_quoted_string() ==
27122729
"(integer() -> integer()) or (float() -> float()) or (pid() -> pid())"
2730+
2731+
assert fun(3) |> to_quoted_string() == "(none(), none(), none() -> term())"
2732+
2733+
assert intersection(fun(), negation(fun())) |> to_quoted_string() == "none()"
2734+
2735+
assert intersection(fun(), negation(fun(3))) |> to_quoted_string() ==
2736+
"fun() and not (none(), none(), none() -> term())"
27132737
end
27142738

27152739
test "function with optimized intersections" do

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

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -182,25 +182,6 @@ defmodule Module.Types.PatternTest do
182182
m = 123
183183
"""
184184
end
185-
186-
test "fields in guards" do
187-
assert typeerror!([x = %Point{}], x.foo_bar, :ok) ==
188-
~l"""
189-
unknown key .foo_bar in expression:
190-
191-
x.foo_bar
192-
193-
the given type does not have the given key:
194-
195-
dynamic(%Point{x: term(), y: term(), z: term()})
196-
197-
where "x" was given the type:
198-
199-
# type: dynamic(%Point{})
200-
# from: types_test.ex:LINE-1
201-
x = %Point{}
202-
"""
203-
end
204185
end
205186

206187
describe "maps" do
@@ -241,11 +222,6 @@ defmodule Module.Types.PatternTest do
241222
)
242223
end
243224

244-
test "atom keys in guards" do
245-
assert typecheck!([x = %{foo: :bar}], x.bar, x) ==
246-
dynamic(open_map(foo: atom([:bar]), bar: atom([true, false, :fail])))
247-
end
248-
249225
test "domain keys in patterns" do
250226
assert typecheck!([x = %{123 => 456}], x) == dynamic(open_map())
251227
assert typecheck!([x = %{123 => 456, foo: :bar}], x) == dynamic(open_map(foo: atom([:bar])))
@@ -413,6 +389,71 @@ defmodule Module.Types.PatternTest do
413389
end
414390

415391
describe "guards" do
392+
test "not" do
393+
assert typecheck!([x], not x, x) == dynamic(atom([false]))
394+
395+
assert typecheck!([x], not x.foo, x) == dynamic(open_map(foo: atom([false])))
396+
397+
assert typeerror!([x], not length(x), x) |> strip_ansi() == ~l"""
398+
incompatible types given to Kernel.not/1:
399+
400+
not length(x)
401+
402+
given types:
403+
404+
integer()
405+
406+
but expected one of:
407+
408+
#1
409+
true
410+
411+
#2
412+
false
413+
414+
where "x" was given the type:
415+
416+
# type: dynamic()
417+
# from: types_test.ex:LINE
418+
x
419+
"""
420+
end
421+
422+
test "is_function/2" do
423+
assert typecheck!([x], is_function(x, 3), x) == dynamic(fun(3))
424+
assert typecheck!([x], not is_function(x, 3), x) == dynamic(negation(fun(3)))
425+
end
426+
427+
test "elem" do
428+
assert typecheck!([x], elem(x, 1), x) ==
429+
dynamic(open_tuple([term(), atom([true, false, :fail])]))
430+
431+
assert typecheck!([x], not elem(x, 1), x) ==
432+
dynamic(open_tuple([term(), atom([false])]))
433+
end
434+
435+
test "map.field" do
436+
assert typecheck!([x = %{foo: :bar}], x.bar, x) ==
437+
dynamic(open_map(foo: atom([:bar]), bar: atom([true, false, :fail])))
438+
439+
assert typeerror!([x = %Point{}], x.foo_bar, :ok) ==
440+
~l"""
441+
unknown key .foo_bar in expression:
442+
443+
x.foo_bar
444+
445+
the given type does not have the given key:
446+
447+
dynamic(%Point{x: term(), y: term(), z: term()})
448+
449+
where "x" was given the type:
450+
451+
# type: dynamic(%Point{})
452+
# from: types_test.ex:LINE-1
453+
x = %Point{}
454+
"""
455+
end
456+
416457
test "domain checks propagate across all operations except 'orelse'" do
417458
assert typecheck!([x], [length(x) == 3], x) == dynamic(list(term()))
418459

0 commit comments

Comments
 (0)