Skip to content

Commit 99b6e43

Browse files
committed
Add tests for function to quoted
1 parent df007f7 commit 99b6e43

File tree

4 files changed

+85
-58
lines changed

4 files changed

+85
-58
lines changed

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

Lines changed: 66 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -93,23 +93,42 @@ defmodule Module.Types.Descr do
9393
Creates a function type with the given arguments and return type.
9494
9595
## Examples
96-
iex> fun([integer()], atom()) # Creates (integer) -> atom
97-
iex> fun([integer(), float()], boolean()) # Creates (integer, float) -> boolean
96+
97+
fun([integer()], atom())
98+
#=> (integer -> atom)
99+
100+
fun([integer(), float()], boolean())
101+
#=> (integer, float -> boolean)
98102
"""
99103
def fun(args, return) when is_list(args), do: fun_descr(args, return)
100104

101105
@doc """
102-
Creates the top function type for the given arity, where all arguments are none()
103-
and return is term().
106+
Creates the top function type for the given arity,
107+
where all arguments are none() and return is term().
104108
105109
## Examples
106-
iex> fun(1) # Creates (none) -> term
107-
iex> fun(2) # Creates (none, none) -> term
110+
111+
fun(1)
112+
#=> (none -> term)
113+
114+
fun(2)
115+
#=> Creates (none, none) -> term
108116
"""
109117
def fun(arity) when is_integer(arity) and arity >= 0 do
110118
fun(List.duplicate(none(), arity), term())
111119
end
112120

121+
@doc """
122+
Tuples represent function domains, using unions to combine parameters.
123+
124+
Example: for functions (integer, float ->:ok) and (float, integer -> :error)
125+
domain isn't {integer|float,integer|float} as that would incorrectly accept {float,float}
126+
Instead, it is {integer,float} or {float,integer}
127+
128+
Made public for testing.
129+
"""
130+
def domain_descr(types) when is_list(types), do: tuple(types)
131+
113132
## Optional
114133

115134
# `not_set()` is a special base type that represents an not_set field in a map.
@@ -704,11 +723,11 @@ defmodule Module.Types.Descr do
704723
if not empty?(descr) and fun_only?(descr, arity), do: :ok, else: :error
705724

706725
{dynamic, static} ->
707-
empty_static = empty?(static)
726+
empty_static? = empty?(static)
708727

709728
cond do
710-
not empty_static -> if fun_only?(static, arity), do: :ok, else: :error
711-
empty_static and not empty?(intersection(dynamic, fun(arity))) -> :ok
729+
not empty_static? -> if fun_only?(static, arity), do: :ok, else: :error
730+
empty_static? and not empty?(intersection(dynamic, fun(arity))) -> :ok
712731
true -> :error
713732
end
714733
end
@@ -880,47 +899,46 @@ defmodule Module.Types.Descr do
880899

881900
# Function types are represented using Binary Decision Diagrams (BDDs) for efficient
882901
# handling of unions, intersections, and negations.
902+
#
903+
# Currently, they only represent weak types. It is yet to be decided how all of the
904+
# types will be encoded in the descr.
883905

884906
### Key concepts:
885907

886-
# * BDD structure: A tree with function nodes and :fun_top/:fun_bottom leaves. Paths to :fun_top
887-
# represent valid function types. Nodes are positive when following a left
888-
# branch (e.g. (int, float) -> bool) and negative otherwise.
908+
# * BDD structure: A tree with function nodes and :fun_top/:fun_bottom leaves.
909+
# Paths to :fun_top represent valid function types. Nodes are positive when
910+
# following a left branch (e.g. (int, float -> bool) and negative otherwise.
889911

890912
# * Function variance:
891913
# - Contravariance in arguments: If s <: t, then (t → r) <: (s → r)
892914
# - Covariance in returns: If s <: t, then (u → s) <: (u → t)
893915

894916
# * Representation:
895917
# - fun(): Top function type (leaf 1)
896-
# - Function literals: {tag, [t1, ..., tn], t} where [t1, ..., tn] are argument types and t is return type
897-
# tag is either `:weak` or `:strong`
898-
# TODO: implement `:strong`
918+
# - Function literals: {[t1, ..., tn], t} where [t1, ..., tn] are argument types and t is return type
899919
# - Normalized form for function applications: {domain, arrows, arity} is produced by `fun_normalize/1`
900920

901921
# * Examples:
902922
# - fun([integer()], atom()): A function from integer to atom
903923
# - intersection(fun([integer()], atom()), fun([float()], boolean())): A function handling both signatures
904924

905-
# Note: Function domains are expressed as tuple types. We use separate representations rather than
906-
# unary functions with tuple domains to handle special cases like representing functions of a
907-
# specific arity (e.g., (none,none->term) for arity 2).
908-
925+
# Note: Function domains are expressed as tuple types. We use separate representations
926+
# rather than unary functions with tuple domains to handle special cases like representing
927+
# functions of a specific arity (e.g., (none,none->term) for arity 2).
909928
defp fun_new(inputs, output), do: {{inputs, output}, :fun_top, :fun_bottom}
910929

911-
@doc """
912-
Creates a function type from a list of inputs and an output where the inputs and/or output may be dynamic.
913-
914-
For function (t → s) with dynamic components:
915-
- Static part: (upper_bound(t) → lower_bound(s))
916-
- Dynamic part: dynamic(lower_bound(t) → upper_bound(s))
917-
918-
When handling dynamic types:
919-
- `upper_bound(t)` extracts the upper bound (most general type) of a gradual type.
920-
For `dynamic(integer())`, it is `integer()`.
921-
- `lower_bound(t)` extracts the lower bound (most specific type) of a gradual type.
922-
"""
923-
def fun_descr(args, output) when is_list(args) do
930+
# Creates a function type from a list of inputs and an output
931+
# where the inputs and/or output may be dynamic.
932+
#
933+
# For function (t → s) with dynamic components:
934+
# - Static part: (upper_bound(t) → lower_bound(s))
935+
# - Dynamic part: dynamic(lower_bound(t) → upper_bound(s))
936+
#
937+
# When handling dynamic types:
938+
# - `upper_bound(t)` extracts the upper bound (most general type) of a gradual type.
939+
# For `dynamic(integer())`, it is `integer()`.
940+
# - `lower_bound(t)` extracts the lower bound (most specific type) of a gradual type.
941+
defp fun_descr(args, output) when is_list(args) do
924942
dynamic_arguments? = are_arguments_dynamic?(args)
925943
dynamic_output? = match?(%{dynamic: _}, output)
926944

@@ -949,16 +967,11 @@ defmodule Module.Types.Descr do
949967
defp lower_bound(:term), do: :term
950968
defp lower_bound(type), do: Map.delete(type, :dynamic)
951969

952-
# Tuples represent function domains, using unions to combine parameters.
953-
# Example: for functions (integer,float)->:ok and (float,integer)->:error
954-
# domain isn't {integer|float,integer|float} as that would incorrectly accept {float,float}
955-
# Instead, it is {integer,float} or {float,integer}
956-
def domain_descr(types) when is_list(types), do: tuple(types)
957-
958970
@doc """
959971
Calculates the domain of a function type.
960972
961973
For a function type, the domain is the set of valid input types.
974+
962975
Returns:
963976
- `:badfunction` if the type is not a function type
964977
- A tuple type representing the domain for valid function types
@@ -1021,7 +1034,7 @@ defmodule Module.Types.Descr do
10211034

