Skip to content

Commit f445cb9

Browse files
committed
Add CHANGELOG for function application and improve pretty printing
1 parent 4b6fdb4 commit f445cb9

File tree

7 files changed

+144
-33
lines changed

7 files changed

+144
-33
lines changed

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,44 @@ but expected a type that implements the Enumerable protocol, it must be one of:
9494
) or fun() or list(term()) or non_struct_map()
9595
```
9696

97+
### Type checking and inference of anonymous functions
98+
99+
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
100+
101+
```elixir
102+
defmodule Example do
103+
def run do
104+
fun = fn %{} -> :map end
105+
fun.("hello")
106+
end
107+
end
108+
```
109+
110+
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
111+
112+
```
113+
warning: incompatible types given on function application:
114+
115+
fun.("hello")
116+
117+
given types:
118+
119+
binary()
120+
121+
but function has type:
122+
123+
(dynamic(map()) -> :map)
124+
125+
typing violation found at:
126+
127+
6 │ fun.("hello")
128+
│ ~
129+
130+
└─ mod.exs:6:8: Example.run/0
131+
```
132+
133+
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
134+
97135
## Faster compile times in large projects
98136

99137
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.

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

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -472,28 +472,33 @@ defmodule Module.Types.Apply do
472472
Returns the type of a remote capture.
473473
"""
474474
def remote_capture(modules, fun, arity, meta, stack, context) do
475-
if stack.mode == :traversal or modules == [] do
476-
{dynamic(fun(arity)), context}
477-
else
478-
{type, fallback?, context} =
479-
Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} ->
480-
case signature(module, fun, arity, meta, stack, context) do
481-
{{:strong, _, clauses}, context} ->
482-
{union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
475+
cond do
476+
stack.mode == :traversal ->
477+
{dynamic(), context}
483478

484-
{{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
485-
{union(type, fun_from_overlapping_clauses(clauses)), fallback?, context}
479+
modules == [] ->
480+
{dynamic(fun(arity)), context}
486481

487-
{_, context} ->
488-
{type, true, context}
489-
end
490-
end)
482+
true ->
483+
{type, fallback?, context} =
484+
Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} ->
485+
case signature(module, fun, arity, meta, stack, context) do
486+
{{:strong, _, clauses}, context} ->
487+
{union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
491488

492-
if fallback? do
493-
{dynamic(fun(arity)), context}
494-
else
495-
{type, context}
496-
end
489+
{{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
490+
{union(type, fun_from_overlapping_clauses(clauses)), fallback?, context}
491+
492+
{_, context} ->
493+
{type, true, context}
494+
end
495+
end)
496+
497+
if fallback? do
498+
{dynamic(fun(arity)), context}
499+
else
500+
{type, context}
501+
end
497502
end
498503
end
499504

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

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,15 @@ defmodule Module.Types.Descr do
111111
Creates the top function type for the given arity,
112112
where all arguments are none() and return is term().
113113
114+
This function cannot be applied to, unless it is made dynamic.
115+
114116
## Examples
115117
116118
fun(1)
117119
#=> (none -> term)
118120
119121
fun(2)
120-
#=> Creates (none, none) -> term
122+
#=> Creates (none, none -> term)
121123
"""
122124
def fun(arity) when is_integer(arity) and arity >= 0 do
123125
fun(List.duplicate(none(), arity), term())
@@ -1464,24 +1466,61 @@ defmodule Module.Types.Descr do
14641466
defp fun_intersection(bdd1, bdd2) do
14651467
case {bdd1, bdd2} do
14661468
# Base cases
1467-
{_, :fun_bottom} -> :fun_bottom
1468-
{:fun_bottom, _} -> :fun_bottom
1469-
{:fun_top, bdd} -> bdd
1470-
{bdd, :fun_top} -> bdd
1469+
{_, :fun_bottom} ->
1470+
:fun_bottom
1471+
1472+
{:fun_bottom, _} ->
1473+
:fun_bottom
1474+
1475+
{:fun_top, bdd} ->
1476+
bdd
1477+
1478+
{bdd, :fun_top} ->
1479+
bdd
1480+
14711481
# Optimizations
14721482
# If intersecting with a single positive or negative function, we insert
14731483
# it at the root instead of merging the trees (this avoids going down the
14741484
# whole bdd).
1475-
{bdd, {fun, :fun_top, :fun_bottom}} -> {fun, bdd, :fun_bottom}
1476-
{bdd, {fun, :fun_bottom, :fun_top}} -> {fun, :fun_bottom, bdd}
1477-
{{fun, :fun_top, :fun_bottom}, bdd} -> {fun, bdd, :fun_bottom}
1478-
{{fun, :fun_bottom, :fun_top}, bdd} -> {fun, :fun_bottom, bdd}
1485+
{bdd, {{args, return} = fun, :fun_top, :fun_bottom}} ->
1486+
# If intersecting with the top type for that arity, no-op
1487+
if return == :term and Enum.all?(args, &(&1 == %{})) and
1488+
matching_arity?(bdd, length(args)) do
1489+
bdd
1490+
else
1491+
{fun, bdd, :fun_bottom}
1492+
end
1493+
1494+
{bdd, {fun, :fun_bottom, :fun_top}} ->
1495+
{fun, :fun_bottom, bdd}
1496+
1497+
{{{args, return} = fun, :fun_top, :fun_bottom}, bdd} ->
1498+
# If intersecting with the top type for that arity, no-op
1499+
if return == :term and Enum.all?(args, &(&1 == %{})) and
1500+
matching_arity?(bdd, length(args)) do
1501+
bdd
1502+
else
1503+
{fun, bdd, :fun_bottom}
1504+
end
1505+
1506+
{{fun, :fun_bottom, :fun_top}, bdd} ->
1507+
{fun, :fun_bottom, bdd}
1508+
14791509
# General cases
1480-
{{fun, l1, r1}, {fun, l2, r2}} -> {fun, fun_intersection(l1, l2), fun_intersection(r1, r2)}
1481-
{{fun, l, r}, bdd} -> {fun, fun_intersection(l, bdd), fun_intersection(r, bdd)}
1510+
{{fun, l1, r1}, {fun, l2, r2}} ->
1511+
{fun, fun_intersection(l1, l2), fun_intersection(r1, r2)}
1512+
1513+
{{fun, l, r}, bdd} ->
1514+
{fun, fun_intersection(l, bdd), fun_intersection(r, bdd)}
14821515
end
14831516
end
14841517

1518+
defp matching_arity?({{args, _return}, l, r}, arity) do
1519+
length(args) == arity and matching_arity?(l, arity) and matching_arity?(r, arity)
1520+
end
1521+
1522+
defp matching_arity?(_, _arity), do: true
1523+
14851524
defp fun_difference(bdd1, bdd2) do
14861525
case {bdd1, bdd2} do
14871526
{:fun_bottom, _} -> :fun_bottom

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ defmodule Module.Types.Expr do
455455
end
456456

457457
def of_expr({{:., _, [fun]}, _, args} = call, _expected, _expr, stack, context) do
458-
{fun_type, context} = of_expr(fun, fun(length(args)), call, stack, context)
458+
{fun_type, context} = of_expr(fun, dynamic(fun(length(args))), call, stack, context)
459459

460460
# TODO: Perform inference based on the strong domain of a function
461461
{args_types, context} =

lib/elixir/pages/references/gradual-set-theoretic-types.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ This function also has the type `(boolean() -> boolean())`, because it receives
101101

102102
At this point, you may ask, why not a union? As a real-world example, take a t-shirt with green and yellow stripes. We can say the t-shirt belongs to the set of "t-shirts with green color". We can also say the t-shirt belongs to the set of "t-shirts with yellow color". Let's see the difference between unions and intersections:
103103

104-
* `(t_shirts_with_green() or t_shirts_with_yellow())` - contains t-shirts with either green or yellow, such as green, green and red, green and yellow, yellow, yellow and red, etc.
104+
* `(t_shirts_with_green() or t_shirts_with_yellow())` - contains t-shirts with either green or yellow, such as green, green and red, green and yellow, but also only yellow, yellow and red, etc.
105105

106-
* `(t_shirts_with_green() and t_shirts_with_yellow())` - contains t-shirts with both green and yellow (and also other colors)
106+
* `(t_shirts_with_green() and t_shirts_with_yellow())` - contains t-shirts with both green and yellow (and maybe other colors)
107107

108108
Since the t-shirt has both colors, we say it belongs to the intersection of both sets. The same way that a function that goes from `(integer() -> integer())` and `(boolean() -> boolean())` is also an intersection. In practice, it does not make sense to define the union of two functions in Elixir, so the compiler will always point to the right direction.
109109

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,13 @@ defmodule Module.Types.DescrTest do
18121812
assert fun() |> to_quoted_string() == "fun()"
18131813
assert none_fun(1) |> to_quoted_string() == "(none() -> term())"
18141814

1815+
assert none_fun(1)
1816+
|> intersection(none_fun(2))
1817+
|> to_quoted_string() == "none()"
1818+
1819+
assert fun([integer()], atom()) |> intersection(none_fun(1)) |> to_quoted_string() ==
1820+
"(integer() -> atom())"
1821+
18151822
assert fun([integer(), float()], boolean()) |> to_quoted_string() ==
18161823
"(integer(), float() -> boolean())"
18171824

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,28 @@ defmodule Module.Types.ExprTest do
234234
to said function are types that satisfy all sides of the union (which may be none)
235235
"""
236236
end
237+
238+
test "bad arguments from inferred type" do
239+
assert typeerror!(
240+
(
241+
fun = fn %{} -> :map end
242+
fun.(:error)
243+
)
244+
)
245+
|> strip_ansi() == """
246+
incompatible types given on function application:
247+
248+
fun.(:error)
249+
250+
given types:
251+
252+
:error
253+
254+
but function has type:
255+
256+
(dynamic(map()) -> :map)
257+
"""
258+
end
237259
end
238260

239261
describe "remotes" do

0 commit comments

Comments
 (0)