From 7e7c357cec6d853437fcd47a60f8cd1cd155662e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 10:48:00 +0200 Subject: [PATCH 01/11] Fewer ok --- lib/elixir/lib/module/behaviour.ex | 2 - lib/elixir/lib/module/types.ex | 8 +- lib/elixir/lib/module/types/expr.ex | 21 +++-- lib/elixir/lib/module/types/helpers.ex | 16 ++++ lib/elixir/lib/module/types/of.ex | 88 ++++++++++--------- lib/elixir/lib/module/types/pattern.ex | 73 ++++++++------- .../test/elixir/module/types/expr_test.exs | 2 +- 7 files changed, 122 insertions(+), 88 deletions(-) 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..e5fadf4e32f 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -83,10 +83,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..9fc0870328d 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -76,11 +76,8 @@ defmodule Module.Types.Expr do # <<...>>> 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) + {:ok, binary(), context} end def of_expr({:__CALLER__, _meta, var_context}, _stack, context) @@ -104,7 +101,8 @@ defmodule Module.Types.Expr do # 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) + {type, context} = Pattern.of_match(left_expr, right_type, expr, stack, context) + {:ok, type, context} end end @@ -136,7 +134,7 @@ defmodule Module.Types.Expr do {: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)} + {:ok, 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) @@ -290,7 +288,7 @@ defmodule Module.Types.Expr do context = case fun_fetch(fun_type, length(args)) do :ok -> context - :error -> Of.incompatible_warn(fun, fun(), fun_type, stack, context) + :error -> Of.incompatible_error(fun, fun(), fun_type, stack, context) end {:ok, dynamic(), context} @@ -405,13 +403,14 @@ defmodule Module.Types.Expr do 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 + with {:ok, right_type, context} <- of_expr(right, stack, context) do + {_pattern_type, context} = Pattern.of_match(left, binary(), left, stack, context) + if binary_type?(right_type) do {:ok, context} else warning = {:badbinary, right_type, right, context} - {:ok, warn(__MODULE__, warning, meta, stack, context)} + {:ok, error(__MODULE__, warning, meta, stack, context)} end end end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index 820c66aa0a2..fc7ec699c32 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -75,6 +75,10 @@ defmodule Module.Types.Helpers do {:error, reason} -> {:error, reason} + + _ -> + IO.inspect({head, fun}) + raise "oops" end end @@ -271,4 +275,16 @@ 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 end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 6fb22053aea..996797e7c24 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -47,7 +47,7 @@ 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({:refine_var, old_type, type, var, context}, meta, stack, context)} else {:ok, new_type, context} end @@ -83,7 +83,7 @@ defmodule Module.Types.Of do reason -> {:ok, dynamic(), - warn({reason, expr, type, field, context}, elem(expr, 1), stack, context)} + error({reason, expr, type, field, context}, elem(expr, 1), stack, context)} end end @@ -211,7 +211,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 +219,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,40 +228,47 @@ 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 = + context = case kind do :match -> - Module.Types.Pattern.of_match_var(left, type, expr, stack, context) + case Module.Types.Pattern.of_match_var(left, type, expr, stack, context) do + {:ok, _type, context} -> context + {:error, context} -> context + end :guard -> - Module.Types.Pattern.of_guard(left, type, expr, stack, context) + case Module.Types.Pattern.of_guard(left, type, expr, stack, context) do + {:ok, _type, context} -> context + {:error, context} -> context + end :expr -> - with {:ok, actual, context} <- Module.Types.Expr.of_expr(left, stack, context) do - intersect(actual, type, expr, stack, context) + case Module.Types.Expr.of_expr(left, stack, context) do + {:ok, actual, context} -> + {_type, context} = intersect(actual, type, expr, stack, context) + context + + {:error, context} -> + context end 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,8 +293,8 @@ 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 + with {:ok, actual, context} <- Module.Types.Expr.of_expr(arg, stack, context) do + {_, context} = intersect(actual, integer(), expr, stack, context) context else {:error, context} -> context @@ -321,7 +326,7 @@ defmodule Module.Types.Of do {:ok, value_type, context} reason -> - {:ok, dynamic(), warn({reason, expr, type, index - 1, context}, meta, stack, context)} + {:ok, dynamic(), error({reason, expr, type, index - 1, context}, meta, stack, context)} end end @@ -333,14 +338,14 @@ 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)} + {:ok, result, error(warning, elem(expr, 1), stack, context)} number_type?(left) and number_type?(right) -> {:ok, result, context} disjoint?(left, right) -> warning = {:mismatched_comparison, expr, context} - {:ok, result, warn(warning, elem(expr, 1), stack, context)} + {:ok, result, error(warning, elem(expr, 1), stack, context)} true -> {:ok, result, context} @@ -368,7 +373,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 +395,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 = + error(__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) + error(__MODULE__, {:undefined_module, module, fun, arity}, meta, stack, context) else context end @@ -403,7 +410,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} + error(__MODULE__, payload, meta, stack, context) else context end @@ -412,7 +420,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) + error(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) else context end @@ -422,12 +430,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) + error(__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) + error(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) _ -> context @@ -461,15 +469,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)} + {dynamic(), incompatible_error(expr, expected, actual, stack, context)} else - {:ok, type, context} + {type, context} end end @@ -479,15 +487,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 +620,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..fa0b15918ac 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -44,8 +44,8 @@ defmodule Module.Types.Pattern do {:ok, [], context} end - defp of_pattern_args(patterns, expected_types, stack, context) do - context = %{context | pattern_info: {%{}, %{}}} + defp of_pattern_args(patterns, expected_types, stack, init_context) do + context = %{init_context | pattern_info: {%{}, %{}}, failed: false} changed = :lists.seq(0, length(patterns) - 1) with {:ok, trees, context} <- @@ -54,7 +54,7 @@ defmodule Module.Types.Pattern do 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} + {:ok, trees, types, merge_failed(init_context, context)} end end @@ -110,8 +110,8 @@ defmodule Module.Types.Pattern do Return the type and typing context of a pattern expression with the given expected and expr or an error in case of a typing conflict. """ - def of_match(pattern, expected, expr, stack, context) do - context = %{context | pattern_info: {%{}, %{}}} + def of_match(pattern, expected, expr, stack, init_context) do + context = %{init_context | pattern_info: {%{}, %{}}, failed: false} with {:ok, tree, context} <- of_pattern(pattern, [{:arg, 0, expected, expr}], stack, context), @@ -121,7 +121,9 @@ defmodule Module.Types.Pattern do {:ok, [type], context} end end) do - {:ok, type, context} + {type, merge_failed(init_context, context)} + else + {:error, context} -> {dynamic(), context} end end @@ -136,6 +138,10 @@ defmodule Module.Types.Pattern do {:ok, types, context} end + defp of_pattern_recur(_types, _, _vars, _args, _stack, %{failed: true} = context, _callback) do + {:error, 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 = @@ -154,7 +160,7 @@ defmodule Module.Types.Pattern do end :error -> - {:error, Of.incompatible_warn(expr, expected, actual, stack, context)} + {:error, Of.incompatible_error(expr, expected, actual, stack, context)} end end) @@ -189,18 +195,19 @@ defmodule Module.Types.Pattern do 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 @@ -278,7 +285,8 @@ defmodule Module.Types.Pattern do and binary patterns. """ def of_match_var({:^, _, [var]}, expected, expr, stack, context) do - Of.intersect(Of.var(var, context), expected, expr, stack, context) + {type, dynamic} = Of.intersect(Of.var(var, context), expected, expr, stack, context) + {:ok, type, dynamic} end def of_match_var({:_, _, _}, expected, _expr, _stack, context) do @@ -424,9 +432,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) + {:ok, binary(), context} end # left ++ right @@ -561,7 +568,7 @@ defmodule Module.Types.Pattern do if atom_type?(expected, atom) do {:ok, atom([atom]), context} else - {:error, Of.incompatible_warn(expr, expected, atom([atom]), stack, context)} + {:error, Of.incompatible_error(expr, expected, atom([atom]), stack, context)} end end @@ -570,7 +577,7 @@ defmodule Module.Types.Pattern do if integer_type?(expected) do {:ok, integer(), context} else - {:error, Of.incompatible_warn(expr, expected, integer(), stack, context)} + {:error, Of.incompatible_error(expr, expected, integer(), stack, context)} end end @@ -579,7 +586,7 @@ defmodule Module.Types.Pattern do if float_type?(expected) do {:ok, float(), context} else - {:error, Of.incompatible_warn(expr, expected, float(), stack, context)} + {:error, Of.incompatible_error(expr, expected, float(), stack, context)} end end @@ -588,7 +595,7 @@ defmodule Module.Types.Pattern do if binary_type?(expected) do {:ok, binary(), context} else - {:error, Of.incompatible_warn(expr, expected, binary(), stack, context)} + {:error, Of.incompatible_error(expr, expected, binary(), stack, context)} end end @@ -597,7 +604,7 @@ defmodule Module.Types.Pattern do if empty_list_type?(expected) do {:ok, empty_list(), context} else - {:error, Of.incompatible_warn(expr, expected, empty_list(), stack, context)} + {:error, Of.incompatible_error(expr, expected, empty_list(), stack, context)} end end @@ -632,18 +639,18 @@ 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) + {:ok, binary(), context} else - {:error, Of.incompatible_warn(expr, expected, binary(), stack, context)} + {:error, Of.incompatible_error(expr, expected, binary(), stack, context)} end end # ^var def of_guard({:^, _meta, [var]}, expected, expr, stack, context) do # This is by definition a variable defined outside of this pattern, so we don't track it. - Of.intersect(Of.var(var, context), expected, expr, stack, context) + {type, context} = Of.intersect(Of.var(var, context), expected, expr, stack, context) + {:ok, type, context} end # {...} @@ -673,11 +680,15 @@ defmodule Module.Types.Pattern do # var def of_guard(var, expected, expr, stack, context) when is_var(var) do - Of.intersect(Of.var(var, context), expected, expr, stack, context) + {type, context} = Of.intersect(Of.var(var, context), expected, expr, stack, context) + {:ok, type, context} end ## Helpers + defp merge_failed(%{failed: false}, %{failed: false} = post), do: post + defp merge_failed(_pre, post), do: %{post | failed: true} + def format_diagnostic({:invalid_pattern, expr, context}) do traces = collect_traces(expr, context) 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) From b369df4a6754942818f16344572892529946285c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 10:53:50 +0200 Subject: [PATCH 02/11] More refactor --- lib/elixir/lib/module/types/expr.ex | 10 +---- lib/elixir/lib/module/types/of.ex | 6 +-- lib/elixir/lib/module/types/pattern.ex | 60 ++++++++++++-------------- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 9fc0870328d..83ebdc7f729 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -376,14 +376,8 @@ defmodule Module.Types.Expr do _ -> 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) - + formatter = fn expr -> {"rescue #{expr_to_string(expr)} ->", hints} end + {_type, context} = Of.refine_var(var, expected, expr, formatter, stack, context) context end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 996797e7c24..2bc0b027f13 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -47,9 +47,9 @@ defmodule Module.Types.Of do # We need to return error otherwise it leads to cascading errors if empty?(new_type) do - {:error, error({:refine_var, old_type, type, var, context}, meta, stack, context)} + {dynamic(), error({:refine_var, old_type, type, var, context}, meta, stack, context)} else - {:ok, new_type, context} + {new_type, context} end %{} -> @@ -61,7 +61,7 @@ defmodule Module.Types.Of do } context = put_in(context.vars[version], data) - {:ok, type, context} + {type, context} end end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index fa0b15918ac..fcc6586c107 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -144,52 +144,47 @@ defmodule Module.Types.Pattern do 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} -> + {changed, context} = + Enum.reduce(vars, {[], context}, fn {version, paths}, {changed, context} -> current_type = context_vars[version][:type] - result = - reduce_ok(paths, {false, context}, fn + {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} -> - with {:ok, type, context} <- Of.refine_var(var, type, expr, stack, context) do - {:ok, {var_changed? or current_type != type, context}} - end + {type, context} = Of.refine_var(var, type, expr, stack, context) + {var_changed? or current_type != type, context} :error -> - {:error, Of.incompatible_error(expr, expected, actual, stack, context)} + {var_changed?, Of.incompatible_error(expr, expected, actual, stack, context)} end end) - with {:ok, {var_changed?, context}} <- result do - case var_changed? do - false -> - {:ok, {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}} - 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}} - end - end + case var_changed? do + false -> + {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} -> {[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) + {var_changed ++ changed, context} + end end end) - with {:ok, {changed, context}} <- result do - of_pattern_recur(types, :lists.usort(changed), vars, args, stack, context, callback) - end + of_pattern_recur(types, :lists.usort(changed), vars, args, stack, context, callback) end end @@ -294,7 +289,8 @@ defmodule Module.Types.Pattern do end def of_match_var(var, expected, expr, stack, context) when is_var(var) do - Of.refine_var(var, expected, expr, stack, context) + {type, dynamic} = Of.refine_var(var, expected, expr, stack, context) + {:ok, type, dynamic} end def of_match_var(ast, expected, expr, stack, context) do From 6ff6b44accbfef9a8ff017821a236935d2654a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 11:00:49 +0200 Subject: [PATCH 03/11] More refactor --- lib/elixir/lib/module/types/expr.ex | 21 +++-- lib/elixir/lib/module/types/of.ex | 101 ++++++++++++++----------- lib/elixir/lib/module/types/pattern.ex | 6 +- 3 files changed, 75 insertions(+), 53 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 83ebdc7f729..65488e4b13b 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -108,8 +108,9 @@ defmodule Module.Types.Expr do # %{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 + {_args_type, context} = Of.closed_map(args, stack, context, &of_expr/3) + + with {: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} @@ -129,7 +130,7 @@ defmodule Module.Types.Expr do end end), # TODO: args_types could be an empty list - {:ok, struct_type, context} <- + {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 @@ -137,20 +138,25 @@ defmodule Module.Types.Expr do {:ok, 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) + {type, context} = + Of.struct(module, args_types, :merge_defaults, struct_meta, stack, context) + + {:ok, type, context} end end end # %{...} def of_expr({:%{}, _meta, args}, stack, context) do - Of.closed_map(args, stack, context, &of_expr/3) + {type, context} = Of.closed_map(args, stack, context, &of_expr/3) + {:ok, type, context} end # %Struct{} def of_expr({:%, _, [module, {:%{}, _, args}]} = expr, stack, context) do # TODO: We should not skip defaults - Of.struct(expr, module, args, :skip_defaults, stack, context, &of_expr/3) + {type, context} = Of.struct(expr, module, args, :skip_defaults, stack, context, &of_expr/3) + {:ok, type, context} end # () @@ -361,7 +367,8 @@ defmodule Module.Types.Expr do # 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) + {type, context} = Of.struct(exception, args, :merge_defaults, meta, stack, context) + {:ok, type, context} else # If the exception cannot be found or is invalid, # we call Of.remote/5 to emit a warning. diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 2bc0b027f13..5a8008b9af3 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -91,53 +91,59 @@ 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 + of_fun = fn arg1, arg2, arg3 -> + case of_fun.(arg1, arg2, arg3) do + {:ok, type, context} -> {type, context} + {:error, context} -> {dynamic(), context} + {type, context} -> {type, context} + end + end + + {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 @@ -156,15 +162,22 @@ 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) + of_fun = fn arg1, arg2, arg3 -> + case of_fun.(arg1, arg2, arg3) do + {:ok, type, context} -> {type, context} + {:error, context} -> {dynamic(), context} + {type, context} -> {type, context} + end end + + # The compiler has already checked the keys are atoms and which ones are required. + {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 +199,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 """ diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index fcc6586c107..9f02622db7c 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -624,12 +624,14 @@ defmodule Module.Types.Pattern do def of_guard({:%, _, [module, {:%{}, _, args}]} = struct, _expected, _expr, stack, context) when is_atom(module) do fun = &of_guard(&1, dynamic(), struct, &2, &3) - Of.struct(struct, module, args, :skip_defaults, stack, context, fun) + {type, context} = Of.struct(struct, module, args, :skip_defaults, stack, context, fun) + {:ok, type, context} end # %{...} def of_guard({:%{}, _meta, args}, _expected, expr, stack, context) do - Of.closed_map(args, stack, context, &of_guard(&1, dynamic(), expr, &2, &3)) + {type, context} = Of.closed_map(args, stack, context, &of_guard(&1, dynamic(), expr, &2, &3)) + {:ok, type, context} end # <<>> From 89849aca45f9bbed98bed66b355562eb37042257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 11:04:16 +0200 Subject: [PATCH 04/11] Fewer ok --- lib/elixir/lib/module/types/expr.ex | 20 +++++++++++--------- lib/elixir/lib/module/types/of.ex | 14 +++++++------- lib/elixir/lib/module/types/pattern.ex | 3 ++- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 65488e4b13b..45047ea3f74 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -308,7 +308,8 @@ defmodule Module.Types.Expr 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) + {type, context} = apply_many(mods, key_or_fun, [], expr, stack, context) + {:ok, type, context} end end end @@ -318,7 +319,8 @@ defmodule Module.Types.Expr 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) + {type, context} = apply_many(mods, name, args_types, expr, stack, context) + {:ok, type, context} end end @@ -464,7 +466,7 @@ defmodule Module.Types.Expr do 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,12 +474,12 @@ 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 diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 5a8008b9af3..8fd19cfee64 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -336,10 +336,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(), error({reason, expr, type, index - 1, context}, meta, stack, context)} + {dynamic(), error({reason, expr, type, index - 1, context}, meta, stack, context)} end end @@ -351,24 +351,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, error(warning, elem(expr, 1), stack, context)} + {result, error(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, error(warning, elem(expr, 1), stack, context)} + {result, error(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 diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 9f02622db7c..9d61d1b3f9f 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -672,7 +672,8 @@ defmodule Module.Types.Pattern do 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) + {type, context} = Of.apply(:erlang, function, args_type, expr, stack, context) + {:ok, type, context} end end From 208936b6d7d726aa8da7ce1ed08fcb2ecdcb025f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 11:05:21 +0200 Subject: [PATCH 05/11] Fewer ok --- lib/elixir/lib/module/types/expr.ex | 3 ++- lib/elixir/lib/module/types/of.ex | 5 ++--- lib/elixir/lib/module/types/pattern.ex | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 45047ea3f74..fb607704a53 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -305,7 +305,8 @@ defmodule Module.Types.Expr do 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) + {type, context} = Of.map_fetch(expr, type, key_or_fun, stack, context) + {:ok, type, context} else {mods, context} = Of.remote(type, key_or_fun, 0, [:dot], expr, meta, stack, context) {type, context} = apply_many(mods, key_or_fun, [], expr, stack, context) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 8fd19cfee64..0233a7181d3 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -79,11 +79,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(), - error({reason, expr, type, field, context}, elem(expr, 1), stack, context)} + {dynamic(), error({reason, expr, type, field, context}, elem(expr, 1), stack, context)} end end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 9d61d1b3f9f..45b9d1032df 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -663,7 +663,8 @@ defmodule Module.Types.Pattern do 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) + {type, context} = Of.map_fetch(map_fetch, type, key, stack, context) + {:ok, type, context} end end From 299cb91cdf38e18acb720e8ab4320f98d92c1ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 11:15:00 +0200 Subject: [PATCH 06/11] No ok in pattern --- lib/elixir/lib/module/types/of.ex | 16 +- lib/elixir/lib/module/types/pattern.ex | 229 ++++++++++++------------- 2 files changed, 111 insertions(+), 134 deletions(-) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 0233a7181d3..510e52a0fc3 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -255,28 +255,24 @@ defmodule Module.Types.Of do type = specifier_type(kind, right) expr = {:<<>>, meta, args} - context = + {_type, context} = case kind do :match -> - case Module.Types.Pattern.of_match_var(left, type, expr, stack, context) do - {:ok, _type, context} -> context - {:error, context} -> context - end + Module.Types.Pattern.of_match_var(left, type, expr, stack, context) :guard -> case Module.Types.Pattern.of_guard(left, type, expr, stack, context) do - {:ok, _type, context} -> context - {:error, context} -> context + {:ok, type, context} -> {type, context} + {:error, context} -> {dynamic(), context} end :expr -> case Module.Types.Expr.of_expr(left, stack, context) do {:ok, actual, context} -> - {_type, context} = intersect(actual, type, expr, stack, context) - context + intersect(actual, type, expr, stack, context) {:error, context} -> - context + {dynamic(), context} end end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 45b9d1032df..deb26279cfe 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -48,9 +48,9 @@ defmodule Module.Types.Pattern do context = %{init_context | pattern_info: {%{}, %{}}, failed: false} changed = :lists.seq(0, length(patterns) - 1) - with {:ok, trees, context} <- - of_pattern_args_index(patterns, expected_types, 0, [], stack, context), - {:ok, types, context} <- + {trees, context} = of_pattern_args_index(patterns, expected_types, 0, [], stack, context) + + with {: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 @@ -66,15 +66,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,10 +110,9 @@ defmodule Module.Types.Pattern do """ def of_match(pattern, expected, expr, stack, init_context) do context = %{init_context | pattern_info: {%{}, %{}}, failed: false} + {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} <- + with {: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} @@ -280,17 +277,15 @@ defmodule Module.Types.Pattern do and binary patterns. """ def of_match_var({:^, _, [var]}, expected, expr, stack, context) do - {type, dynamic} = Of.intersect(Of.var(var, context), expected, expr, stack, context) - {:ok, type, dynamic} + Of.intersect(Of.var(var, context), expected, expr, stack, context) 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 - {type, dynamic} = Of.refine_var(var, expected, expr, stack, context) - {:ok, type, dynamic} + Of.refine_var(var, expected, expr, stack, context) end def of_match_var(ast, expected, expr, stack, context) do @@ -301,23 +296,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 @@ -335,27 +330,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 @@ -365,48 +358,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} - end + {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) - with {:ok, pairs, context} <- result do - pairs = Map.new(pairs) - term = term() - static = [__struct__: atom([struct])] - dynamic = [] + 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} + {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 => value_type} -> + {static, [{field, value_type} | dynamic]} - _ -> - {[{field, term} | static], dynamic} - end - end) + _ -> + {[{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 %_{...} @@ -416,9 +405,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 # %{...} @@ -429,7 +417,7 @@ defmodule Module.Types.Pattern do # <<...>>> defp of_pattern({:<<>>, _meta, args}, _path, stack, context) do context = Of.binary(args, :match, stack, context) - {:ok, binary(), context} + {binary(), context} end # left ++ right @@ -444,12 +432,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 @@ -470,56 +458,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}} + {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) - is_descr(value_type) -> - {:ok, {[{key, value_type} | static], dynamic, context}} + cond do + # Only atom keys become part of the type because the other keys are divisible + not is_atom(key) -> + {static, dynamic, context} - true -> - {:ok, {static, [{key, value_type} | dynamic], context}} - end + 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 @@ -529,30 +512,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) + {suffix, context} = of_pattern(suffix, [:tail | path], stack, context) - 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} + acc = + Enum.reduce(prefix, {[], [], context}, fn arg, {static, dynamic, context} -> + {type, context} = of_pattern(arg, [:head | path], stack, context) - {[], dynamic, context} -> - {:ok, {:non_empty_list, dynamic, suffix}, 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 From 1f14265aed71ef653ef0b9fe5a4440edd5fb563a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 11:20:55 +0200 Subject: [PATCH 07/11] No ok in guards --- lib/elixir/lib/module/types/of.ex | 11 ++-- lib/elixir/lib/module/types/pattern.ex | 72 +++++++++++--------------- 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 510e52a0fc3..0378d6d9e42 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -261,10 +261,7 @@ defmodule Module.Types.Of do Module.Types.Pattern.of_match_var(left, type, expr, stack, context) :guard -> - case Module.Types.Pattern.of_guard(left, type, expr, stack, context) do - {:ok, type, context} -> {type, context} - {:error, context} -> {dynamic(), context} - end + Module.Types.Pattern.of_guard(left, type, expr, stack, context) :expr -> case Module.Types.Expr.of_expr(left, stack, context) do @@ -311,10 +308,8 @@ defmodule Module.Types.Of do 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 diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index deb26279cfe..22a6f998c00 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -33,9 +33,8 @@ defmodule Module.Types.Pattern do 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 + of_pattern_args(patterns, expected_types, stack, context) do + {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) {:ok, types, context} end end @@ -543,45 +542,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_error(expr, expected, atom([atom]), stack, context)} + {dynamic(), 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_error(expr, expected, integer(), stack, context)} + {dynamic(), 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_error(expr, expected, float(), stack, context)} + {dynamic(), 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_error(expr, expected, binary(), stack, context)} + {dynamic(), 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_error(expr, expected, empty_list(), stack, context)} + {dynamic(), Of.incompatible_error(expr, expected, empty_list(), stack, context)} end end @@ -589,11 +588,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} @@ -605,64 +604,55 @@ defmodule Module.Types.Pattern do def of_guard({:%, _, [module, {:%{}, _, args}]} = struct, _expected, _expr, stack, context) when is_atom(module) do fun = &of_guard(&1, dynamic(), struct, &2, &3) - {type, context} = Of.struct(struct, module, args, :skip_defaults, stack, context, fun) - {:ok, type, context} + Of.struct(struct, module, args, :skip_defaults, stack, context, fun) end # %{...} def of_guard({:%{}, _meta, args}, _expected, expr, stack, context) do - {type, context} = Of.closed_map(args, stack, context, &of_guard(&1, dynamic(), expr, &2, &3)) - {:ok, type, context} + Of.closed_map(args, stack, context, &of_guard(&1, dynamic(), expr, &2, &3)) end # <<>> def of_guard({:<<>>, _meta, args}, expected, expr, stack, context) do if binary_type?(expected) do context = Of.binary(args, :guard, stack, context) - {:ok, binary(), context} + {binary(), context} else - {:error, Of.incompatible_error(expr, expected, binary(), stack, context)} + {dynamic(), Of.incompatible_error(expr, expected, binary(), stack, context)} end end # ^var def of_guard({:^, _meta, [var]}, expected, expr, stack, context) do # This is by definition a variable defined outside of this pattern, so we don't track it. - {type, context} = Of.intersect(Of.var(var, context), expected, expr, stack, context) - {:ok, type, context} + Of.intersect(Of.var(var, context), expected, expr, stack, context) end # {...} 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 - {type, context} = Of.map_fetch(map_fetch, type, key, stack, context) - {:ok, type, 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 - {type, context} = Of.apply(:erlang, function, args_type, expr, stack, context) - {:ok, type, 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 def of_guard(var, expected, expr, stack, context) when is_var(var) do - {type, context} = Of.intersect(Of.var(var, context), expected, expr, stack, context) - {:ok, type, context} + Of.intersect(Of.var(var, context), expected, expr, stack, context) end ## Helpers From 9e382c09d67e4c80ebd33f639f9eb470ac9aecf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 11:39:55 +0200 Subject: [PATCH 08/11] No ok in expr --- lib/elixir/lib/module/types.ex | 12 +- lib/elixir/lib/module/types/expr.ex | 375 ++++++++---------- lib/elixir/lib/module/types/helpers.ex | 51 +-- lib/elixir/lib/module/types/of.ex | 42 +- lib/elixir/lib/module/types/pattern.ex | 14 +- .../test/elixir/module/types/type_helper.exs | 27 +- 6 files changed, 210 insertions(+), 311 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index e5fadf4e32f..629a7d3dbf5 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -52,11 +52,13 @@ 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 + case Pattern.of_head(args, guards, meta, stack, context) do + {:ok, _types, context} -> + {_type, context} = Expr.of_expr(body, stack, context) + context.warnings + + {:error, context} -> + context.warnings end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index fb607704a53..a327825c279 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -33,88 +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 context = Of.binary(args, :expr, stack, context) - {:ok, binary(), 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 - {type, context} = Pattern.of_match(left_expr, right_type, expr, stack, context) - {:ok, type, 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 {_args_type, context} = Of.closed_map(args, stack, context, &of_expr/3) - - with {: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 + {_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 | ...} @@ -123,90 +113,79 @@ 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 - {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(), error(__MODULE__, warning, update_meta, stack, context)} - else - # TODO: Merge args_type into map_type with dynamic/static key requirement - {type, context} = - Of.struct(module, args_types, :merge_defaults, struct_meta, stack, context) - - {:ok, type, 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 # %{...} def of_expr({:%{}, _meta, args}, stack, context) do - {type, context} = Of.closed_map(args, stack, context, &of_expr/3) - {:ok, type, context} + Of.closed_map(args, stack, context, &of_expr/3) end # %Struct{} def of_expr({:%, _, [module, {:%{}, _, args}]} = expr, stack, context) do # TODO: We should not skip defaults - {type, context} = Of.struct(expr, module, args, :skip_defaults, stack, context, &of_expr/3) - {:ok, type, context} + Of.struct(expr, module, args, :skip_defaults, stack, context, &of_expr/3) end # () 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] @@ -214,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) @@ -232,97 +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_error(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 - {type, context} = Of.map_fetch(expr, type, key_or_fun, stack, context) - {:ok, type, context} - else - {mods, context} = Of.remote(type, key_or_fun, 0, [:dot], expr, meta, stack, context) - {type, context} = apply_many(mods, key_or_fun, [], expr, stack, context) - {:ok, type, 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) - {type, context} = apply_many(mods, name, args_types, expr, stack, context) - {:ok, type, 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 @@ -332,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 @@ -365,57 +324,56 @@ 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 - {type, context} = Of.struct(exception, args, :merge_defaults, meta, stack, context) - {:ok, type, 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 - {_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 + {_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]) + {_expr_type, context} = of_expr(expr, stack, context) - 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} + case Pattern.of_head([pattern], guards, meta, stack, context) do + {:ok, _, context} -> context + {:error, context} -> context + end end defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]}, stack, context) do - with {:ok, right_type, context} <- of_expr(right, stack, context) do - {_pattern_type, context} = Pattern.of_match(left, binary(), left, stack, context) - - if binary_type?(right_type) do - {:ok, context} - else - warning = {:badbinary, right_type, right, context} - {:ok, error(__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 @@ -432,7 +390,7 @@ defmodule Module.Types.Expr do end defp for_option({:uniq, _}, _stack, context) do - {:ok, context} + context end ## With @@ -440,14 +398,14 @@ 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 + case Pattern.of_head([pattern], guards, meta, stack, context) do + {:ok, _, context} -> + {_expr_type, context} = of_expr(expr, stack, context) + context - defp with_clause(list, stack, context) when is_list(list) do - reduce_ok(list, context, &with_option(&1, stack, &2)) + {:error, context} -> + context + end end defp with_clause(expr, stack, context) do @@ -464,8 +422,6 @@ defmodule Module.Types.Expr do ## General helpers - defp error_type(), do: dynamic() - defp apply_many([], _function, _args_types, _expr, _stack, context) do {dynamic(), context} end @@ -484,12 +440,17 @@ defmodule Module.Types.Expr do 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} + case Pattern.of_head(patterns, guards, meta, stack, context) do + {:ok, _, context} -> + {_, context} = of_expr(body, stack, context) + context + + {:error, context} -> + context + end end) end @@ -513,10 +474,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 fc7ec699c32..19f8f81475f 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -38,52 +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} - - _ -> - IO.inspect({head, fun}) - raise "oops" - end - end - - defp do_map_reduce_ok([], list, acc, _fun), do: {:ok, Enum.reverse(list), acc} - ## Warnings @doc """ @@ -287,4 +241,9 @@ defmodule Module.Types.Helpers do %{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 0378d6d9e42..3ce17bc9eba 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -47,7 +47,7 @@ defmodule Module.Types.Of do # We need to return error otherwise it leads to cascading errors if empty?(new_type) do - {dynamic(), error({:refine_var, old_type, type, var, context}, meta, stack, context)} + {error_type(), error({:refine_var, old_type, type, var, context}, meta, stack, context)} else {new_type, context} end @@ -82,7 +82,7 @@ defmodule Module.Types.Of do {value_type, context} reason -> - {dynamic(), error({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 @@ -90,14 +90,6 @@ defmodule Module.Types.Of do Builds a closed map. """ def closed_map(pairs, extra \\ [], stack, context, of_fun) do - of_fun = fn arg1, arg2, arg3 -> - case of_fun.(arg1, arg2, arg3) do - {:ok, type, context} -> {type, context} - {:error, context} -> {dynamic(), context} - {type, context} -> {type, context} - end - end - {closed?, single, multiple, context} = Enum.reduce(pairs, {true, extra, [], context}, fn {key, value}, {closed?, single, multiple, context} -> @@ -161,14 +153,6 @@ defmodule Module.Types.Of do """ def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun) when is_atom(struct) do - of_fun = fn arg1, arg2, arg3 -> - case of_fun.(arg1, arg2, arg3) do - {:ok, type, context} -> {type, context} - {:error, context} -> {dynamic(), context} - {type, context} -> {type, context} - end - end - # The compiler has already checked the keys are atoms and which ones are required. {args_types, context} = Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> @@ -264,13 +248,8 @@ defmodule Module.Types.Of do Module.Types.Pattern.of_guard(left, type, expr, stack, context) :expr -> - case Module.Types.Expr.of_expr(left, stack, context) do - {:ok, actual, context} -> - intersect(actual, type, expr, stack, context) - - {:error, context} -> - {dynamic(), context} - end + {actual, context} = Module.Types.Expr.of_expr(left, stack, context) + intersect(actual, type, expr, stack, context) end specifier_size(kind, right, expr, stack, context) @@ -298,12 +277,9 @@ 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) do - {_, context} = intersect(actual, integer(), expr, stack, context) - 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) @@ -329,7 +305,7 @@ defmodule Module.Types.Of do {value_type, context} reason -> - {dynamic(), error({reason, expr, type, index - 1, context}, meta, stack, context)} + {error_type(), error({reason, expr, type, index - 1, context}, meta, stack, context)} end end @@ -478,7 +454,7 @@ defmodule Module.Types.Of do type = intersection(actual, expected) if empty?(type) do - {dynamic(), incompatible_error(expr, expected, actual, stack, context)} + {error_type(), incompatible_error(expr, expected, actual, stack, context)} else {type, context} end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 22a6f998c00..8608296d10f 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -119,7 +119,7 @@ defmodule Module.Types.Pattern do end) do {type, merge_failed(init_context, context)} else - {:error, context} -> {dynamic(), context} + {:error, context} -> {error_type(), context} end end @@ -544,7 +544,7 @@ defmodule Module.Types.Pattern do if atom_type?(expected, atom) do {atom([atom]), context} else - {dynamic(), Of.incompatible_error(expr, expected, atom([atom]), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, atom([atom]), stack, context)} end end @@ -553,7 +553,7 @@ defmodule Module.Types.Pattern do if integer_type?(expected) do {integer(), context} else - {dynamic(), Of.incompatible_error(expr, expected, integer(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, integer(), stack, context)} end end @@ -562,7 +562,7 @@ defmodule Module.Types.Pattern do if float_type?(expected) do {float(), context} else - {dynamic(), Of.incompatible_error(expr, expected, float(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, float(), stack, context)} end end @@ -571,7 +571,7 @@ defmodule Module.Types.Pattern do if binary_type?(expected) do {binary(), context} else - {dynamic(), Of.incompatible_error(expr, expected, binary(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, binary(), stack, context)} end end @@ -580,7 +580,7 @@ defmodule Module.Types.Pattern do if empty_list_type?(expected) do {empty_list(), context} else - {dynamic(), Of.incompatible_error(expr, expected, empty_list(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, empty_list(), stack, context)} end end @@ -618,7 +618,7 @@ defmodule Module.Types.Pattern do context = Of.binary(args, :guard, stack, context) {binary(), context} else - {dynamic(), Of.incompatible_error(expr, expected, binary(), stack, context)} + {error_type(), Of.incompatible_error(expr, expected, binary(), stack, context)} end end diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 28b3b0b1128..174a1ada529 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 @@ -129,9 +132,9 @@ 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} + case Pattern.of_head(patterns, guards, [], stack, new_context()) do + {:ok, _types, context} -> Expr.of_expr(body, stack, context) + {:error, context} -> {Module.Types.Descr.dynamic(), context} end end From cc072acaf5b3110533f786a9cff56b4893e460a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 11:57:52 +0200 Subject: [PATCH 09/11] Fix diagnostics --- lib/elixir/lib/module/types/of.ex | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 3ce17bc9eba..cf302cd5a93 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -317,14 +317,14 @@ defmodule Module.Types.Of do match?({false, _}, map_fetch(left, :__struct__)) or match?({false, _}, map_fetch(right, :__struct__)) -> warning = {:struct_comparison, expr, context} - {result, error(warning, elem(expr, 1), stack, context)} + {result, warn(__MODULE__, warning, elem(expr, 1), stack, context)} number_type?(left) and number_type?(right) -> {result, context} disjoint?(left, right) -> warning = {:mismatched_comparison, expr, context} - {result, error(warning, elem(expr, 1), stack, context)} + {result, warn(__MODULE__, warning, elem(expr, 1), stack, context)} true -> {result, context} @@ -375,13 +375,13 @@ defmodule Module.Types.Of do {:ok, mode, :defmacro, reason} -> context = - error(__MODULE__, {:unrequired_module, module, fun, arity}, meta, stack, 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 - error(__MODULE__, {:undefined_module, module, fun, arity}, meta, stack, context) + warn(__MODULE__, {:undefined_module, module, fun, arity}, meta, stack, context) else context end @@ -390,7 +390,7 @@ defmodule Module.Types.Of do if warn_undefined?(module, fun, arity, stack) do exports = ParallelChecker.all_exports(stack.cache, module) payload = {:undefined_function, module, fun, arity, exports} - error(__MODULE__, payload, meta, stack, context) + warn(__MODULE__, payload, meta, stack, context) else context end @@ -399,7 +399,7 @@ defmodule Module.Types.Of do defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do if reason do - error(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) + warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) else context end @@ -409,12 +409,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() - error(__MODULE__, {: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}" - error(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) + warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context) _ -> context From dddb9b698bc4f9989ca5c327921c591297ec4cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 12:18:37 +0200 Subject: [PATCH 10/11] One failure down --- lib/elixir/lib/module/types/expr.ex | 2 +- lib/elixir/lib/module/types/of.ex | 7 +- lib/elixir/lib/module/types/pattern.ex | 150 +++++++++++++------------ 3 files changed, 81 insertions(+), 78 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index a327825c279..5bd9f99371b 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -346,7 +346,7 @@ defmodule Module.Types.Expr do _ -> expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2) formatter = fn expr -> {"rescue #{expr_to_string(expr)} ->", hints} end - {_type, context} = Of.refine_var(var, expected, expr, formatter, stack, context) + {_ok?, _type, context} = Of.refine_var(var, expected, expr, formatter, stack, context) context end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index cf302cd5a93..c34b333336c 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -47,9 +47,10 @@ defmodule Module.Types.Of do # We need to return error otherwise it leads to cascading errors if empty?(new_type) do - {error_type(), error({: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 - {new_type, context} + {:ok, new_type, context} end %{} -> @@ -61,7 +62,7 @@ defmodule Module.Types.Of do } context = put_in(context.vars[version], data) - {type, context} + {:ok, type, context} end end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 8608296d10f..836c2df1441 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -32,29 +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) do - {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) - {: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)) + {:ok, types, context} end defp of_pattern_args([], [], _stack, context) do - {:ok, [], context} + {[], [], context} end - defp of_pattern_args(patterns, expected_types, stack, init_context) do - context = %{init_context | pattern_info: {%{}, %{}}, failed: false} + defp of_pattern_args(patterns, expected_types, stack, context) do + context = %{context | pattern_info: {%{}, %{}}} changed = :lists.seq(0, length(patterns) - 1) {trees, context} = of_pattern_args_index(patterns, expected_types, 0, [], stack, context) - with {: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, merge_failed(init_context, context)} - end + {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( @@ -107,20 +105,18 @@ defmodule Module.Types.Pattern do Return the type and typing context of a pattern expression with the given expected and expr or an error in case of a typing conflict. """ - def of_match(pattern, expected, expr, stack, init_context) do - context = %{init_context | pattern_info: {%{}, %{}}, failed: false} + 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, [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 - {type, merge_failed(init_context, context)} - else - {:error, context} -> {error_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 @@ -128,59 +124,67 @@ 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} - end - - defp of_pattern_recur(_types, _, _vars, _args, _stack, %{failed: true} = context, _callback) do - {:error, 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 - {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} -> - {type, context} = Of.refine_var(var, type, expr, stack, context) - {var_changed? or current_type != type, context} - - :error -> - {var_changed?, Of.incompatible_error(expr, expected, actual, stack, context)} - end - end) - - case var_changed? do - false -> - {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} -> {[index | changed], context} - %{^index => false} -> {changed, context} + 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) + + case var_changed? do + false -> + {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} -> {[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) + {var_changed ++ changed, context} + end + end + end) - # Several changes, we have to recompute all indexes. - _ -> - var_changed = Enum.map(paths, fn [_var, {:arg, index, _, _} | _] -> index end) - {var_changed ++ changed, context} - end - end - end) + of_pattern_recur(types, :lists.usort(changed), vars, args, stack, context, callback) - of_pattern_recur(types, :lists.usort(changed), vars, args, stack, context, callback) + {:error, context} -> + {types, context} end end @@ -284,7 +288,8 @@ defmodule Module.Types.Pattern do 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 @@ -657,9 +662,6 @@ defmodule Module.Types.Pattern do ## Helpers - defp merge_failed(%{failed: false}, %{failed: false} = post), do: post - defp merge_failed(_pre, post), do: %{post | failed: true} - def format_diagnostic({:invalid_pattern, expr, context}) do traces = collect_traces(expr, context) From e1936ec4a512aa116cc96d23d8397f3816391419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Oct 2024 12:22:02 +0200 Subject: [PATCH 11/11] Rewrite done --- lib/elixir/lib/module/types.ex | 11 ++----- lib/elixir/lib/module/types/expr.ex | 29 +++++-------------- lib/elixir/lib/module/types/pattern.ex | 2 +- .../test/elixir/module/types/type_helper.exs | 7 ++--- 4 files changed, 14 insertions(+), 35 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 629a7d3dbf5..8d3a52d7220 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -52,14 +52,9 @@ defmodule Module.Types do end defp warnings_from_clause(meta, args, guards, body, stack, context) do - case Pattern.of_head(args, guards, meta, stack, context) do - {:ok, _types, context} -> - {_type, context} = Expr.of_expr(body, stack, context) - context.warnings - - {: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 diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 5bd9f99371b..663333ba1ea 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -358,11 +358,8 @@ defmodule Module.Types.Expr do defp for_clause({:<-, meta, [left, expr]}, stack, context) do {pattern, guards} = extract_head([left]) {_expr_type, context} = of_expr(expr, stack, context) - - case Pattern.of_head([pattern], guards, meta, stack, context) do - {:ok, _, context} -> context - {:error, context} -> context - end + {[_type], context} = Pattern.of_head([pattern], guards, meta, stack, context) + context end defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]}, stack, context) do @@ -398,14 +395,9 @@ defmodule Module.Types.Expr do defp with_clause({:<-, meta, [left, expr]}, stack, context) do {pattern, guards} = extract_head([left]) - case Pattern.of_head([pattern], guards, meta, stack, context) do - {:ok, _, context} -> - {_expr_type, context} = of_expr(expr, stack, context) - context - - {:error, context} -> - context - end + {[_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 @@ -443,14 +435,9 @@ defmodule Module.Types.Expr do Enum.reduce(clauses, context, fn {:->, meta, [head, body]}, context -> {patterns, guards} = extract_head(head) - case Pattern.of_head(patterns, guards, meta, stack, context) do - {:ok, _, context} -> - {_, context} = of_expr(body, stack, context) - context - - {:error, context} -> - context - end + {_types, context} = Pattern.of_head(patterns, guards, meta, stack, context) + {_, context} = of_expr(body, stack, context) + context end) end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 836c2df1441..9fca122ed7e 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -34,7 +34,7 @@ defmodule Module.Types.Pattern do {_trees, types, context} = of_pattern_args(patterns, expected_types, stack, context) {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) - {:ok, types, context} + {types, context} end defp of_pattern_args([], [], _stack, context) do diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 174a1ada529..4593d4026fb 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -131,11 +131,8 @@ defmodule TypeHelper do def __typecheck__(patterns, guards, body) do stack = new_stack() - - case Pattern.of_head(patterns, guards, [], stack, new_context()) do - {:ok, _types, context} -> Expr.of_expr(body, stack, context) - {:error, context} -> {Module.Types.Descr.dynamic(), 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