diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 0581a5d9c7..3215a69cea 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -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()])) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index b9c042e461..32a3d87fcf 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -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} @@ -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}} -> @@ -651,7 +692,12 @@ 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 @@ -659,6 +705,9 @@ defmodule Module.Types.Descr do 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 diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 33e8c6b686..7c8870a609 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -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()])) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 20a94a39ca..aebe4a5e0f 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -3160,7 +3160,8 @@ defmodule Module.Types.ExprTest do _ -> "unknown" end end - ) == opt_union(list(integer()), binary()) + ) + |> equal?(chardata()) assert typecheck!( try do diff --git a/lib/elixir/test/elixir/module/types/recursive_test.exs b/lib/elixir/test/elixir/module/types/recursive_test.exs index 5914d12d70..cb494b46bc 100644 --- a/lib/elixir/test/elixir/module/types/recursive_test.exs +++ b/lib/elixir/test/elixir/module/types/recursive_test.exs @@ -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() @@ -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} =