Skip to content

Commit 77f1a85

Browse files
committed
Properly implement static and dynamic modes
1 parent 1b29932 commit 77f1a85

File tree

4 files changed

+112
-50
lines changed

4 files changed

+112
-50
lines changed

lib/elixir/lib/module/types.ex

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ defmodule Module.Types do
88
context = context()
99

1010
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
11-
stack = stack(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
11+
stack =
12+
stack(:dynamic, with_file_meta(meta, file), module, function, no_warn_undefined, cache)
1213

1314
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
1415
try do
@@ -58,7 +59,8 @@ defmodule Module.Types do
5859
end
5960

6061
@doc false
61-
def stack(file, module, function, no_warn_undefined, cache) do
62+
def stack(mode, file, module, function, no_warn_undefined, cache)
63+
when mode in [:static, :dynamic, :infer] do
6264
%{
6365
# The fallback meta used for literals in patterns and guards
6466
meta: [],
@@ -71,7 +73,29 @@ defmodule Module.Types do
7173
# List of calls to not warn on as undefined
7274
no_warn_undefined: no_warn_undefined,
7375
# A list of cached modules received from the parallel compiler
74-
cache: cache
76+
cache: cache,
77+
# The mode control what happens on function application when
78+
# there are gradual arguments. Non-gradual arguments always
79+
# perform subtyping and return its output (OUT).
80+
#
81+
# The mode may also control exhaustiveness checks in the future
82+
# (to be decided).
83+
#
84+
# * :strict - Requires types signatures (not implemented).
85+
# * Strong arrows with gradual performs subtyping and returns OUT
86+
# * Weak arrows with gradual performs subtyping and returns OUT
87+
#
88+
# * :static - Type signatures have been given.
89+
# * Strong arrows with gradual performs compatibility and returns OUT
90+
# * Weak arrows with gradual performs compatibility and returns OUT
91+
#
92+
# * :dynamic - Type signatures have not been given.
93+
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
94+
# * Weak arrows with gradual performs compatibility and returns dynamic()
95+
#
96+
# * :infer - Same as :dynamic but skips remote calls.
97+
#
98+
mode: mode
7599
}
76100
end
77101

lib/elixir/lib/module/types/of.ex

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,6 @@ defmodule Module.Types.Of do
422422
end
423423
end
424424

425-
# TODO: Implement element without a literal index
426-
427425
def apply(:erlang, :element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
428426
when is_integer(index) do
429427
case tuple_fetch(tuple, index - 1) do
@@ -498,7 +496,7 @@ defmodule Module.Types.Of do
498496
end
499497
end
500498

501-
def apply(:erlang, name, [left, right], expr, stack, context)
499+
def apply(:erlang, name, [left, right] = args_types, expr, stack, context)
502500
when name in [:>=, :"=<", :>, :<, :min, :max] do
503501
context =
504502
cond do
@@ -518,19 +516,14 @@ defmodule Module.Types.Of do
518516
context
519517
end
520518

521-
cond do
522-
name in [:min, :max] ->
523-
{union(left, right), context}
524-
525-
gradual?(left) or gradual?(right) ->
526-
{dynamic(boolean()), context}
527-
528-
true ->
529-
{boolean(), context}
519+
if name in [:min, :max] do
520+
{union(left, right), context}
521+
else
522+
{comparison_return(boolean(), args_types, stack), context}
530523
end
531524
end
532525

533-
def apply(:erlang, name, [left, right], expr, stack, context)
526+
def apply(:erlang, name, [left, right] = args_types, expr, stack, context)
534527
when name in [:==, :"/=", :"=:=", :"=/="] do
535528
context =
536529
cond do
@@ -545,11 +538,7 @@ defmodule Module.Types.Of do
545538
context
546539
end
547540

548-
if gradual?(left) or gradual?(right) do
549-
{dynamic(boolean()), context}
550-
else
551-
{boolean(), context}
552-
end
541+
{comparison_return(boolean(), args_types, stack), context}
553542
end
554543

555544
def apply(mod, name, args_types, expr, stack, context) do
@@ -562,7 +551,7 @@ defmodule Module.Types.Of do
562551
false ->
563552
{info, context} = remote(mod, name, arity, elem(expr, 1), stack, context)
564553

565-
case apply_remote(info, args_types) do
554+
case apply_remote(info, args_types, stack) do
566555
{:ok, type} ->
567556
{type, context}
568557

@@ -573,18 +562,43 @@ defmodule Module.Types.Of do
573562
end
574563
end
575564

576-
defp apply_remote(:none, _args_types) do
565+
defp comparison_return(type, args_types, stack) do
566+
cond do
567+
stack.mode == :static -> type
568+
Enum.any?(args_types, &gradual?/1) -> dynamic(type)
569+
true -> type
570+
end
571+
end
572+
573+
defp apply_remote(:none, _args_types, _stack) do
577574
{:ok, dynamic()}
578575
end
579576

580-
defp apply_remote({:strong, clauses}, args_types) do
581-
Enum.find_value(clauses, {:error, clauses}, fn {expected, return} ->
582-
if zip_compatible?(args_types, expected) do
583-
{:ok, return}
577+
defp apply_remote({:strong, clauses}, args_types, stack) do
578+
if Enum.any?(args_types) do
579+
returns =
580+
for({expected, return} <- clauses, zip_compatible?(args_types, expected), do: return)
581+
582+
cond do
583+
returns == [] -> {:error, clauses}
584+
stack.mode == :static -> {:ok, Enum.reduce(returns, &union/2)}
585+
true -> {:ok, dynamic(Enum.reduce(returns, &union/2))}
584586
end
585-
end)
587+
else
588+
Enum.find_value(clauses, {:error, clauses}, fn {expected, return} ->
589+
if zip_subtype?(args_types, expected) do
590+
{:ok, return}
591+
end
592+
end)
593+
end
586594
end
587595

596+
defp zip_subtype?([actual | actuals], [expected | expecteds]) do
597+
subtype?(actual, expected) and zip_subtype?(actuals, expecteds)
598+
end
599+
600+
defp zip_subtype?([], []), do: true
601+
588602
defp zip_compatible?([actual | actuals], [expected | expecteds]) do
589603
compatible?(actual, expected) and zip_compatible?(actuals, expecteds)
590604
end

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -575,10 +575,22 @@ defmodule Module.Types.ExprTest do
575575
end
576576

577577
describe "comparison" do
578-
test "works across numbers" do
578+
test "in static mode" do
579+
assert typecheck!([x = 123, y = 456.0], x < y) == boolean()
580+
assert typecheck!([x = 123, y = 456.0], x == y) == boolean()
581+
end
582+
583+
test "in dynamic mode" do
584+
assert typedyn!([x = 123, y = 456.0], x < y) == dynamic(boolean())
585+
assert typedyn!([x = 123, y = 456.0], x == y) == dynamic(boolean())
586+
assert typedyn!(123 == 456) == boolean()
587+
end
588+
589+
test "min/max" do
590+
assert typecheck!(min(123, 456.0)) == union(integer(), float())
591+
# min/max uses parametric types, which will carry dynamic regardless of being a strong arrow
579592
assert typecheck!([x = 123, y = 456.0], min(x, y)) == dynamic(union(integer(), float()))
580-
assert typecheck!([x = 123, y = 456.0], x < y) == dynamic(boolean())
581-
assert typecheck!([x = 123, y = 456.0], x == y) == dynamic(boolean())
593+
assert typedyn!([x = 123, y = 456.0], min(x, y)) == dynamic(union(integer(), float()))
582594
end
583595

584596
test "warns when comparison is constant" do
@@ -606,7 +618,7 @@ defmodule Module.Types.ExprTest do
606618
"""}
607619

608620
assert typewarn!([x = 123, y = 456.0], x === y) ==
609-
{dynamic(boolean()),
621+
{boolean(),
610622
~l"""
611623
comparison between incompatible types found:
612624
@@ -631,7 +643,7 @@ defmodule Module.Types.ExprTest do
631643

632644
test "warns on comparison with struct across dynamic call" do
633645
assert typewarn!([x = :foo, y = %Point{}, mod = Kernel], mod.<=(x, y)) ==
634-
{dynamic(boolean()),
646+
{boolean(),
635647
~l"""
636648
comparison with structs found:
637649
@@ -662,6 +674,9 @@ defmodule Module.Types.ExprTest do
662674

663675
describe ":erlang rewrites" do
664676
test "Integer.to_string/1" do
677+
assert typecheck!([x = 123], Integer.to_string(x)) == binary()
678+
assert typedyn!([x = 123], Integer.to_string(x)) == dynamic(binary())
679+
665680
assert typeerror!([x = :foo], Integer.to_string(x)) |> strip_ansi() ==
666681
~l"""
667682
incompatible types given to Integer.to_string/1:
@@ -685,6 +700,9 @@ defmodule Module.Types.ExprTest do
685700
end
686701

687702
test "Bitwise.bnot/1" do
703+
assert typecheck!([x = 123], Bitwise.bnot(x)) == integer()
704+
assert typedyn!([x = 123], Bitwise.bnot(x)) == dynamic(integer())
705+
688706
assert typeerror!([x = :foo], Bitwise.bnot(x)) |> strip_ansi() ==
689707
~l"""
690708
incompatible types given to Bitwise.bnot/1:

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

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,22 @@ defmodule TypeHelper do
1818
end
1919
end
2020

21+
@doc """
22+
Main helper for checking the given AST type checks without warnings.
23+
"""
24+
defmacro typedyn!(patterns \\ [], guards \\ true, body) do
25+
quote do
26+
unquote(typecheck(:dynamic, patterns, guards, body, __CALLER__))
27+
|> TypeHelper.__typecheck__!()
28+
end
29+
end
30+
2131
@doc """
2232
Main helper for checking the given AST type checks without warnings.
2333
"""
2434
defmacro typecheck!(patterns \\ [], guards \\ true, body) do
2535
quote do
26-
unquote(typecheck(patterns, guards, body, __CALLER__))
36+
unquote(typecheck(:static, patterns, guards, body, __CALLER__))
2737
|> TypeHelper.__typecheck__!()
2838
end
2939
end
@@ -35,7 +45,7 @@ defmodule TypeHelper do
3545
[patterns, guards, body] = prune_columns([patterns, guards, body])
3646

3747
quote do
38-
unquote(typecheck(patterns, guards, body, __CALLER__))
48+
unquote(typecheck(:static, patterns, guards, body, __CALLER__))
3949
|> TypeHelper.__typeerror__!()
4050
end
4151
end
@@ -47,7 +57,7 @@ defmodule TypeHelper do
4757
[patterns, guards, body] = prune_columns([patterns, guards, body])
4858

4959
quote do
50-
unquote(typecheck(patterns, guards, body, __CALLER__))
60+
unquote(typecheck(:static, patterns, guards, body, __CALLER__))
5161
|> TypeHelper.__typewarn__!()
5262
end
5363
end
@@ -57,7 +67,7 @@ defmodule TypeHelper do
5767
"""
5868
defmacro typediag!(patterns \\ [], guards \\ true, body) do
5969
quote do
60-
unquote(typecheck(patterns, guards, body, __CALLER__))
70+
unquote(typecheck(:static, patterns, guards, body, __CALLER__))
6171
|> TypeHelper.__typediag__!()
6272
end
6373
end
@@ -104,10 +114,7 @@ defmodule TypeHelper do
104114
def __typewarn__!({_type, %{warnings: warnings, failed: true}}),
105115
do: raise("type checking errored with warnings: #{inspect(warnings)}")
106116

107-
@doc """
108-
Building block for typeinferring a given AST.
109-
"""
110-
def typeinfer(patterns, guards, env) do
117+
defp typeinfer(patterns, guards, env) do
111118
{patterns, guards, :ok} = expand_and_unpack(patterns, guards, :ok, env)
112119

113120
quote do
@@ -119,26 +126,24 @@ defmodule TypeHelper do
119126
end
120127

121128
def __typeinfer__(patterns, guards) do
122-
Pattern.of_head(patterns, guards, [], new_stack(), new_context())
129+
Pattern.of_head(patterns, guards, [], new_stack(:infer), new_context())
123130
end
124131

125-
@doc """
126-
Building block for typechecking a given AST.
127-
"""
128-
def typecheck(patterns, guards, body, env) do
132+
defp typecheck(mode, patterns, guards, body, env) do
129133
{patterns, guards, body} = expand_and_unpack(patterns, guards, body, env)
130134

131135
quote do
132136
TypeHelper.__typecheck__(
137+
unquote(mode),
133138
unquote(Macro.escape(patterns)),
134139
unquote(Macro.escape(guards)),
135140
unquote(Macro.escape(body))
136141
)
137142
end
138143
end
139144

140-
def __typecheck__(patterns, guards, body) do
141-
stack = new_stack()
145+
def __typecheck__(mode, patterns, guards, body) do
146+
stack = new_stack(mode)
142147
{_types, context} = Pattern.of_head(patterns, guards, [], stack, new_context())
143148
Expr.of_expr(body, stack, context)
144149
end
@@ -155,8 +160,9 @@ defmodule TypeHelper do
155160
{patterns, guards, body}
156161
end
157162

158-
defp new_stack() do
159-
Types.stack("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache())
163+
defp new_stack(mode) do
164+
cache = Module.ParallelChecker.test_cache()
165+
Types.stack(mode, "types_test.ex", TypesTest, {:test, 0}, [], cache)
160166
end
161167

162168
defp new_context() do

0 commit comments

Comments
 (0)