diff --git a/lib/elixir/lib/module/behaviour.ex b/lib/elixir/lib/module/behaviour.ex index 3601291ef69..950c47a1640 100644 --- a/lib/elixir/lib/module/behaviour.ex +++ b/lib/elixir/lib/module/behaviour.ex @@ -33,9 +33,7 @@ defmodule Module.Behaviour do module: module, file: file, line: line, - # Map containing the callbacks to be implemented callbacks: %{}, - # list of warnings {message, env} warnings: [] } end diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index a355f29fcd5..8d3a52d7220 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -52,12 +52,9 @@ defmodule Module.Types do end defp warnings_from_clause(meta, args, guards, body, stack, context) do - with {:ok, _types, context} <- Pattern.of_head(args, guards, meta, stack, context), - {:ok, _type, context} <- Expr.of_expr(body, stack, context) do - context.warnings - else - {:error, context} -> context.warnings - end + {_types, context} = Pattern.of_head(args, guards, meta, stack, context) + {_type, context} = Expr.of_expr(body, stack, context) + context.warnings end @doc false @@ -83,10 +80,12 @@ defmodule Module.Types do %{ # A list of all warnings found so far warnings: [], - # Information about all vars and their types + # All vars and their types vars: %{}, - # Information about variables and arguments from patterns - pattern_info: nil + # Variables and arguments from patterns + pattern_info: nil, + # If type checking has found an error/failure + failed: false } end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 8dfacd81e76..663333ba1ea 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -33,89 +33,78 @@ defmodule Module.Types.Expr do # :atom def of_expr(atom, _stack, context) when is_atom(atom), - do: {:ok, atom([atom]), context} + do: {atom([atom]), context} # 12 def of_expr(literal, _stack, context) when is_integer(literal), - do: {:ok, integer(), context} + do: {integer(), context} # 1.2 def of_expr(literal, _stack, context) when is_float(literal), - do: {:ok, float(), context} + do: {float(), context} # "..." def of_expr(literal, _stack, context) when is_binary(literal), - do: {:ok, binary(), context} + do: {binary(), context} # #PID<...> def of_expr(literal, _stack, context) when is_pid(literal), - do: {:ok, pid(), context} + do: {pid(), context} # [] def of_expr([], _stack, context), - do: {:ok, empty_list(), context} + do: {empty_list(), context} # [expr, ...] def of_expr(list, stack, context) when is_list(list) do {prefix, suffix} = unpack_list(list, []) - - with {:ok, prefix, context} <- - map_reduce_ok(prefix, context, &of_expr(&1, stack, &2)), - {:ok, suffix, context} <- of_expr(suffix, stack, context) do - {:ok, non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} - end + {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, stack, &2)) + {suffix, context} = of_expr(suffix, stack, context) + {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} end # {left, right} def of_expr({left, right}, stack, context) do - with {:ok, left, context} <- of_expr(left, stack, context), - {:ok, right, context} <- of_expr(right, stack, context) do - {:ok, tuple([left, right]), context} - end + {left, context} = of_expr(left, stack, context) + {right, context} = of_expr(right, stack, context) + {tuple([left, right]), context} end # <<...>>> def of_expr({:<<>>, _meta, args}, stack, context) do - case Of.binary(args, :expr, stack, context) do - {:ok, context} -> {:ok, binary(), context} - # It is safe to discard errors from binaries, we can continue typechecking - {:error, context} -> {:ok, binary(), context} - end + context = Of.binary(args, :expr, stack, context) + {binary(), context} end - def of_expr({:__CALLER__, _meta, var_context}, _stack, context) - when is_atom(var_context) do - {:ok, @caller, context} + def of_expr({:__CALLER__, _meta, var_context}, _stack, context) when is_atom(var_context) do + {@caller, context} end # TODO: __STACKTRACE__ def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context) when is_atom(var_context) do - {:ok, list(term()), context} + {list(term()), context} end # {...} def of_expr({:{}, _meta, exprs}, stack, context) do - with {:ok, types, context} <- map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do - {:ok, tuple(types), context} - end + {types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, stack, &2)) + {tuple(types), context} end # left = right def of_expr({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do - with {:ok, right_type, context} <- of_expr(right_expr, stack, context) do - Pattern.of_match(left_expr, right_type, expr, stack, context) - end + {right_type, context} = of_expr(right_expr, stack, context) + Pattern.of_match(left_expr, right_type, expr, stack, context) end # %{map | ...} def of_expr({:%{}, _, [{:|, _, [map, args]}]}, stack, context) do - with {:ok, _args_type, context} <- Of.closed_map(args, stack, context, &of_expr/3), - {:ok, _map_type, context} <- of_expr(map, stack, context) do - # TODO: intersect map with keys of terms for args - # TODO: Merge args_type into map_type with dynamic/static key requirement - {:ok, dynamic(open_map()), context} - end + {_args_type, context} = Of.closed_map(args, stack, context, &of_expr/3) + {_map_type, context} = of_expr(map, stack, context) + # TODO: intersect map with keys of terms for args + # TODO: Merge args_type into map_type with dynamic/static key requirement + {dynamic(open_map()), context} end # %Struct{map | ...} @@ -124,23 +113,24 @@ defmodule Module.Types.Expr do stack, context ) do - with {:ok, args_types, context} <- - map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) -> - with {:ok, type, context} <- of_expr(value, stack, context) do - {:ok, {key, type}, context} - end - end), - # TODO: args_types could be an empty list - {:ok, struct_type, context} <- - Of.struct(module, args_types, :only_defaults, struct_meta, stack, context), - {:ok, map_type, context} <- of_expr(map, stack, context) do - if disjoint?(struct_type, map_type) do - warning = {:badupdate, :struct, expr, struct_type, map_type, context} - {:ok, error_type(), warn(__MODULE__, warning, update_meta, stack, context)} - else - # TODO: Merge args_type into map_type with dynamic/static key requirement - Of.struct(module, args_types, :merge_defaults, struct_meta, stack, context) - end + {args_types, context} = + Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> + {type, context} = of_expr(value, stack, context) + {{key, type}, context} + end) + + # TODO: args_types could be an empty list + {struct_type, context} = + Of.struct(module, args_types, :only_defaults, struct_meta, stack, context) + + {map_type, context} = of_expr(map, stack, context) + + if disjoint?(struct_type, map_type) do + warning = {:badupdate, :struct, expr, struct_type, map_type, context} + {error_type(), error(__MODULE__, warning, update_meta, stack, context)} + else + # TODO: Merge args_type into map_type with dynamic/static key requirement + Of.struct(module, args_types, :merge_defaults, struct_meta, stack, context) end end @@ -157,52 +147,45 @@ defmodule Module.Types.Expr do # () def of_expr({:__block__, _meta, []}, _stack, context) do - {:ok, atom([nil]), context} + {atom([nil]), context} end # (expr; expr) def of_expr({:__block__, _meta, exprs}, stack, context) do {pre, [post]} = Enum.split(exprs, -1) - result = - map_reduce_ok(pre, context, fn expr, context -> - of_expr(expr, stack, context) + context = + Enum.reduce(pre, context, fn expr, context -> + {_, context} = of_expr(expr, stack, context) + context end) - case result do - {:ok, _, context} -> of_expr(post, stack, context) - {:error, context} -> {:error, context} - end + of_expr(post, stack, context) end # TODO: cond do pat -> expr end def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do - {result, context} = - reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context -> - with {:ok, _, context} <- of_expr(head, stack, context), - {:ok, _, context} <- of_expr(body, stack, context), - do: {:ok, context} + context = + Enum.reduce(clauses, context, fn {:->, _meta, [head, body]}, context -> + {_, context} = of_expr(head, stack, context) + {_, context} = of_expr(body, stack, context) + context end) - case result do - :ok -> {:ok, dynamic(), context} - :error -> {:error, context} - end + {dynamic(), context} end # TODO: case expr do pat -> expr end def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do - with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context), - {:ok, context} <- of_clauses(clauses, stack, context), - do: {:ok, dynamic(), context} + {_expr_type, context} = of_expr(case_expr, stack, context) + context = of_clauses(clauses, stack, context) + {dynamic(), context} end # TODO: fn pat -> expr end def of_expr({:fn, _meta, clauses}, stack, context) do - case of_clauses(clauses, stack, context) do - {:ok, context} -> {:ok, fun(), context} - {:error, context} -> {:error, context} - end + context = of_clauses(clauses, stack, context) + {fun(), context} end @try_blocks [:do, :after] @@ -210,10 +193,10 @@ defmodule Module.Types.Expr do # TODO: try do expr end def of_expr({:try, _meta, [blocks]}, stack, context) do - {result, context} = - reduce_ok(blocks, context, fn + context = + Enum.reduce(blocks, context, fn {:rescue, clauses}, context -> - reduce_ok(clauses, context, fn + Enum.reduce(clauses, context, fn {:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, context -> of_rescue(var, exceptions, body, expr, [], meta, stack, context) @@ -228,94 +211,80 @@ defmodule Module.Types.Expr do of_clauses(clauses, stack, context) end) - case result do - :ok -> {:ok, dynamic(), context} - :error -> {:error, context} - end + {dynamic(), context} end # TODO: receive do pat -> expr end def of_expr({:receive, _meta, [blocks]}, stack, context) do - {result, context} = - reduce_ok(blocks, context, fn + context = + Enum.reduce(blocks, context, fn {:do, {:__block__, _, []}}, context -> - {:ok, context} + context {:do, clauses}, context -> of_clauses(clauses, stack, context) {:after, [{:->, _meta, [head, body]}]}, context -> - with {:ok, _type, context} <- of_expr(head, stack, context), - {:ok, _type, context} <- of_expr(body, stack, context), - do: {:ok, context} + {_type, context} = of_expr(head, stack, context) + {_type, context} = of_expr(body, stack, context) + context end) - case result do - :ok -> {:ok, dynamic(), context} - :error -> {:error, context} - end + {dynamic(), context} end # TODO: for pat <- expr do expr end def of_expr({:for, _meta, [_ | _] = args}, stack, context) do {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) - - with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)), - {:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do - if Keyword.has_key?(opts, :reduce) do - with {:ok, context} <- of_clauses(block, stack, context) do - {:ok, dynamic(), context} - end - else - with {:ok, _type, context} <- of_expr(block, stack, context) do - {:ok, dynamic(), context} - end - end + context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2)) + context = Enum.reduce(opts, context, &for_option(&1, stack, &2)) + + if Keyword.has_key?(opts, :reduce) do + context = of_clauses(block, stack, context) + {dynamic(), context} + else + {_type, context} = of_expr(block, stack, context) + {dynamic(), context} end end # TODO: with pat <- expr do expr end def of_expr({:with, _meta, [_ | _] = clauses}, stack, context) do - case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do - {:ok, context} -> {:ok, dynamic(), context} - {:error, context} -> {:error, context} - end + {clauses, [options]} = Enum.split(clauses, -1) + context = Enum.reduce(clauses, context, &with_clause(&1, stack, &2)) + context = Enum.reduce(options, context, &with_option(&1, stack, &2)) + {dynamic(), context} end # TODO: fun.(args) def of_expr({{:., _meta1, [fun]}, _meta2, args}, stack, context) do - with {:ok, fun_type, context} <- of_expr(fun, stack, context), - {:ok, _args_types, context} <- - map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do - context = - case fun_fetch(fun_type, length(args)) do - :ok -> context - :error -> Of.incompatible_warn(fun, fun(), fun_type, stack, context) - end + {fun_type, context} = of_expr(fun, stack, context) + {_args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) - {:ok, dynamic(), context} + case fun_fetch(fun_type, length(args)) do + :ok -> {dynamic(), context} + :error -> {dynamic(), Of.incompatible_error(fun, fun(), fun_type, stack, context)} end end def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = expr, stack, context) when not is_atom(callee) and is_atom(key_or_fun) do - with {:ok, type, context} <- of_expr(callee, stack, context) do - if Keyword.get(meta, :no_parens, false) do - Of.map_fetch(expr, type, key_or_fun, stack, context) - else - {mods, context} = Of.remote(type, key_or_fun, 0, [:dot], expr, meta, stack, context) - apply_many(mods, key_or_fun, [], expr, stack, context) - end + {type, context} = of_expr(callee, stack, context) + + if Keyword.get(meta, :no_parens, false) do + Of.map_fetch(expr, type, key_or_fun, stack, context) + else + {mods, context} = Of.remote(type, key_or_fun, 0, [:dot], expr, meta, stack, context) + apply_many(mods, key_or_fun, [], expr, stack, context) end end # TODO: expr.fun(arg) def of_expr({{:., _, [remote, name]}, meta, args} = expr, stack, context) do - with {:ok, remote_type, context} <- of_expr(remote, stack, context), - {:ok, args_types, context} <- map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do - {mods, context} = Of.remote(remote_type, name, length(args), expr, meta, stack, context) - apply_many(mods, name, args_types, expr, stack, context) - end + {remote_type, context} = of_expr(remote, stack, context) + {args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) + {mods, context} = Of.remote(remote_type, name, length(args), expr, meta, stack, context) + apply_many(mods, name, args_types, expr, stack, context) end # TODO: &Foo.bar/1 @@ -325,32 +294,29 @@ defmodule Module.Types.Expr do context ) when is_atom(name) and is_integer(arity) do - with {:ok, remote_type, context} <- of_expr(remote, stack, context) do - # TODO: We cannot return the unions of functions. Do we forbid this? - # Do we check it is always the same return type? Do we simply say it is a function? - {_mods, context} = Of.remote(remote_type, name, arity, expr, meta, stack, context) - {:ok, fun(), context} - end + {remote_type, context} = of_expr(remote, stack, context) + # TODO: We cannot return the unions of functions. Do we forbid this? + # Do we check it is always the same return type? Do we simply say it is a function? + {_mods, context} = Of.remote(remote_type, name, arity, expr, meta, stack, context) + {fun(), context} end # &foo/1 # TODO: & &1 def of_expr({:&, _meta, _arg}, _stack, context) do - {:ok, fun(), context} + {fun(), context} end # TODO: local_call(arg) def of_expr({fun, _meta, args}, stack, context) when is_atom(fun) and is_list(args) do - with {:ok, _arg_types, context} <- - map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do - {:ok, dynamic(), context} - end + {_arg_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) + {dynamic(), context} end # var def of_expr(var, _stack, context) when is_var(var) do - {:ok, Of.var(var, context), context} + {Of.var(var, context), context} end ## Try @@ -358,61 +324,53 @@ defmodule Module.Types.Expr do defp of_rescue(var, exceptions, body, expr, hints, meta, stack, context) do args = [__exception__: @atom_true] - with {:ok, structs, context} <- - map_reduce_ok(exceptions, context, fn exception, context -> - # Exceptions are not validated in the compiler, - # to avoid export dependencies. So we do it here. - if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do - Of.struct(exception, args, :merge_defaults, meta, stack, context) - else - # If the exception cannot be found or is invalid, - # we call Of.remote/5 to emit a warning. - context = Of.remote(exception, :__struct__, 0, meta, stack, context) - {:ok, error_type(), context} - end - end) do - context = - case var do - {:_, _, _} -> - context - - _ -> - expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2) - - formatter = fn expr -> - {"rescue #{expr_to_string(expr)} ->", hints} - end - - {:ok, _type, context} = - Of.refine_var(var, expected, expr, formatter, stack, context) - - context + {structs, context} = + Enum.map_reduce(exceptions, context, fn exception, context -> + # Exceptions are not validated in the compiler, + # to avoid export dependencies. So we do it here. + if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do + Of.struct(exception, args, :merge_defaults, meta, stack, context) + else + # If the exception cannot be found or is invalid, + # we call Of.remote/5 to emit a warning. + context = Of.remote(exception, :__struct__, 0, meta, stack, context) + {error_type(), context} end + end) - of_expr_context(body, stack, context) - end + context = + case var do + {:_, _, _} -> + context + + _ -> + expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2) + formatter = fn expr -> {"rescue #{expr_to_string(expr)} ->", hints} end + {_ok?, _type, context} = Of.refine_var(var, expected, expr, formatter, stack, context) + context + end + + of_expr_context(body, stack, context) end ## Comprehensions defp for_clause({:<-, meta, [left, expr]}, stack, context) do {pattern, guards} = extract_head([left]) - - with {:ok, _expr_type, context} <- of_expr(expr, stack, context), - {:ok, _pattern_type, context} <- - Pattern.of_head([pattern], guards, meta, stack, context), - do: {:ok, context} + {_expr_type, context} = of_expr(expr, stack, context) + {[_type], context} = Pattern.of_head([pattern], guards, meta, stack, context) + context end defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]}, stack, context) do - with {:ok, right_type, context} <- of_expr(right, stack, context), - {:ok, _pattern_type, context} <- Pattern.of_match(left, binary(), left, stack, context) do - if binary_type?(right_type) do - {:ok, context} - else - warning = {:badbinary, right_type, right, context} - {:ok, warn(__MODULE__, warning, meta, stack, context)} - end + {right_type, context} = of_expr(right, stack, context) + {_pattern_type, context} = Pattern.of_match(left, binary(), left, stack, context) + + if binary_type?(right_type) do + context + else + warning = {:badbinary, right_type, right, context} + error(__MODULE__, warning, meta, stack, context) end end @@ -429,7 +387,7 @@ defmodule Module.Types.Expr do end defp for_option({:uniq, _}, _stack, context) do - {:ok, context} + context end ## With @@ -437,14 +395,9 @@ defmodule Module.Types.Expr do defp with_clause({:<-, meta, [left, expr]}, stack, context) do {pattern, guards} = extract_head([left]) - with {:ok, _pattern_type, context} <- - Pattern.of_head([pattern], guards, meta, stack, context), - {:ok, _expr_type, context} <- of_expr(expr, stack, context), - do: {:ok, context} - end - - defp with_clause(list, stack, context) when is_list(list) do - reduce_ok(list, context, &with_option(&1, stack, &2)) + {[_type], context} = Pattern.of_head([pattern], guards, meta, stack, context) + {_expr_type, context} = of_expr(expr, stack, context) + context end defp with_clause(expr, stack, context) do @@ -461,10 +414,8 @@ defmodule Module.Types.Expr do ## General helpers - defp error_type(), do: dynamic() - defp apply_many([], _function, _args_types, _expr, _stack, context) do - {:ok, dynamic(), context} + {dynamic(), context} end defp apply_many([mod], function, args_types, expr, stack, context) do @@ -472,21 +423,21 @@ defmodule Module.Types.Expr do end defp apply_many(mods, function, args_types, expr, stack, context) do - with {:ok, returns, context} <- - map_reduce_ok(mods, context, fn mod, context -> - Of.apply(mod, function, args_types, expr, stack, context) - end) do - {:ok, Enum.reduce(returns, &union/2), context} - end + {returns, context} = + Enum.map_reduce(mods, context, fn mod, context -> + Of.apply(mod, function, args_types, expr, stack, context) + end) + + {Enum.reduce(returns, &union/2), context} end defp of_clauses(clauses, stack, context) do - reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context -> + Enum.reduce(clauses, context, fn {:->, meta, [head, body]}, context -> {patterns, guards} = extract_head(head) - with {:ok, _, context} <- Pattern.of_head(patterns, guards, meta, stack, context), - {:ok, _, context} <- of_expr(body, stack, context), - do: {:ok, context} + {_types, context} = Pattern.of_head(patterns, guards, meta, stack, context) + {_, context} = of_expr(body, stack, context) + context end) end @@ -510,10 +461,8 @@ defmodule Module.Types.Expr do end defp of_expr_context(expr, stack, context) do - case of_expr(expr, stack, context) do - {:ok, _type, context} -> {:ok, context} - {:error, context} -> {:error, context} - end + {_type, context} = of_expr(expr, stack, context) + context end ## Warning formatting diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index 820c66aa0a2..19f8f81475f 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -38,48 +38,6 @@ defmodule Module.Types.Helpers do def get_meta({_, meta, _}), do: meta def get_meta(_other), do: [] - ## Traversal helpers - - @doc """ - Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}` - and stops on `{:error, reason}`. - """ - def reduce_ok(list, acc, fun) do - do_reduce_ok(list, acc, fun) - end - - defp do_reduce_ok([head | tail], acc, fun) do - case fun.(head, acc) do - {:ok, acc} -> - do_reduce_ok(tail, acc, fun) - - {:error, reason} -> - {:error, reason} - end - end - - defp do_reduce_ok([], acc, _fun), do: {:ok, acc} - - @doc """ - Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}` - and stops on `{:error, reason}`. - """ - def map_reduce_ok(list, acc, fun) do - do_map_reduce_ok(list, [], acc, fun) - end - - defp do_map_reduce_ok([head | tail], list, acc, fun) do - case fun.(head, acc) do - {:ok, elem, acc} -> - do_map_reduce_ok(tail, [elem | list], acc, fun) - - {:error, reason} -> - {:error, reason} - end - end - - defp do_map_reduce_ok([], list, acc, _fun), do: {:ok, Enum.reverse(list), acc} - ## Warnings @doc """ @@ -271,4 +229,21 @@ defmodule Module.Types.Helpers do %{context | warnings: [{module, warning, location} | context.warnings]} end end + + @doc """ + Emits an error. + + In practice an error is a warning that halts other errors from being collected. + """ + def error(module, warning, meta, stack, context) do + case context do + %{failed: true} -> context + %{failed: false} -> warn(module, warning, meta, stack, %{context | failed: true}) + end + end + + @doc """ + The type to return when there is an error. + """ + def error_type, do: Module.Types.Descr.dynamic() end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 6fb22053aea..c34b333336c 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -47,7 +47,8 @@ defmodule Module.Types.Of do # We need to return error otherwise it leads to cascading errors if empty?(new_type) do - {:error, warn({:refine_var, old_type, type, var, context}, meta, stack, context)} + {:error, error_type(), + error({:refine_var, old_type, type, var, context}, meta, stack, context)} else {:ok, new_type, context} end @@ -79,11 +80,10 @@ defmodule Module.Types.Of do def map_fetch(expr, type, field, stack, context) when is_atom(field) do case map_fetch(type, field) do {_optional?, value_type} -> - {:ok, value_type, context} + {value_type, context} reason -> - {:ok, dynamic(), - warn({reason, expr, type, field, context}, elem(expr, 1), stack, context)} + {error_type(), error({reason, expr, type, field, context}, elem(expr, 1), stack, context)} end end @@ -91,53 +91,51 @@ defmodule Module.Types.Of do Builds a closed map. """ def closed_map(pairs, extra \\ [], stack, context, of_fun) do - result = - reduce_ok(pairs, {true, extra, [], context}, fn + {closed?, single, multiple, context} = + Enum.reduce(pairs, {true, extra, [], context}, fn {key, value}, {closed?, single, multiple, context} -> - with {:ok, keys, context} <- of_finite_key_type(key, stack, context, of_fun), - {:ok, value_type, context} <- of_fun.(value, stack, context) do - case keys do - :none -> - {:ok, {false, single, multiple, context}} - - [key] when multiple == [] -> - {:ok, {closed?, [{key, value_type} | single], multiple, context}} - - keys -> - {:ok, {closed?, single, [{keys, value_type} | multiple], context}} - end + {keys, context} = of_finite_key_type(key, stack, context, of_fun) + {value_type, context} = of_fun.(value, stack, context) + + case keys do + :none -> + {false, single, multiple, context} + + [key] when multiple == [] -> + {closed?, [{key, value_type} | single], multiple, context} + + keys -> + {closed?, single, [{keys, value_type} | multiple], context} end end) - with {:ok, {closed?, single, multiple, context}} <- result do - map = - case Enum.reverse(multiple) do - [] -> - pairs = Enum.reverse(single) - if closed?, do: closed_map(pairs), else: open_map(pairs) + map = + case Enum.reverse(multiple) do + [] -> + pairs = Enum.reverse(single) + if closed?, do: closed_map(pairs), else: open_map(pairs) - [{keys, type} | tail] -> - for key <- keys, t <- cartesian_map(tail) do - pairs = Enum.reverse(single, [{key, type} | t]) - if closed?, do: closed_map(pairs), else: open_map(pairs) - end - |> Enum.reduce(&union/2) - end + [{keys, type} | tail] -> + for key <- keys, t <- cartesian_map(tail) do + pairs = Enum.reverse(single, [{key, type} | t]) + if closed?, do: closed_map(pairs), else: open_map(pairs) + end + |> Enum.reduce(&union/2) + end - {:ok, map, context} - end + {map, context} end defp of_finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do - {:ok, [key], context} + {[key], context} end defp of_finite_key_type(key, stack, context, of_fun) do - with {:ok, key_type, context} <- of_fun.(key, stack, context) do - case atom_fetch(key_type) do - {:finite, list} -> {:ok, list, context} - _ -> {:ok, :none, context} - end + {key_type, context} = of_fun.(key, stack, context) + + case atom_fetch(key_type) do + {:finite, list} -> {list, context} + _ -> {:none, context} end end @@ -157,14 +155,13 @@ defmodule Module.Types.Of do def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun) when is_atom(struct) do # The compiler has already checked the keys are atoms and which ones are required. - with {:ok, args_types, context} <- - map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) -> - with {:ok, type, context} <- of_fun.(value, stack, context) do - {:ok, {key, type}, context} - end - end) do - struct(struct, args_types, default_handling, meta, stack, context) - end + {args_types, context} = + Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> + {type, context} = of_fun.(value, stack, context) + {{key, type}, context} + end) + + struct(struct, args_types, default_handling, meta, stack, context) end @doc """ @@ -186,7 +183,7 @@ defmodule Module.Types.Of do :only_defaults -> [{:__struct__, atom([struct])} | defaults] end - {:ok, dynamic(closed_map(pairs)), context} + {dynamic(closed_map(pairs)), context} end @doc """ @@ -211,7 +208,7 @@ defmodule Module.Types.Of do based on the position of the expression within the binary. """ def binary([], _kind, _stack, context) do - {:ok, context} + context end def binary([head], kind, stack, context) do @@ -219,10 +216,8 @@ defmodule Module.Types.Of do end def binary([head | tail], kind, stack, context) do - case binary_segment(head, kind, [head, @suffix], stack, context) do - {:ok, context} -> binary_many(tail, kind, stack, context) - {:error, context} -> {:error, context} - end + context = binary_segment(head, kind, [head, @suffix], stack, context) + binary_many(tail, kind, stack, context) end defp binary_many([last], kind, stack, context) do @@ -230,24 +225,22 @@ defmodule Module.Types.Of do end defp binary_many([head | tail], kind, stack, context) do - case binary_segment(head, kind, [@prefix, head, @suffix], stack, context) do - {:ok, context} -> binary_many(tail, kind, stack, context) - {:error, context} -> {:error, context} - end + context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context) + binary_many(tail, kind, stack, context) end # If the segment is a literal, the compiler has already checked its validity, # so we just skip it. defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context) when is_binary(left) or is_number(left) do - {:ok, context} + context end defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do type = specifier_type(kind, right) expr = {:<<>>, meta, args} - result = + {_type, context} = case kind do :match -> Module.Types.Pattern.of_match_var(left, type, expr, stack, context) @@ -256,14 +249,11 @@ defmodule Module.Types.Of do Module.Types.Pattern.of_guard(left, type, expr, stack, context) :expr -> - with {:ok, actual, context} <- Module.Types.Expr.of_expr(left, stack, context) do - intersect(actual, type, expr, stack, context) - end + {actual, context} = Module.Types.Expr.of_expr(left, stack, context) + intersect(actual, type, expr, stack, context) end - with {:ok, _type, context} <- result do - {:ok, specifier_size(kind, right, expr, stack, context)} - end + specifier_size(kind, right, expr, stack, context) end defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left) @@ -288,20 +278,15 @@ defmodule Module.Types.Of do defp specifier_size(:expr, {:size, _, [arg]}, expr, stack, context) when not is_integer(arg) do - with {:ok, actual, context} <- Module.Types.Expr.of_expr(arg, stack, context), - {:ok, _, context} <- intersect(actual, integer(), expr, stack, context) do - context - else - {:error, context} -> context - end + {actual, context} = Module.Types.Expr.of_expr(arg, stack, context) + {_, context} = intersect(actual, integer(), expr, stack, context) + context end defp specifier_size(_pattern_or_guard, {:size, _, [arg]}, expr, stack, context) when not is_integer(arg) do - case Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context) do - {:ok, _, context} -> context - {:error, context} -> context - end + {_type, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context) + context end defp specifier_size(_kind, _specifier, _expr, _stack, context) do @@ -318,10 +303,10 @@ defmodule Module.Types.Of do when is_integer(index) do case tuple_fetch(type, index - 1) do {_optional?, value_type} -> - {:ok, value_type, context} + {value_type, context} reason -> - {:ok, dynamic(), warn({reason, expr, type, index - 1, context}, meta, stack, context)} + {error_type(), error({reason, expr, type, index - 1, context}, meta, stack, context)} end end @@ -333,24 +318,24 @@ defmodule Module.Types.Of do match?({false, _}, map_fetch(left, :__struct__)) or match?({false, _}, map_fetch(right, :__struct__)) -> warning = {:struct_comparison, expr, context} - {:ok, result, warn(warning, elem(expr, 1), stack, context)} + {result, warn(__MODULE__, warning, elem(expr, 1), stack, context)} number_type?(left) and number_type?(right) -> - {:ok, result, context} + {result, context} disjoint?(left, right) -> warning = {:mismatched_comparison, expr, context} - {:ok, result, warn(warning, elem(expr, 1), stack, context)} + {result, warn(__MODULE__, warning, elem(expr, 1), stack, context)} true -> - {:ok, result, context} + {result, context} end end def apply(mod, name, args, expr, stack, context) do case :elixir_rewrite.inline(mod, name, length(args)) do {mod, name} -> apply(mod, name, args, expr, stack, context) - false -> {:ok, dynamic(), context} + false -> {dynamic(), context} end end @@ -368,7 +353,7 @@ defmodule Module.Types.Of do :error -> warning = {:badmodule, expr, type, fun, arity, hints, context} - {[], warn(warning, meta, stack, context)} + {[], error(warning, meta, stack, context)} end end @@ -390,12 +375,14 @@ defmodule Module.Types.Of do check_deprecated(mode, module, fun, arity, reason, meta, stack, context) {:ok, mode, :defmacro, reason} -> - context = warn({:unrequired_module, module, fun, arity}, meta, stack, context) + context = + warn(__MODULE__, {:unrequired_module, module, fun, arity}, meta, stack, context) + check_deprecated(mode, module, fun, arity, reason, meta, stack, context) {:error, :module} -> if warn_undefined?(module, fun, arity, stack) do - warn({:undefined_module, module, fun, arity}, meta, stack, context) + warn(__MODULE__, {:undefined_module, module, fun, arity}, meta, stack, context) else context end @@ -403,7 +390,8 @@ defmodule Module.Types.Of do {:error, :function} -> if warn_undefined?(module, fun, arity, stack) do exports = ParallelChecker.all_exports(stack.cache, module) - warn({:undefined_function, module, fun, arity, exports}, meta, stack, context) + payload = {:undefined_function, module, fun, arity, exports} + warn(__MODULE__, payload, meta, stack, context) else context end @@ -412,7 +400,7 @@ defmodule Module.Types.Of do defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do if reason do - warn({:deprecated, module, fun, arity, reason}, meta, stack, context) + warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) else context end @@ -422,12 +410,12 @@ defmodule Module.Types.Of do case :otp_internal.obsolete(module, fun, arity) do {:deprecated, string} when is_list(string) -> reason = string |> List.to_string() |> :string.titlecase() - warn({:deprecated, module, fun, arity, reason}, meta, stack, context) + warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) {:deprecated, string, removal} when is_list(string) and is_list(removal) -> reason = string |> List.to_string() |> :string.titlecase() reason = "It will be removed in #{removal}. #{reason}" - warn({:deprecated, module, fun, arity, reason}, meta, stack, context) + warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) _ -> context @@ -461,15 +449,15 @@ defmodule Module.Types.Of do ## Warning helpers @doc """ - Intersects two types and emit an incompatible warning if empty. + Intersects two types and emit an incompatible error if empty. """ def intersect(actual, expected, expr, stack, context) do type = intersection(actual, expected) if empty?(type) do - {:error, incompatible_warn(expr, expected, actual, stack, context)} + {error_type(), incompatible_error(expr, expected, actual, stack, context)} else - {:ok, type, context} + {type, context} end end @@ -479,15 +467,15 @@ defmodule Module.Types.Of do This is a generic warning for when the expected/actual types themselves may come from several different circumstances. """ - def incompatible_warn(expr, expected_type, actual_type, stack, context) do + def incompatible_error(expr, expected_type, actual_type, stack, context) do meta = get_meta(expr) || stack.meta hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: [] warning = {:incompatible, expr, expected_type, actual_type, hints, context} - warn(warning, meta, stack, context) + error(warning, meta, stack, context) end - defp warn(warning, meta, stack, context) do - warn(__MODULE__, warning, meta, stack, context) + defp error(warning, meta, stack, context) do + error(__MODULE__, warning, meta, stack, context) end ## Warning formatting @@ -612,7 +600,7 @@ defmodule Module.Types.Of do message: IO.iodata_to_binary([ """ - out of range index #{index} in expression: + out of range tuple access at index #{index} in expression: #{expr_to_string(expr) |> indent(4)} diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index ef2786d0861..9fca122ed7e 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -32,30 +32,27 @@ defmodule Module.Types.Pattern do dynamic = dynamic() expected_types = Enum.map(patterns, fn _ -> dynamic end) - with {:ok, _trees, types, context} <- - of_pattern_args(patterns, expected_types, stack, context), - {:ok, _, context} <- - map_reduce_ok(guards, context, &of_guard(&1, @guard, &1, stack, &2)) do - {:ok, types, context} - end + {_trees, types, context} = of_pattern_args(patterns, expected_types, stack, context) + {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) + {types, context} end defp of_pattern_args([], [], _stack, context) do - {:ok, [], context} + {[], [], context} end defp of_pattern_args(patterns, expected_types, stack, context) do context = %{context | pattern_info: {%{}, %{}}} changed = :lists.seq(0, length(patterns) - 1) - with {:ok, trees, context} <- - of_pattern_args_index(patterns, expected_types, 0, [], stack, context), - {:ok, types, context} <- - of_pattern_recur(expected_types, changed, stack, context, fn types, changed, context -> - of_pattern_args_tree(trees, types, changed, 0, [], stack, context) - end) do - {:ok, trees, types, context} - end + {trees, context} = of_pattern_args_index(patterns, expected_types, 0, [], stack, context) + + {types, context} = + of_pattern_recur(expected_types, changed, stack, context, fn types, changed, context -> + of_pattern_args_tree(trees, types, changed, 0, [], stack, context) + end) + + {trees, types, context} end defp of_pattern_args_index( @@ -66,15 +63,13 @@ defmodule Module.Types.Pattern do stack, context ) do - with {:ok, tree, context} <- - of_pattern(pattern, [{:arg, index, type, pattern}], stack, context) do - acc = [{pattern, tree} | acc] - of_pattern_args_index(tail, expected_types, index + 1, acc, stack, context) - end + {tree, context} = of_pattern(pattern, [{:arg, index, type, pattern}], stack, context) + acc = [{pattern, tree} | acc] + of_pattern_args_index(tail, expected_types, index + 1, acc, stack, context) end defp of_pattern_args_index([], [], _index, acc, _stack, context), - do: {:ok, Enum.reverse(acc), context} + do: {Enum.reverse(acc), context} defp of_pattern_args_tree( [{pattern, tree} | tail], @@ -112,17 +107,16 @@ defmodule Module.Types.Pattern do """ def of_match(pattern, expected, expr, stack, context) do context = %{context | pattern_info: {%{}, %{}}} + {tree, context} = of_pattern(pattern, [{:arg, 0, expected, expr}], stack, context) - with {:ok, tree, context} <- - of_pattern(pattern, [{:arg, 0, expected, expr}], stack, context), - {:ok, [type], context} <- - of_pattern_recur([expected], [0], stack, context, fn [type], [0], context -> - with {:ok, type, context} <- of_pattern_intersect(tree, type, expr, stack, context) do - {:ok, [type], context} - end - end) do - {:ok, type, context} - end + {[type], context} = + of_pattern_recur([expected], [0], stack, context, fn [type], [0], context -> + with {:ok, type, context} <- of_pattern_intersect(tree, type, expr, stack, context) do + {:ok, [type], context} + end + end) + + {type, context} end defp of_pattern_recur(types, changed, stack, context, callback) do @@ -130,77 +124,85 @@ defmodule Module.Types.Pattern do context = %{context | pattern_info: nil} pattern_vars = Map.to_list(pattern_vars) of_pattern_recur(types, changed, pattern_vars, pattern_args, stack, context, callback) + catch + {types, context} -> {types, context} end defp of_pattern_recur(types, [], _vars, _args, _stack, context, _callback) do - {:ok, types, context} + {types, context} end defp of_pattern_recur(types, changed, vars, args, stack, context, callback) do - with {:ok, types, %{vars: context_vars} = context} <- callback.(types, changed, context) do - result = - reduce_ok(vars, {[], context}, fn {version, paths}, {changed, context} -> - current_type = context_vars[version][:type] - - result = - reduce_ok(paths, {false, context}, fn - [var, {:arg, index, expected, expr} | path], {var_changed?, context} -> - actual = Enum.fetch!(types, index) - - case of_pattern_var(path, actual) do - {:ok, type} -> - with {:ok, type, context} <- Of.refine_var(var, type, expr, stack, context) do - {:ok, {var_changed? or current_type != type, context}} - end + case callback.(types, changed, context) do + {:ok, types, %{vars: context_vars} = context} -> + {changed, context} = + Enum.reduce(vars, {[], context}, fn {version, paths}, {changed, context} -> + current_type = context_vars[version][:type] + + {var_changed?, context} = + Enum.reduce(paths, {false, context}, fn + [var, {:arg, index, expected, expr} | path], {var_changed?, context} -> + actual = Enum.fetch!(types, index) + + case of_pattern_var(path, actual) do + {:ok, type} -> + case Of.refine_var(var, type, expr, stack, context) do + {:ok, type, context} -> + {var_changed? or current_type != type, context} + + {:error, _type, context} -> + throw({types, context}) + end + + :error -> + context = Of.incompatible_error(expr, expected, actual, stack, context) + throw({types, context}) + end + end) - :error -> - {:error, Of.incompatible_warn(expr, expected, actual, stack, context)} - end - end) - - with {:ok, {var_changed?, context}} <- result do case var_changed? do false -> - {:ok, {changed, context}} + {changed, context} true -> case paths do # A single change, check if there are other variables in this index. [[_var, {:arg, index, _, _} | _]] -> case args do - %{^index => true} -> {:ok, {[index | changed], context}} - %{^index => false} -> {:ok, {changed, context}} + %{^index => true} -> {[index | changed], context} + %{^index => false} -> {changed, context} end # Several changes, we have to recompute all indexes. _ -> var_changed = Enum.map(paths, fn [_var, {:arg, index, _, _} | _] -> index end) - {:ok, {var_changed ++ changed, context}} + {var_changed ++ changed, context} end end - end - end) + end) - with {:ok, {changed, context}} <- result do of_pattern_recur(types, :lists.usort(changed), vars, args, stack, context, callback) - end + + {:error, context} -> + {types, context} end end defp of_pattern_intersect(tree, expected, expr, stack, context) do actual = of_pattern_tree(tree, context) + type = intersection(actual, expected) - case Of.intersect(actual, expected, expr, stack, context) do - {:ok, type, context} -> + cond do + not empty?(type) -> {:ok, type, context} - {:error, intersection_context} -> - if empty?(actual) do - meta = get_meta(expr) || stack.meta - {:error, warn(__MODULE__, {:invalid_pattern, expr, context}, meta, stack, context)} - else - {:error, intersection_context} - end + empty?(actual) -> + # The pattern itself is invalid + meta = get_meta(expr) || stack.meta + {:error, error(__MODULE__, {:invalid_pattern, expr, context}, meta, stack, context)} + + true -> + {:error, Of.incompatible_error(expr, expected, actual, stack, context)} end end @@ -282,11 +284,12 @@ defmodule Module.Types.Pattern do end def of_match_var({:_, _, _}, expected, _expr, _stack, context) do - {:ok, expected, context} + {expected, context} end def of_match_var(var, expected, expr, stack, context) when is_var(var) do - Of.refine_var(var, expected, expr, stack, context) + {_ok?, type, context} = Of.refine_var(var, expected, expr, stack, context) + {type, context} end def of_match_var(ast, expected, expr, stack, context) do @@ -297,23 +300,23 @@ defmodule Module.Types.Pattern do # :atom defp of_pattern(atom, _path, _stack, context) when is_atom(atom), - do: {:ok, atom([atom]), context} + do: {atom([atom]), context} # 12 defp of_pattern(literal, _path, _stack, context) when is_integer(literal), - do: {:ok, integer(), context} + do: {integer(), context} # 1.2 defp of_pattern(literal, _path, _stack, context) when is_float(literal), - do: {:ok, float(), context} + do: {float(), context} # "..." defp of_pattern(literal, _path, _stack, context) when is_binary(literal), - do: {:ok, binary(), context} + do: {binary(), context} # [] defp of_pattern([], _path, _stack, context), - do: {:ok, empty_list(), context} + do: {empty_list(), context} # [expr, ...] defp of_pattern(list, path, stack, context) when is_list(list) do @@ -331,27 +334,25 @@ defmodule Module.Types.Pattern do result = match |> unpack_match([]) - |> reduce_ok({[], [], context}, fn pattern, {static, dynamic, context} -> - with {:ok, type, context} <- of_pattern(pattern, path, stack, context) do - if is_descr(type) do - {:ok, {[type | static], dynamic, context}} - else - {:ok, {static, [type | dynamic], context}} - end + |> Enum.reduce({[], [], context}, fn pattern, {static, dynamic, context} -> + {type, context} = of_pattern(pattern, path, stack, context) + + if is_descr(type) do + {[type | static], dynamic, context} + else + {static, [type | dynamic], context} end end) - with {:ok, acc} <- result do - case acc do - {[], dynamic, context} -> - {:ok, {:intersection, dynamic}, context} + case result do + {[], dynamic, context} -> + {{:intersection, dynamic}, context} - {static, [], context} -> - {:ok, Enum.reduce(static, &intersection/2), context} + {static, [], context} -> + {Enum.reduce(static, &intersection/2), context} - {static, dynamic, context} -> - {:ok, {:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}, context} - end + {static, dynamic, context} -> + {{:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}, context} end end @@ -361,48 +362,44 @@ defmodule Module.Types.Pattern do when is_atom(struct) do {info, context} = Of.struct_info(struct, meta, stack, context) - result = - map_reduce_ok(args, context, fn {key, value}, context -> - with {:ok, value_type, context} <- of_pattern(value, [{:key, key} | path], stack, context) do - {:ok, {key, value_type}, context} + {pairs, context} = + Enum.map_reduce(args, context, fn {key, value}, context -> + {value_type, context} = of_pattern(value, [{:key, key} | path], stack, context) + {{key, value_type}, context} + end) + + pairs = Map.new(pairs) + term = term() + static = [__struct__: atom([struct])] + dynamic = [] + + {static, dynamic} = + Enum.reduce(info, {static, dynamic}, fn %{field: field}, {static, dynamic} -> + case pairs do + %{^field => value_type} when is_descr(value_type) -> + {[{field, value_type} | static], dynamic} + + %{^field => value_type} -> + {static, [{field, value_type} | dynamic]} + + _ -> + {[{field, term} | static], dynamic} end end) - with {:ok, pairs, context} <- result do - pairs = Map.new(pairs) - term = term() - static = [__struct__: atom([struct])] - dynamic = [] - - {static, dynamic} = - Enum.reduce(info, {static, dynamic}, fn %{field: field}, {static, dynamic} -> - case pairs do - %{^field => value_type} when is_descr(value_type) -> - {[{field, value_type} | static], dynamic} - - %{^field => value_type} -> - {static, [{field, value_type} | dynamic]} - - _ -> - {[{field, term} | static], dynamic} - end - end) - - if dynamic == [] do - {:ok, closed_map(static), context} - else - {:ok, {:closed_map, static, dynamic}, context} - end + if dynamic == [] do + {closed_map(static), context} + else + {{:closed_map, static, dynamic}, context} end end # %var{...} defp of_pattern({:%, _, [{name, _, ctx} = var, {:%{}, _, args}]}, path, stack, context) when is_atom(name) and is_atom(ctx) and name != :_ do - with {:ok, var, context} <- of_pattern(var, [{:key, :__struct__} | path], stack, context) do - dynamic = [__struct__: {:intersection, [atom(), var]}] - of_open_map(args, [], dynamic, path, stack, context) - end + {var, context} = of_pattern(var, [{:key, :__struct__} | path], stack, context) + dynamic = [__struct__: {:intersection, [atom(), var]}] + of_open_map(args, [], dynamic, path, stack, context) end # %^var{...} and %_{...} @@ -412,9 +409,8 @@ defmodule Module.Types.Pattern do stack, context ) do - with {:ok, refined, context} <- of_match_var(var, atom(), expr, stack, context) do - of_open_map(args, [__struct__: refined], [], path, stack, context) - end + {refined, context} = of_match_var(var, atom(), expr, stack, context) + of_open_map(args, [__struct__: refined], [], path, stack, context) end # %{...} @@ -424,9 +420,8 @@ defmodule Module.Types.Pattern do # <<...>>> defp of_pattern({:<<>>, _meta, args}, _path, stack, context) do - with {:ok, context} <- Of.binary(args, :match, stack, context) do - {:ok, binary(), context} - end + context = Of.binary(args, :match, stack, context) + {binary(), context} end # left ++ right @@ -441,12 +436,12 @@ defmodule Module.Types.Pattern do # ^var defp of_pattern({:^, _meta, [var]}, _path, _stack, context) do - {:ok, Of.var(var, context), context} + {Of.var(var, context), context} end # _ defp of_pattern({:_, _meta, _var_context}, _path, _stack, context) do - {:ok, term(), context} + {term(), context} end # var @@ -467,56 +462,51 @@ defmodule Module.Types.Pattern do %{} -> Map.put(args, arg, false) end - {:ok, {:var, version}, %{context | pattern_info: {vars, args}}} + {{:var, version}, %{context | pattern_info: {vars, args}}} end # TODO: Properly traverse domain keys # TODO: Properly handle pin operator in keys defp of_open_map(args, static, dynamic, path, stack, context) do - result = - reduce_ok(args, {static, dynamic, context}, fn {key, value}, {static, dynamic, context} -> - with {:ok, value_type, context} <- of_pattern(value, [{:key, key} | path], stack, context) do - cond do - # Only atom keys become part of the type because the other keys are divisible - not is_atom(key) -> - {:ok, {static, dynamic, context}} - - is_descr(value_type) -> - {:ok, {[{key, value_type} | static], dynamic, context}} - - true -> - {:ok, {static, [{key, value_type} | dynamic], context}} - end + {static, dynamic, context} = + Enum.reduce(args, {static, dynamic, context}, fn {key, value}, {static, dynamic, context} -> + {value_type, context} = of_pattern(value, [{:key, key} | path], stack, context) + + cond do + # Only atom keys become part of the type because the other keys are divisible + not is_atom(key) -> + {static, dynamic, context} + + is_descr(value_type) -> + {[{key, value_type} | static], dynamic, context} + + true -> + {static, [{key, value_type} | dynamic], context} end end) - with {:ok, {static, dynamic, context}} <- result do - case dynamic do - [] -> {:ok, open_map(static), context} - _ -> {:ok, {:open_map, static, dynamic}, context} - end + case dynamic do + [] -> {open_map(static), context} + _ -> {{:open_map, static, dynamic}, context} end end defp of_tuple(args, path, stack, context) do - result = - reduce_ok(args, {0, true, [], context}, fn arg, {index, static?, acc, context} -> - with {:ok, type, context} <- of_pattern(arg, [{:elem, index} | path], stack, context) do - {:ok, {index + 1, static? and is_descr(type), [type | acc], context}} - end + {_index, static?, entries, context} = + Enum.reduce(args, {0, true, [], context}, fn arg, {index, static?, acc, context} -> + {type, context} = of_pattern(arg, [{:elem, index} | path], stack, context) + {index + 1, static? and is_descr(type), [type | acc], context} end) - with {:ok, {_index, static?, entries, context}} <- result do - case static? do - true -> {:ok, tuple(Enum.reverse(entries)), context} - false -> {:ok, {:tuple, Enum.reverse(entries)}, context} - end + case static? do + true -> {tuple(Enum.reverse(entries)), context} + false -> {{:tuple, Enum.reverse(entries)}, context} end end # [] ++ [] defp of_list([], [], _path, _stack, context) do - {:ok, empty_list(), context} + {empty_list(), context} end # [] ++ suffix @@ -526,30 +516,28 @@ defmodule Module.Types.Pattern do # [prefix1, prefix2, prefix3], [prefix1, prefix2 | suffix] defp of_list(prefix, suffix, path, stack, context) do - with {:ok, suffix, context} <- of_pattern(suffix, [:tail | path], stack, context) do - result = - reduce_ok(prefix, {[], [], context}, fn arg, {static, dynamic, context} -> - with {:ok, type, context} <- of_pattern(arg, [:head | path], stack, context) do - if is_descr(type) do - {:ok, {[type | static], dynamic, context}} - else - {:ok, {static, [type | dynamic], context}} - end - end - end) - - with {:ok, acc} <- result do - case acc do - {static, [], context} when is_descr(suffix) -> - {:ok, non_empty_list(Enum.reduce(static, &union/2), suffix), context} + {suffix, context} = of_pattern(suffix, [:tail | path], stack, context) - {[], dynamic, context} -> - {:ok, {:non_empty_list, dynamic, suffix}, context} + acc = + Enum.reduce(prefix, {[], [], context}, fn arg, {static, dynamic, context} -> + {type, context} = of_pattern(arg, [:head | path], stack, context) - {static, dynamic, context} -> - {:ok, {:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix}, context} + if is_descr(type) do + {[type | static], dynamic, context} + else + {static, [type | dynamic], context} end - end + end) + + case acc do + {static, [], context} when is_descr(suffix) -> + {non_empty_list(Enum.reduce(static, &union/2), suffix), context} + + {[], dynamic, context} -> + {{:non_empty_list, dynamic, suffix}, context} + + {static, dynamic, context} -> + {{:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix}, context} end end @@ -559,45 +547,45 @@ defmodule Module.Types.Pattern do # :atom def of_guard(atom, expected, expr, stack, context) when is_atom(atom) do if atom_type?(expected, atom) do - {:ok, atom([atom]), context} + {atom([atom]), context} else - {:error, Of.incompatible_warn(expr, expected, atom([atom]), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, atom([atom]), stack, context)} end end # 12 def of_guard(literal, expected, expr, stack, context) when is_integer(literal) do if integer_type?(expected) do - {:ok, integer(), context} + {integer(), context} else - {:error, Of.incompatible_warn(expr, expected, integer(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, integer(), stack, context)} end end # 1.2 def of_guard(literal, expected, expr, stack, context) when is_float(literal) do if float_type?(expected) do - {:ok, float(), context} + {float(), context} else - {:error, Of.incompatible_warn(expr, expected, float(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, float(), stack, context)} end end # "..." def of_guard(literal, expected, expr, stack, context) when is_binary(literal) do if binary_type?(expected) do - {:ok, binary(), context} + {binary(), context} else - {:error, Of.incompatible_warn(expr, expected, binary(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, binary(), stack, context)} end end # [] def of_guard([], expected, expr, stack, context) do if empty_list_type?(expected) do - {:ok, empty_list(), context} + {empty_list(), context} else - {:error, Of.incompatible_warn(expr, expected, empty_list(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, empty_list(), stack, context)} end end @@ -605,11 +593,11 @@ defmodule Module.Types.Pattern do def of_guard(list, _expected, expr, stack, context) when is_list(list) do {prefix, suffix} = unpack_list(list, []) - with {:ok, prefix, context} <- - map_reduce_ok(prefix, context, &of_guard(&1, dynamic(), expr, stack, &2)), - {:ok, suffix, context} <- of_guard(suffix, dynamic(), expr, stack, context) do - {:ok, non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} - end + {prefix, context} = + Enum.map_reduce(prefix, context, &of_guard(&1, dynamic(), expr, stack, &2)) + + {suffix, context} = of_guard(suffix, dynamic(), expr, stack, context) + {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} end # {left, right} @@ -632,11 +620,10 @@ defmodule Module.Types.Pattern do # <<>> def of_guard({:<<>>, _meta, args}, expected, expr, stack, context) do if binary_type?(expected) do - with {:ok, context} <- Of.binary(args, :guard, stack, context) do - {:ok, binary(), context} - end + context = Of.binary(args, :guard, stack, context) + {binary(), context} else - {:error, Of.incompatible_warn(expr, expected, binary(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, binary(), stack, context)} end end @@ -648,27 +635,24 @@ defmodule Module.Types.Pattern do # {...} def of_guard({:{}, _meta, args}, _expected, expr, stack, context) do - with {:ok, types, context} <- - map_reduce_ok(args, context, &of_guard(&1, dynamic(), expr, stack, &2)) do - {:ok, tuple(types), context} - end + {types, context} = Enum.map_reduce(args, context, &of_guard(&1, dynamic(), expr, stack, &2)) + {tuple(types), context} end # var.field def of_guard({{:., _, [callee, key]}, _, []} = map_fetch, _expected, expr, stack, context) when not is_atom(callee) do - with {:ok, type, context} <- of_guard(callee, dynamic(), expr, stack, context) do - Of.map_fetch(map_fetch, type, key, stack, context) - end + {type, context} = of_guard(callee, dynamic(), expr, stack, context) + Of.map_fetch(map_fetch, type, key, stack, context) end # Remote def of_guard({{:., _, [:erlang, function]}, _, args}, _expected, expr, stack, context) when is_atom(function) do - with {:ok, args_type, context} <- - map_reduce_ok(args, context, &of_guard(&1, dynamic(), expr, stack, &2)) do - Of.apply(:erlang, function, args_type, expr, stack, context) - end + {args_type, context} = + Enum.map_reduce(args, context, &of_guard(&1, dynamic(), expr, stack, &2)) + + Of.apply(:erlang, function, args_type, expr, stack, context) end # var diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 7bb60a146a8..9e25c90c79c 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -279,7 +279,7 @@ defmodule Module.Types.ExprTest do assert typewarn!(elem({:ok, 123}, 2)) == {dynamic(), ~l""" - out of range index 2 in expression: + out of range tuple access at index 2 in expression: elem({:ok, 123}, 2) diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 28b3b0b1128..4593d4026fb 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -59,32 +59,35 @@ defmodule TypeHelper do end @doc false - def __typecheck__!({:ok, type, %{warnings: []}}), do: type + def __typecheck__!({type, %{warnings: []}}), do: type - def __typecheck__!({:ok, _type, %{warnings: warnings}}), + def __typecheck__!({_type, %{warnings: warnings, failed: false}}), do: raise("type checking ok but with warnings: #{inspect(warnings)}") - def __typecheck__!({:error, %{warnings: warnings}}), + def __typecheck__!({_type, %{warnings: warnings, failed: true}}), do: raise("type checking errored with warnings: #{inspect(warnings)}") @doc false - def __typeerror__!({:error, %{warnings: [{module, warning, _locs} | _]}}), + def __typeerror__!({_type, %{warnings: [{module, warning, _locs} | _], failed: true}}), do: module.format_diagnostic(warning).message - def __typeerror__!({:ok, type, _context}), + def __typeerror__!({_type, %{warnings: warnings, failed: false}}), + do: raise("type checking with warnings but expected error: #{inspect(warnings)}") + + def __typeerror__!({type, _}), do: raise("type checking ok but expected error: #{Descr.to_quoted_string(type)}") @doc false - def __typediag__!({:ok, type, %{warnings: [{module, warning, _locs}]}}), + def __typediag__!({type, %{warnings: [{module, warning, _locs}]}}), do: {type, module.format_diagnostic(warning)} - def __typediag__!({:ok, type, %{warnings: []}}), + def __typediag__!({type, %{warnings: []}}), do: raise("type checking ok without warnings: #{Descr.to_quoted_string(type)}") - def __typediag__!({:ok, _type, %{warnings: warnings}}), + def __typediag__!({_type, %{warnings: warnings, failed: false}}), do: raise("type checking ok but many warnings: #{inspect(warnings)}") - def __typediag__!({:error, %{warnings: warnings}}), + def __typediag__!({:error, %{warnings: warnings, failed: true}}), do: raise("type checking errored with warnings: #{inspect(warnings)}") @doc false @@ -128,11 +131,8 @@ defmodule TypeHelper do def __typecheck__(patterns, guards, body) do stack = new_stack() - - with {:ok, _types, context} <- Pattern.of_head(patterns, guards, [], stack, new_context()), - {:ok, type, context} <- Expr.of_expr(body, stack, context) do - {:ok, type, context} - end + {_types, context} = Pattern.of_head(patterns, guards, [], stack, new_context()) + Expr.of_expr(body, stack, context) end defp expand_and_unpack(patterns, guards, body, env) do