diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index c266adcd4e..d1bbc1403d 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5873,11 +5873,15 @@ defmodule Kernel do end @doc """ - Defines a macro suitable for use in guard expressions. + Defines a custom guard with the given name. - It raises at compile time if the `guard` uses expressions that aren't - allowed in [guard clauses](patterns-and-guards.html#guards), - and otherwise creates a macro that can be used both inside or outside guards. + Once defined, custom guards can be invoked within regular code or in + guards. The module that contains the custom guard must be required before usage. + + Custom guards are defined by providing a valid guard expression to + the right-hand side of `when`. `defguard` will then expand and validate + the expressions as guards. `defguard` will raise at compile time if the + guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards). When defining your own guards, consider the [naming conventions](naming-conventions.html#is_-prefix-is_foo) @@ -5885,31 +5889,30 @@ defmodule Kernel do ## Example + For example, to define a guard similar to `Integer.is_even/1`, you can write: + defmodule Integer.Guards do defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 end - defmodule Collatz do - @moduledoc "Tools for working with the Collatz sequence." - import Integer.Guards + which can then be used as: - @doc "Determines the number of steps `n` takes to reach `1`." - # If this function never converges, please let me know what `n` you used. - def converge(n) when n > 0, do: step(n, 0) + require Integer.Guards + Integer.Guards.is_even(3) + #=> false - defp step(1, step_count) do - step_count - end + ## Implementation details - defp step(n, step_count) when is_even(n) do - step(div(n, 2), step_count + 1) - end + Behind the scenes, `defguard` will generate a macro which can be used + inside and outside of guards, preserving their respective semantics. - defp step(n, step_count) do - step(3 * n + 1, step_count + 1) - end - end + When invoked inside a guard, it behaves as if the right-hand side of + `when` is injected as part of the guard, replacing the custom guard + arguments by the expressions given as inputs. + When invoked outside of a guard, it preserves regular function calling + semantics with one caveat: all arguments are evaluated before invocation, + except arguments which are unused, which are then never evaluated. """ @doc since: "1.6.0" @spec defguard(Macro.t()) :: Macro.t() diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index d3f9ae3cad..8d6099ac34 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -339,7 +339,7 @@ defmodule Kernel.Utils do unquote(literal_quote(unquote_every_ref(expr, vars), [])) false -> - unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true)) + unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true)) end end end @@ -369,7 +369,9 @@ defmodule Kernel.Utils do end # Prefaces `guard` with unquoted versions of `refs`. - defp unquote_refs_once(guard, refs, module) do + defp unquote_refs_once(guard, refs, %{module: module} = env) do + env = %{env | context: nil} + {guard, used_refs} = Macro.postwalk(guard, %{}, fn {ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) -> @@ -391,6 +393,12 @@ defmodule Kernel.Utils do {var, acc} end + {{:., dot_meta, [:erlang, :orelse]}, meta, [left, right]}, acc -> + {Macro.expand({{:., dot_meta, [Kernel, :or]}, meta, [left, right]}, env), acc} + + {{:., dot_meta, [:erlang, :andalso]}, meta, [left, right]}, acc -> + {Macro.expand({{:., dot_meta, [Kernel, :and]}, meta, [left, right]}, env), acc} + node, acc -> {node, acc} end) diff --git a/lib/elixir/test/elixir/kernel/guard_test.exs b/lib/elixir/test/elixir/kernel/guard_test.exs index 546c789614..7cd704faae 100644 --- a/lib/elixir/test/elixir/kernel/guard_test.exs +++ b/lib/elixir/test/elixir/kernel/guard_test.exs @@ -483,6 +483,44 @@ defmodule Kernel.GuardTest do """ end + defguard with_or_and_or(foo, bar, baz) when foo or (bar and (baz or foo)) + + test "expands conditionals" do + args = quote(do: [1 + 1, 2 + 2, 3 + 3]) + + assert expand_defguard_to_string(:with_or_and_or, args, :guard) == """ + :erlang.orelse(1 + 1, :erlang.andalso(2 + 2, :erlang.orelse(3 + 3, 1 + 1))) + """ + + assert expand_defguard_to_string(:with_or_and_or, args, nil) == """ + {arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3} + + case arg1 do + false -> + case arg2 do + false -> + false + + true -> + case arg3 do + false -> arg1 + true -> true + other -> :erlang.error({:badbool, :or, other}) + end + + other -> + :erlang.error({:badbool, :and, other}) + end + + true -> + true + + other -> + :erlang.error({:badbool, :or, other}) + end + """ + end + defguard in_list(a) when Kernel.in(a, [:test]) test "expands remote functions" do