10221035
defp fun_domain_static(:term), do: :badfunction
10231036
defp fun_domain_static(%{}), do: {:ok, none()}
1024-
defp fun_domain_static(:emptyfunction), do: {:ok, none()}
1037+
defp fun_domain_static(:empty_function), do: {:ok, none()}
10251038

10261039
@doc """
10271040
Applies a function type to a list of argument types.
@@ -1205,7 +1218,7 @@ defmodule Module.Types.Descr do
12051218
## Return Values
12061219
#
12071220
# - `{domain, arrows, arity}` for valid function BDDs
1208-
# - `:emptyfunction` if the BDD represents an empty function type
1221+
# - `:empty_function` if the BDD represents an empty function type
12091222
#
12101223
# ## Internal Use
12111224
#
@@ -1232,7 +1245,7 @@ defmodule Module.Types.Descr do
12321245
end
12331246
end)
12341247

1235-
if arrows == [], do: :emptyfunction, else: {domain, arrows, arity}
1248+
if arrows == [], do: :empty_function, else: {domain, arrows, arity}
12361249
end
12371250

12381251
# Checks if a function type is empty.
@@ -1262,12 +1275,12 @@ defmodule Module.Types.Descr do
12621275
# 2. There exists a negative function that negates the whole positive intersection
12631276

12641277
## Examples
1278+
#
12651279
# - `{[fun(1)], []}` is not empty
12661280
# - `{[fun(1), fun(2)], []}` is empty (different arities)
12671281
# - `{[fun(integer() -> atom())], [fun(none() -> term())]}` is empty
12681282
# - `{[], _}` (representing the top function type fun()) is never empty
12691283
#
1270-
# TODO: test performance
12711284
defp fun_empty?([], _), do: false
12721285

12731286
defp fun_empty?(positives, negatives) do
@@ -1278,9 +1291,12 @@ defmodule Module.Types.Descr do
12781291

12791292
{positive_arity, positive_domain} ->
12801293
# Check if any negative function negates the whole positive intersection
1281-
# e.g. (integer()->atom()) is negated by
1282-
# i) (none()->term()) ii) (none()->atom())
1283-
# ii) (integer()->term()) iv) (integer()->atom())
1294+
# e.g. (integer() -> atom()) is negated by:
1295+
#
1296+
# * (none() -> term())
1297+
# * (none() -> atom())
1298+
# * (integer() -> term())
1299+
# * (integer() -> atom())
12841300
Enum.any?(negatives, fn {neg_arguments, neg_return} ->
12851301
# Filter positives to only those with matching arity, then check if the negative
12861302
# function's domain is a supertype of the positive domain and if the phi function
@@ -1311,7 +1327,7 @@ defmodule Module.Types.Descr do
13111327

13121328
# Implements the Φ (phi) function for determining function subtyping relationships.
13131329
#
1314-
## Algorithm
1330+
# ## Algorithm
13151331
#
13161332
# For inputs t₁...tₙ, booleans b₁...bₙ, negated return type t, and set of arrow types P:
13171333
#
@@ -1324,7 +1340,6 @@ defmodule Module.Types.Descr do
13241340
# Returns true if the intersection of the positives is a subtype of (t1,...,tn)->(not t).
13251341
#
13261342
# See [Castagna and Lanvin (2024)](https://arxiv.org/abs/2408.14345), Theorem 4.2.
1327-
13281343
defp phi_starter(arguments, return, positives) do
13291344
n = length(arguments)
13301345
# Arity mismatch: if there is one positive function with a different arity,
@@ -1399,27 +1414,25 @@ defmodule Module.Types.Descr do
13991414
defp fun_to_quoted(:fun, _opts), do: [{:fun, [], []}]
14001415

14011416
defp fun_to_quoted(bdd, opts) do
1402-
arrows = bdd |> fun_get()
1417+
arrows = fun_get(bdd)
14031418

1404-
for {positives, negatives} <- arrows,
1405-
not fun_empty?(positives, negatives) do
1419+
for {positives, negatives} <- arrows, not fun_empty?(positives, negatives) do
14061420
fun_intersection_to_quoted(positives, opts)
14071421
end
14081422
|> case do
14091423
[] -> []
1410-
[single] -> [single]
14111424
multiple -> [Enum.reduce(multiple, &{:or, [], [&2, &1]})]
14121425
end
14131426
end
14141427

14151428
defp fun_intersection_to_quoted(intersection, opts) do
14161429
intersection
14171430
|> Enum.map(fn {args, ret} ->
1418-
{:->, [], [[to_quoted(tuple_descr(:closed, args), opts)], to_quoted(ret, opts)]}
1431+
{:__block__, [],
1432+
[[{:->, [], [Enum.map(args, &to_quoted(&1, opts)), to_quoted(ret, opts)]}]]}
14191433
end)
14201434
|> case do
14211435
[] -> {:fun, [], []}
1422-
[single] -> single
14231436
multiple -> Enum.reduce(multiple, &{:and, [], [&2, &1]})
14241437
end
14251438
end

lib/elixir/test/elixir/kernel/raise_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ defmodule Kernel.RaiseTest do
400400

401401
result =
402402
try do
403-
fun.(1, 2)
403+
Process.get(:unused, fun).(1, 2)
404404
rescue
405405
x in [BadArityError] -> Exception.message(x)
406406
end

lib/elixir/test/elixir/kernel_test.exs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,10 +465,6 @@ defmodule KernelTest do
465465

466466
test "then/2" do
467467
assert 1 |> then(fn x -> x * 2 end) == 2
468-
469-
assert_raise BadArityError, fn ->
470-
1 |> then(fn x, y -> x * y end)
471-
end
472468
end
473469

474470
test "if/2 boolean optimization does not leak variables during expansion" do

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,24 @@ defmodule Module.Types.DescrTest do
16721672
"""
16731673
end
16741674

1675+
test "function" do
1676+
assert fun() |> to_quoted_string() == "fun()"
1677+
assert fun(1) |> to_quoted_string() == "(none() -> term())"
1678+
1679+
assert fun([integer(), float()], boolean()) |> to_quoted_string() ==
1680+
"(integer(), float() -> boolean())"
1681+
1682+
assert fun([integer()], boolean())
1683+
|> union(fun([float()], boolean()))
1684+
|> to_quoted_string() ==
1685+
"(float() -> boolean()) or (integer() -> boolean())"
1686+
1687+
assert fun([integer()], boolean())
1688+
|> intersection(fun([float()], boolean()))
1689+
|> to_quoted_string() ==
1690+
"(integer() -> boolean()) and (float() -> boolean())"
1691+
end
1692+
16751693
test "map" do
16761694
assert empty_map() |> to_quoted_string() == "empty_map()"
16771695
assert open_map() |> to_quoted_string() == "map()"

0 commit comments

Comments
 (0)