Skip to content

Commit 79f0019

Browse files
committed
Initial support for tuples and in elem/2
1 parent 69ad427 commit 79f0019

File tree

6 files changed

+141
-11
lines changed

6 files changed

+141
-11
lines changed

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

+29-9
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ defmodule Module.Types.Descr do
5959
def non_empty_list(), do: %{bitmap: @bit_non_empty_list}
6060
def open_map(), do: %{map: @map_top}
6161
def open_map(pairs), do: map_descr(:open, pairs)
62+
def open_tuple(elements), do: tuple_descr(:open, elements)
6263
def pid(), do: %{bitmap: @bit_pid}
6364
def port(), do: %{bitmap: @bit_port}
6465
def reference(), do: %{bitmap: @bit_reference}
65-
def open_tuple(elements), do: %{tuple: tuple_new(:open, elements)}
66-
def tuple(elements), do: %{tuple: tuple_new(:closed, elements)}
6766
def tuple(), do: %{tuple: @tuple_top}
67+
def tuple(elements), do: tuple_descr(:closed, elements)
6868

6969
@boolset :sets.from_list([true, false], version: 2)
7070
def boolean(), do: %{atom: {:union, @boolset}}
@@ -727,9 +727,8 @@ defmodule Module.Types.Descr do
727727
with the assumption that the descr is exclusively a map (or dynamic).
728728
729729
It returns a two element tuple or `:error`. The first element says
730-
if the type is optional or not, the second element is the type.
731-
In static mode, we likely want to raise if `map.field`
732-
(or pattern matching?) is called on an optional key.
730+
if the type is dynamically optional or not, the second element is
731+
the type. In static mode, optional keys are not allowed.
733732
"""
734733
def map_fetch(:term, _key), do: :badmap
735734

@@ -1234,6 +1233,28 @@ defmodule Module.Types.Descr do
12341233
# - {integer(), atom()} is encoded as {:closed, [integer(), atom()]}
12351234
# - {atom(), boolean(), ...} is encoded as {:open, [atom(), boolean()]}
12361235

1236+
defp tuple_descr(tag, fields) do
1237+
case tuple_descr(fields, [], false) do
1238+
{fields, true} -> %{dynamic: %{tuple: tuple_new(tag, Enum.reverse(fields))}}
1239+
{_, false} -> %{tuple: tuple_new(tag, fields)}
1240+
end
1241+
end
1242+
1243+
defp tuple_descr([:term | rest], acc, dynamic?) do
1244+
tuple_descr(rest, [:term | acc], dynamic?)
1245+
end
1246+
1247+
defp tuple_descr([value | rest], acc, dynamic?) do
1248+
case :maps.take(:dynamic, value) do
1249+
:error -> tuple_descr(rest, [value | acc], dynamic?)
1250+
{dynamic, _static} -> tuple_descr(rest, [dynamic | acc], true)
1251+
end
1252+
end
1253+
1254+
defp tuple_descr([], acc, dynamic?) do
1255+
{acc, dynamic?}
1256+
end
1257+
12371258
defp tuple_new(tag, elements), do: [{tag, elements, []}]
12381259

12391260
defp tuple_intersection(dnf1, dnf2) do
@@ -1395,8 +1416,9 @@ defmodule Module.Types.Descr do
13951416
with the assumption that the descr is exclusively a tuple (or dynamic).
13961417
13971418
Returns one of:
1419+
13981420
- `{false, type}` if the element is always accessible and has the given `type`.
1399-
- `{true, type}` if the element may exist and has the given `type`.
1421+
- `{true, type}` if the element is dynamically optional and has the given `type`.
14001422
- `:badindex` if the index is never accessible in the tuple type.
14011423
- `:badtuple` if the descr is not a tuple type.
14021424
@@ -1405,8 +1427,6 @@ defmodule Module.Types.Descr do
14051427
iex> tuple_fetch(tuple([integer(), atom()]), 0)
14061428
{false, integer()}
14071429
1408-
:badindex
1409-
14101430
iex> tuple_fetch(union(tuple([integer()]), tuple([integer(), atom()])), 1)
14111431
{true, atom()}
14121432
@@ -1420,7 +1440,7 @@ defmodule Module.Types.Descr do
14201440
def tuple_fetch(_, index) when index < 0, do: :badindex
14211441
def tuple_fetch(:term, _key), do: :badtuple
14221442

1423-
def tuple_fetch(%{} = descr, key) do
1443+
def tuple_fetch(%{} = descr, key) when is_integer(key) do
14241444
case :maps.take(:dynamic, descr) do
14251445
:error ->
14261446
if descr_key?(descr, :tuple) and tuple_only?(descr) do

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

+57
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,21 @@ defmodule Module.Types.Of do
266266

267267
## Apply
268268

269+
# TODO: Implement element without a literal index
270+
# TODO: Add a test for an open tuple (inferred from a guard)
271+
# TODO: Implement set_element
272+
273+
def apply(:erlang, :element, [_, type], {_, meta, [index, _]} = expr, stack, context)
274+
when is_integer(index) do
275+
case tuple_fetch(type, index - 1) do
276+
{_optional?, value_type} ->
277+
{:ok, value_type, context}
278+
279+
reason ->
280+
{:ok, dynamic(), warn({reason, expr, type, index - 1, context}, meta, stack, context)}
281+
end
282+
end
283+
269284
def apply(:erlang, name, [left, right], expr, stack, context)
270285
when name in [:>=, :"=<", :>, :<, :min, :max] do
271286
result = if name in [:min, :max], do: union(left, right), else: boolean()
@@ -619,6 +634,48 @@ defmodule Module.Types.Of do
619634
}
620635
end
621636

637+
def format_diagnostic({:badtuple, expr, type, index, context}) do
638+
traces = collect_traces(expr, context)
639+
640+
%{
641+
details: %{typing_traces: traces},
642+
message:
643+
IO.iodata_to_binary([
644+
"""
645+
expected a tuple when accessing element at index #{index} in expression:
646+
647+
#{expr_to_string(expr) |> indent(4)}
648+
649+
but got type:
650+
651+
#{to_quoted_string(type) |> indent(4)}
652+
""",
653+
format_traces(traces)
654+
])
655+
}
656+
end
657+
658+
def format_diagnostic({:badindex, expr, type, index, context}) do
659+
traces = collect_traces(expr, context)
660+
661+
%{
662+
details: %{typing_traces: traces},
663+
message:
664+
IO.iodata_to_binary([
665+
"""
666+
out of range index #{index} in expression:
667+
668+
#{expr_to_string(expr) |> indent(4)}
669+
670+
the given type does not have the given index:
671+
672+
#{to_quoted_string(type) |> indent(4)}
673+
""",
674+
format_traces(traces)
675+
])
676+
}
677+
end
678+
622679
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
623680
traces = collect_traces(expr, context)
624681

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ defmodule Module.Types.Pattern do
299299
# {...}
300300
defp of_shared({:{}, _meta, exprs}, _expected_expr, stack, context, fun) do
301301
case map_reduce_ok(exprs, context, &fun.(&1, {dynamic(), &1}, stack, &2)) do
302-
{:ok, _, context} -> {:ok, tuple(), context}
302+
{:ok, types, context} -> {:ok, tuple(types), context}
303303
{:error, reason} -> {:error, reason}
304304
end
305305
end

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

+4
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,10 @@ defmodule Module.Types.DescrTest do
719719

720720
test "tuples" do
721721
assert tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom()}"
722+
723+
assert tuple([integer(), dynamic(atom())]) |> to_quoted_string() ==
724+
"dynamic({integer(), atom()})"
725+
722726
assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}"
723727

724728
assert union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() ==

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

+43
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,48 @@ defmodule Module.Types.ExprTest do
204204
describe "tuples" do
205205
test "creating tuples" do
206206
assert typecheck!({:ok, 123}) == tuple([atom([:ok]), integer()])
207+
assert typecheck!([x], {:ok, x}) == dynamic(tuple([atom([:ok]), term()]))
208+
end
209+
210+
test "accessing tuples" do
211+
assert typecheck!(elem({:ok, 123}, 0)) == atom([:ok])
212+
assert typecheck!(elem({:ok, 123}, 1)) == integer()
213+
assert typecheck!([x], elem({:ok, x}, 0)) == dynamic(atom([:ok]))
214+
assert typecheck!([x], elem({:ok, x}, 1)) == dynamic(term())
215+
end
216+
217+
test "accessing an index on not a map" do
218+
assert typewarn!([<<x::integer>>], elem(x, 0)) ==
219+
{dynamic(),
220+
~l"""
221+
expected a tuple when accessing element at index 0 in expression:
222+
223+
elem(x, 0)
224+
225+
but got type:
226+
227+
integer()
228+
229+
where "x" was given the type:
230+
231+
# type: integer()
232+
# from: types_test.ex:LINE-2
233+
<<x::integer>>
234+
"""}
235+
end
236+
237+
test "accessing an out of range index" do
238+
assert typewarn!(elem({:ok, 123}, 2)) ==
239+
{dynamic(),
240+
~l"""
241+
out of range index 2 in expression:
242+
243+
elem({:ok, 123}, 2)
244+
245+
the given type does not have the given index:
246+
247+
{:ok, integer()}
248+
"""}
207249
end
208250
end
209251

@@ -212,6 +254,7 @@ defmodule Module.Types.ExprTest do
212254
assert typecheck!(%{foo: :bar}) == closed_map(foo: atom([:bar]))
213255
assert typecheck!(%{123 => 456}) == open_map()
214256
assert typecheck!(%{123 => 456, foo: :bar}) == open_map(foo: atom([:bar]))
257+
assert typecheck!([x], %{key: x}) == dynamic(closed_map(key: term()))
215258

216259
assert typecheck!(
217260
(

lib/mix/test/test_helper.exs

+7-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,13 @@ defmodule MixTest.Case do
132132

133133
defmacro in_fixture(which, block) do
134134
module = inspect(__CALLER__.module)
135-
function = Atom.to_string(elem(__CALLER__.function, 0))
135+
136+
function =
137+
case __CALLER__.function do
138+
{name, _arity} -> Atom.to_string(name)
139+
nil -> raise "expected in_fixture/2 to be called from a function"
140+
end
141+
136142
tmp = Path.join(module, function)
137143

138144
quote do

0 commit comments

Comments
 (0)