diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index c3803470ff1..053a8fd9369 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -2674,6 +2674,84 @@ defmodule Macro do end end + defp dbg_ast_to_debuggable({:with, meta, args} = ast, _env) do + {opts, clauses} = List.pop_at(args, -1) + + acc_var = unique_var(:acc, __MODULE__) + + modified_clauses = + Enum.flat_map(clauses, fn + # We only detail assignments and pattern-matching clauses that + # can be helpful to understand how the result is constructed. + {:<-, _meta, [left, right]} -> + modified_left = + case left do + {:when, meta, [pattern, guard]} -> {:when, meta, [{pattern, quote(do: _)}, guard]} + pattern -> {pattern, quote(do: _)} + end + + quote do + [ + value = unquote(right), + unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)], + unquote(modified_left) <- {value, unquote(acc_var)} + ] + end + + {:=, _meta, [left, right]} -> + quote do + [ + value = unquote(right), + unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)], + unquote(left) = value + ] + end + + # Other expressions like side effects are omitted. + expr -> + [expr] + end) + + modified_opts = + Enum.map(opts, fn + {:do, do_block} -> + {:do, {do_block, acc_var}} + + {:else, else_block} -> + clauses = + Enum.map(else_block, fn + {:->, meta, [[{:when, meta2, [pattern, guard]}], right]} -> + {:->, meta, [[{:when, meta2, [{pattern, acc_var}, guard]}], {right, acc_var}]} + + {:->, meta, [[left], right]} -> + {:->, meta, [[{left, acc_var}], {right, acc_var}]} + + invalid -> + invalid + end) + + error_clause = + quote do + {other, _acc} -> raise WithClauseError, term: other + end + + {:else, clauses ++ error_clause} + + invalid -> + invalid + end) + + modified_with_ast = {:with, meta, modified_clauses ++ [modified_opts]} + + quote do + unquote(acc_var) = [] + + {value, acc} = unquote(modified_with_ast) + + {:with, unquote(escape(ast)), Enum.reverse(acc), value} + end + end + # Any other AST. defp dbg_ast_to_debuggable(ast, _env) do quote do: {:value, unquote(escape(ast)), unquote(ast)} @@ -2828,6 +2906,25 @@ defmodule Macro do {formatted, result} end + defp dbg_format_ast_to_debug({:with, ast, clauses, result}, options) do + formatted_clauses = + Enum.map(clauses, fn {clause_ast, clause_result} -> + dbg_format_ast_with_value(clause_ast, clause_result, options) + end) + + formatted = [ + dbg_maybe_underline("With clauses", options), + ":\n", + formatted_clauses, + ?\n, + dbg_maybe_underline("With expression", options), + ":\n", + dbg_format_ast_with_value(ast, result, options) + ] + + {formatted, result} + end + defp dbg_format_ast_to_debug({:value, code_ast, value}, options) do {dbg_format_ast_with_value(code_ast, value, options), value} end diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index a131caf029c..142968a4780 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -652,6 +652,162 @@ defmodule MacroTest do """ end + test "with with/1 (all clauses match)" do + opts = %{width: 10, height: 15} + + {result, formatted} = + dbg_format( + with {:ok, width} <- Map.fetch(opts, :width), + double_width = width * 2, + IO.puts("just a side effect"), + {:ok, height} <- Map.fetch(opts, :height) do + {:ok, double_width * height} + end + ) + + assert result == {:ok, 300} + + assert formatted =~ "macro_test.exs" + + assert formatted =~ """ + With clauses: + Map.fetch(opts, :width) #=> {:ok, 10} + width * 2 #=> 20 + Map.fetch(opts, :height) #=> {:ok, 15} + + With expression: + with {:ok, width} <- Map.fetch(opts, :width), + double_width = width * 2, + IO.puts("just a side effect"), + {:ok, height} <- Map.fetch(opts, :height) do + {:ok, double_width * height} + end #=> {:ok, 300} + """ + end + + test "with with/1 (no else)" do + opts = %{width: 10} + + {result, formatted} = + dbg_format( + with {:ok, width} <- Map.fetch(opts, :width), + {:ok, height} <- Map.fetch(opts, :height) do + {:ok, width * height} + end + ) + + assert result == :error + + assert formatted =~ "macro_test.exs" + + assert formatted =~ """ + With clauses: + Map.fetch(opts, :width) #=> {:ok, 10} + Map.fetch(opts, :height) #=> :error + + With expression: + with {:ok, width} <- Map.fetch(opts, :width), + {:ok, height} <- Map.fetch(opts, :height) do + {:ok, width * height} + end #=> :error + """ + end + + test "with with/1 (else clause)" do + opts = %{width: 10} + + {result, formatted} = + dbg_format( + with {:ok, width} <- Map.fetch(opts, :width), + {:ok, height} <- Map.fetch(opts, :height) do + width * height + else + :error -> 0 + end + ) + + assert result == 0 + assert formatted =~ "macro_test.exs" + + assert formatted =~ """ + With clauses: + Map.fetch(opts, :width) #=> {:ok, 10} + Map.fetch(opts, :height) #=> :error + + With expression: + with {:ok, width} <- Map.fetch(opts, :width), + {:ok, height} <- Map.fetch(opts, :height) do + width * height + else + :error -> 0 + end #=> 0 + """ + end + + test "with with/1 (guard)" do + opts = %{width: 10, height: 0.0} + + {result, formatted} = + dbg_format( + with {:ok, width} when is_integer(width) <- Map.fetch(opts, :width), + {:ok, height} when is_integer(height) <- Map.fetch(opts, :height) do + width * height + else + _ -> nil + end + ) + + assert result == nil + assert formatted =~ "macro_test.exs" + + assert formatted =~ """ + With clauses: + Map.fetch(opts, :width) #=> {:ok, 10} + Map.fetch(opts, :height) #=> {:ok, 0.0} + + With expression: + with {:ok, width} when is_integer(width) <- Map.fetch(opts, :width), + {:ok, height} when is_integer(height) <- Map.fetch(opts, :height) do + width * height + else + _ -> nil + end #=> nil + """ + end + + test "with with/1 (guard in else)" do + opts = %{} + + {result, _formatted} = + dbg_format( + with {:ok, width} <- Map.fetch(opts, :width) do + width + else + other when is_integer(other) -> :int + other when is_atom(other) -> :atom + end + ) + + assert result == :atom + end + + test "with with/1 respects the WithClauseError" do + value = Enum.random([:unexpected]) + + error = + assert_raise WithClauseError, fn -> + dbg( + with :ok <- value do + true + else + :error -> false + end + ) + end + + assert error.term == :unexpected + end + test "with \"syntax_colors: []\" it doesn't print any color sequences" do {_result, formatted} = dbg_format("hello") refute formatted =~ "\e["