diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index c3803470ff1..d012a499527 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -2674,6 +2674,75 @@ defmodule Macro do end end + defp dbg_ast_to_debuggable({:with, meta, args} = ast, _env) do + {opts, clauses} = List.pop_at(args, -1) + unmatched_ast_var = unique_var(:unmatched_ast, __MODULE__) + unmatched_else_var = unique_var(:unmatched_else, __MODULE__) + result_var = unique_var(:result, __MODULE__) + + modified_clauses = + Enum.flat_map(clauses, fn + {:<-, _meta, [left, right]} = clause -> + quote do + [ + code = unquote(Macro.escape(clause)), + value = unquote(right), + unquote(:<-)({{code, value}, unquote(left)}, {{code, value}, value}) + ] + end + + expr -> + [expr] + end) + + modified_opts = + case opts do + [do: do_code] -> + else_code = + quote do + [ + unquote(:->)( + [{unquote(unmatched_ast_var), unquote(result_var)}], + {:__else__, unquote(unmatched_ast_var), unquote(result_var)} + ) + ] + end + + [do: {:__matched__, do_code}, else: else_code] + + [do: do_code, else: else_code] -> + else_code = + Enum.map(else_code, fn + {:->, _meta, [[left], right]} -> + quote do + unquote(:->)( + [{unquote(unmatched_ast_var), unquote(left)}], + {:__else__, unquote(unmatched_ast_var), unquote(right)} + ) + end + end) + + final_else_catch_all = + quote do + unquote(:->)( + [{unquote(unmatched_ast_var), unquote(unmatched_else_var)}], + {:__else__, unquote(unmatched_ast_var), + raise(WithClauseError, term: unquote(unmatched_else_var))} + ) + end + + [do: {:__matched__, do_code}, else: else_code ++ [final_else_catch_all]] + end + + modified = modified_clauses ++ [modified_opts] + modified_with_ast = {:with, meta, modified} + + quote do + value = unquote(modified_with_ast) + {:with, unquote(Macro.escape(ast)), [], value} + end + end + # Any other AST. defp dbg_ast_to_debuggable(ast, _env) do quote do: {:value, unquote(escape(ast)), unquote(ast)} @@ -2828,6 +2897,34 @@ defmodule Macro do {formatted, result} end + defp dbg_format_ast_to_debug({:with, ast, [], {:__matched__, value}}, options) do + formatted = [ + dbg_maybe_underline("With clause", options), + " (all clauses matched, do block evaluated):\n", + dbg_maybe_underline("With expression", options), + ":\n", + dbg_format_ast_with_value(ast, value, options) + ] + + {formatted, value} + end + + defp dbg_format_ast_to_debug( + {:with, ast, [], {:__else__, {clause_ast, clause_value}, value}}, + options + ) do + formatted = [ + dbg_maybe_underline("With clause unmatched on", options), + ":\n", + dbg_format_ast_with_value(clause_ast, clause_value, options), + dbg_maybe_underline("With expression", options), + ":\n", + dbg_format_ast_with_value(ast, value, options) + ] + + {formatted, value} + 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..125580ebe9d 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -652,6 +652,107 @@ 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 clause (all clauses matched, do block evaluated): + 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 clause unmatched on: + {:ok, height} <- 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 clause unmatched on: + {:ok, height} <- 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 (else clause unmatched)" do + assert_raise( + WithClauseError, + "no with clause matching: :not_found", + fn -> + dbg( + with :ok <- :not_found do + :ok + else + :error -> :error + end + ) + end + ) + end + test "with \"syntax_colors: []\" it doesn't print any color sequences" do {_result, formatted} = dbg_format("hello") refute formatted =~ "\e["