Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/elixir/lib/module/types/apply.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,12 @@ defmodule Module.Types.Apply do
args_or_none = opt_union(list(term()), atom([:none]))
custom_info_key = opt_difference(atom(), atom([:file, :line, :error_info]))

# OTP declares the file entry in stacktrace_extrainfo() as unicode:chardata()
chardata = Module.Types.Descr.chardata()

extra_info =
list(
tuple([atom([:file]), opt_union(list(integer()), binary())])
tuple([atom([:file]), chardata])
|> opt_union(tuple([atom([:line]), integer()]))
|> opt_union(tuple([atom([:error_info]), open_map()]))
|> opt_union(tuple([custom_info_key, term()]))
Expand Down
55 changes: 52 additions & 3 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,39 @@ defmodule Module.Types.Descr do
@boolset :sets.from_list([true, false], version: 2)
def boolean(), do: %{atom: {:union, @boolset}}

@doc """
The recursive `unicode:chardata()` type, as declared by Erlang/OTP:

chardata() = charlist() | unicode_binary()
charlist() = maybe_improper_list(char() | unicode_binary() | charlist(),
unicode_binary() | [])

"""
def chardata() do
%{chardata: node} =
recursive(
%{
chardata: &__MODULE__.chardata_equation/1,
chardata_head: &__MODULE__.chardata_head_equation/1
},
__MODULE__
)

node
end

@doc false
def chardata_equation(recur) do
binary()
|> bare_union(empty_list())
|> bare_union(non_empty_list(recur.(:chardata_head), bare_union(binary(), empty_list())))
end

# The head has its own equation because a node cannot be unfolded from
# within its own generator, and unions unfold their operands.
@doc false
def chardata_head_equation(recur), do: bare_union(integer(), recur.(:chardata))

## Nodes

defp make_node(id, state, generator), do: {id, state, generator}
Expand All @@ -138,11 +171,19 @@ defmodule Module.Types.Descr do
Builds recursive type nodes from mutually recursive equations.

Generators receive `recur`, which returns the node for a named equation.

Each equation is identified by a unique reference, which means the
resulting nodes cannot be escaped into compiled code. Give a `scope`
to identify the equations by `{scope, name}` instead. In such cases,
the scope must be unique across all recursive types it may meet and
the generators must be remote captures, so the nodes are made of
literals that `Macro.escape/1` supports.
"""
def recursive(equations) when is_map(equations) do
def recursive(equations, scope \\ nil) when is_map(equations) do
state =
Map.new(equations, fn {name, generator} ->
{name, {make_ref(), generator}}
id = if scope == nil, do: make_ref(), else: {scope, name}
{name, {id, generator}}
end)

Map.new(state, fn {name, {id, generator}} ->
Expand Down Expand Up @@ -651,14 +692,22 @@ defmodule Module.Types.Descr do
* `:collapse_structs` - do not show struct fields that match
their default type
"""
def to_quoted(descr, opts \\ []) do
def to_quoted(descr, opts \\ [])

# Recursive types are printed by name, as unfolding them would not terminate.
def to_quoted({id, _state, _generator}, _opts), do: {recursive_name(id), [], []}

def to_quoted(descr, opts) do
if term_type?(descr) do
{:term, [], []}
else
non_term_type_to_quoted(descr, opts)
end
end

defp recursive_name({_scope, name}) when is_atom(name), do: name
defp recursive_name(_id), do: :recursive

defp non_term_type_to_quoted(descr, opts) do
{dynamic, static, extra} =
case :maps.take(:dynamic, descr) do
Expand Down
5 changes: 4 additions & 1 deletion lib/elixir/lib/module/types/expr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ defmodule Module.Types.Expr do

custom_info_key = opt_difference(atom(), atom([:file, :line, :error_info]))

# OTP declares the file entry in stacktrace_extrainfo() as unicode:chardata()
chardata = Module.Types.Descr.chardata()

extra_info =
list(
tuple([atom([:file]), opt_union(list(integer()), binary())])
tuple([atom([:file]), chardata])
|> opt_union(tuple([atom([:line]), integer()]))
|> opt_union(tuple([atom([:error_info]), open_map()]))
|> opt_union(tuple([custom_info_key, term()]))
Expand Down
3 changes: 2 additions & 1 deletion lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3160,7 +3160,8 @@ defmodule Module.Types.ExprTest do
_ -> "unknown"
end
end
) == opt_union(list(integer()), binary())
)
|> equal?(chardata())

assert typecheck!(
try do
Expand Down
25 changes: 25 additions & 0 deletions lib/elixir/test/elixir/module/types/recursive_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ defmodule Module.Types.RecursiveTest do
|> unfold()
end

test "scoped nodes can be escaped and are printed by name" do
assert {{Module.Types.Descr, :chardata}, _state, _generator} = chardata()

# Scoped nodes are made of literals, so they can be embedded in compiled code
assert Macro.escape(chardata()) |> Code.eval_quoted() |> elem(0) |> equal?(chardata())

assert to_quoted_string(chardata()) == "chardata()"
assert to_quoted_string(tuple([atom([:file]), chardata()])) == "{:file, chardata()}"
end

test "node infrastructure" do
descr = integer()

Expand Down Expand Up @@ -166,6 +176,21 @@ defmodule Module.Types.RecursiveTest do
assert subtype?(non_empty_list(binary(), binary()), chardata)
refute subtype?(pid(), chardata)

## The chardata/0 constructor is the real unicode:chardata()
assert subtype?(binary(), chardata())
assert subtype?(empty_list(), chardata())
assert subtype?(list(integer()), chardata())
assert subtype?(list(binary()), chardata())
assert subtype?(non_empty_list(integer(), binary()), chardata())
# nested chardata, which the equation above does not admit
assert subtype?(list(list(integer())), chardata())
assert subtype?(list(list(list(binary()))), chardata())
assert subtype?(list(non_empty_list(integer(), binary())), chardata())
refute subtype?(list(atom()), chardata())
refute subtype?(list(float()), chardata())
refute subtype?(non_empty_list(integer(), atom()), chardata())
refute subtype?(atom(), chardata())

## expression trees
# Expr = integer() | {atom, Expr, Expr}, Binop = {atom, Expr, Expr}
%{Expr: nexpr_node, Binop: nbinop_node} =
Expand Down