diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 2e75955f2b..a7d6dde647 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.Of.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 393f887c65..b6e9722969 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -138,11 +138,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}} -> @@ -729,7 +737,7 @@ defmodule Module.Types.Descr do # Find recursive nodes without running their generators. defp contains_recur?({id, state, generator}) - when is_reference(id) and is_map(state) and is_function(generator, 1), + when (is_reference(id) or is_tuple(id)) and is_map(state) and is_function(generator, 1), do: true defp contains_recur?(term) when is_map(term), diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index c1671dcae6..b87adc14c2 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.Of.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/of.ex b/lib/elixir/lib/module/types/of.ex index 0ace2d16ed..ab20290a52 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -17,6 +17,39 @@ defmodule Module.Types.Of do @binary binary() @bitstring bitstring() + @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)) + ## Variables @doc """ diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 764aa27e89..d6ca25e57f 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -8,6 +8,7 @@ defmodule Module.Types.ExprTest do import TypeHelper import Module.Types.Descr + import Module.Types.Of, only: [chardata: 0] defmacro domain_key(arg) when is_atom(arg), do: [arg] defmacro generated_foo_call(x) do @@ -468,6 +469,18 @@ defmodule Module.Types.ExprTest do ]) ) == none() + assert typecheck!( + :erlang.raise(:error, :oops, [ + {__MODULE__, :example, 1, [file: [~c"src/", ["example" | ".ex"]]]} + ]) + ) == none() + + assert typeerror!( + :erlang.raise(:error, :oops, [ + {__MODULE__, :example, 1, [file: [~c"src/", [:invalid]]]} + ]) + ) =~ "incompatible types given to :erlang.raise/3" + assert typeerror!( :erlang.raise(:error, :oops, [ {__MODULE__, :example, 1, [line: :unknown]} @@ -3470,7 +3483,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 87ded5dadc..c846a111b8 100644 --- a/lib/elixir/test/elixir/module/types/recursive_test.exs +++ b/lib/elixir/test/elixir/module/types/recursive_test.exs @@ -7,6 +7,7 @@ defmodule Module.Types.RecursiveTest do use ExUnit.Case, async: true import Module.Types.Descr + import Module.Types.Of, only: [chardata: 0] setup do Process.flag(:max_heap_size, %{size: 25_000_000, kill: true, error_logger: false}) @@ -43,6 +44,34 @@ defmodule Module.Types.RecursiveTest do |> unfold() end + test "scoped nodes can be escaped and pretty printed" do + assert {{Module.Types.Of, :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()) + + expected = + """ + recur( + t1, + binary() or empty_list() or + non_empty_list( + recur( + t2, + binary() or empty_list() or integer() or non_empty_list(t2, binary() or empty_list()) + ), + binary() or empty_list() + ) + ) + """ + |> String.trim() + + assert to_quoted_string(chardata()) == expected + + assert to_quoted(tuple([atom([:file]), chardata()])) == + {:{}, [], [to_quoted(atom([:file])), to_quoted(chardata())]} + end + test "node infrastructure" do descr = integer() @@ -166,6 +195,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} =