Skip to content

Instantly share code, notes, and snippets.

@christhekeele
Last active October 21, 2023 10:09
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christhekeele/76c3e37cb9082274f52f79fa94bab6fe to your computer and use it in GitHub Desktop.
Save christhekeele/76c3e37cb9082274f52f79fa94bab6fe to your computer and use it in GitHub Desktop.
Elixir 1.5 `Kernel.defguard/1` implementation effort notes, concerning a fun learning process
# None of this code represents the finished version, but it does show
# the evolution and learning process of the polished implementation:
# https://github.com/elixir-lang/elixir/pull/5854
defmodule Guard do
# These would go in Kernel
@doc """
Makes a macro suitable for use in guard expressions.
It raises at compile time if the definition uses expressions that aren't
allowed in guards, and otherwise creates a macro that can be used both inside
or outside guards, as per the requirements of `Macro.guard/3`.
## Example
defmodule Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end
defmodule IntegerUtils do
import Guards
def even_guard(value) when is_even(value), do: true
def even_guard(_), do: false
def even_func(value) do
if is_even(value), do: true, else: false
end
end
"""
defmacro defguard(guard) do
# `:elixir_utils.extract_guards` is new in 1.5.
# it can be done without it, but this is nicer
case :elixir_utils.extract_guards(guard) do
{_, []} -> raise ArgumentError, message: "defguard expects to define guard of format `name(args) when implementation`"
{definition, implementation} -> do_defguard definition, implementation, __CALLER__
end
end
defp do_defguard(definition, implementation, env) do
case validate_guard(implementation, env) do
:ok ->
vars = extract_varnames(elem(definition, 2))
quote do
defmacro unquote(definition) do
unquote(guard(implementation, vars))
end
end
{:error, remainder} ->
raise ArgumentError, "not allowed in guard expression: `#{Macro.to_string(remainder)}`"
end
end
defp extract_varnames(ast) do
{_ast, vars} = Macro.prewalk ast, [], fn
{token, _, atom} = ast, acc when is_atom(atom) -> {ast, [token | acc]}
ast, acc -> {ast, acc}
end
vars
end
# The rest would go under Macro
# The implementation for Macro.guard isn't too bad
@doc """
Rewrites an expression so it can be used both inside and outside a guard.
Take, for example, the expression:
```elixir
is_integer(value) and rem(value, 2) == 0
```
If we wanted to create a macro, `is_even`, from this expression, that could be
used in guards, we'd have to take several things into account.
First, if this expression is being used inside a guard, `value` needs to be
unquoted each place it occurs, since it has not yet been at that point in our
macro.
Secondly, if the expression is being used outside of a guard, we want to unquote
`value`––but only once, and then re-use the unquoted form throughout the expression.
This helper does exactly that: takes the AST for an expression and a list of
variable names it should be aware of, and rewrites it into a new expression that
checks for its presence in a guard, then unquotes the variable references as
appropriate.
The resulting transformation looks something like this:
> expr = quote do: is_integer(value) and rem(value, 2) == 0
> vars = [:value]
> Macro.to_string(Macro.guard(expr, vars))
case Macro.Env.in_guard? env do
true -> quote do
is_integer(unquote(value)) and rem(unquote(value), 2) == 0
end
false -> quote do
value = unquote(value)
is_integer(value) and rem(value, 2) == 0
end
end
Note that this function does nothing to ensure that the expression itself is
appropriate for use in a guard, it just rewrites the expression. To ensure that
the expression is suitable, first run it through `validate_guard/2`.
"""
@spec guard(Macro.t, list(Atom.t)) :: Macro.t | no_return
def guard(expr, vars) do
quote do
case Macro.Env.in_guard?(var!(:__CALLER__)) do
true -> unquote(quote_ast guard_quotation(expr, vars, in_guard: true))
false -> unquote(quote_ast guard_quotation(expr, vars, in_guard: false))
end
end
end
# Finds every reference to `vars` in `expr` and wraps them in an unquote.
defp guard_quotation(expr, vars, in_guard: true) do
Macro.postwalk expr, fn
{ token, meta, atom } when is_atom(atom) ->
case token in vars do
true -> unquote_ast({ token, meta, atom })
false -> { token, meta, atom }
end
node -> node
end
end
# Prefaces `expr` with unquoted versions of `vars`.
defp guard_quotation(expr, vars, in_guard: false) do
for var <- vars, ref = Macro.var(var, nil) do
quote do: unquote(ref) = unquote(unquote_ast(ref))
end ++ List.wrap(expr)
end
# It's hard to generate ast when the ast must be quoted, unquoted into,
# and the ast needs to contain quotes/unquotes. These can be used to insert
# quotes/unquotes into the final ast that won't be processed as a directive.
defp quote_ast(ast) do
{ :quote, [], [[do: { :__block__, [], List.wrap(ast)} ]] }
end
defp unquote_ast(ast) do
{ :unquote, [], List.wrap(ast) }
end
# Here's the real sticking point, though: guard detection.
# José mentioned:
# > On the positive side, the expansion can be done by calling Macro.traverse + Macro.expand.
# > Then you can use erl_internal to check if all entries are valid guard expressions or not.
# But it is more complicated than this.
# To build up to the issue, let's try re-implementing the existing `Macro.validate/1` in terms of
# a `Macro.validate/2` that allows us to pass in a custom validation function, so we can check for
# general macro validity in the same breath as our own custom critera without walking the tree
# multiple times.
@doc """
Validates that the given expressions are valid quoted expressions.
Checks the `t:Macro.t/0` for the specification of a valid
quoted expression.
It returns `:ok` if the expression is valid. Otherwise it returns a tuple in the form of
`{:error, remainder}` where `remainder` is the invalid part of the quoted expression.
## Examples
iex> Macro.validate({:two_element, :tuple})
:ok
iex> Macro.validate({:three, :element, :tuple})
{:error, {:three, :element, :tuple}}
iex> Macro.validate([1, 2, 3])
:ok
iex> Macro.validate([1, 2, 3, {4}])
{:error, {4}}
"""
@spec validate(term) :: :ok | {:error, term}
def validate(expr), do: do_validate(expr, false)
@doc """
Validates that the given expressions are valid, and satisfy a custom `validator` function.
The `validator` must accept any AST and return truthy if it is valid, or falsey otherwise.
This `validator` check is evaluated before the the AST itself or its contents are judged
to represent valid Elixir code.
## Examples
iex> Macro.validate({:two_element, :tuple}, &(&1!=1))
:ok
iex> Macro.validate({:three, :element, :tuple}, &(&1!=1))
{:error, {:three, :element, :tuple}}
iex> Macro.validate({:has_one, 1}, &(&1!=1))
{:error, 1}
iex> Macro.validate([2, 3], &(&1!=1))
:ok
iex> Macro.validate([2, 3, {4}], &(&1!=1))
{:error, {4}}
iex> Macro.validate([1, 2, 3], &(&1!=1))
{:error, 1}
"""
@spec validate(term, (term -> {term | nil})) :: :ok | {:error, term}
def validate(expr, validator) when is_function(validator), do: do_validate(expr, validator)
defp do_validate(expr, valid), do: find_invalid(expr, valid) || :ok
defp find_invalid(expr = {left, right}, valid), do:
is_valid?(expr, valid || true) || find_invalid(left, valid) || find_invalid(right, valid)
defp find_invalid(expr = {left, meta, right}, valid) when is_list(meta) and (is_atom(right) or is_list(right)), do:
is_valid?(expr, valid || true) || find_invalid(left, valid) || find_invalid(right, valid)
defp find_invalid(list, valid) when is_list(list), do:
is_valid?(list, valid || true) || Enum.find_value(list, &(find_invalid(&1, valid)))
defp find_invalid(pid, valid) when is_pid(pid), do: is_valid?(pid, valid || true) || nil
defp find_invalid(atm, valid) when is_atom(atm), do: is_valid?(atm, valid || true) || nil
defp find_invalid(num, valid) when is_number(num), do: is_valid?(num, valid || true) || nil
defp find_invalid(bin, valid) when is_binary(bin), do: is_valid?(bin, valid || true) || nil
defp find_invalid(fun, valid) when is_function(fun) do
unless :erlang.fun_info(fun, :env) == {:env, []} and
:erlang.fun_info(fun, :type) == {:type, :external} do
is_valid?(fun, valid || true)
end
end
defp find_invalid(ast, valid) do
is_valid?(ast, valid)
end
defp is_valid?(ast, valid) when is_function(valid) do
unless valid.(ast) do
{:error, ast}
end
end
defp is_valid?(ast, valid) when is_boolean(valid) do
unless valid do
{:error, ast}
end
end
# This gives us the tools to implement `Macro.validate_guard/2`, which will take
# ast and an env to expand it in.
@doc """
Expands a guard `expr` in `env` and determines if it is valid.
"""
@spec validate_guard(Macro.t, Macro.Env.t) :: :ok | {:error, term}
def validate_guard(expr, env) do
expr |> expand_guard(env) |> validate_guard
end
# `Macro.validate_guard/1` just assumes a sufficiently expanded ast as not to need an env,
# and leverages a custom validator to `Macro.validate/1` to determine guard eligiblity.
@doc """
Determines if an expanded guard `expr` is valid.
Returns `:ok` if valid, otherwise returns `{:error, subexprs}` where `subexprs` is
the segments of the `expr` that are not allowed in guards.
"""
@spec validate_guard(Macro.t) :: :ok | {:error, [term]}
def validate_guard(expr) do
validate expr, &guard_validity_check/1
end
# So the question becomes: what do we have to do to expand an ast such that
# we can walk each node as if in `prewalk` and determine if any of them wouldn't work
# in a guard expression?
# Expands a guard `expr` in `env`
@spec expand_guard(Macro.t, Macro.Env.t) :: Macro.t
defp expand_guard(expr, env) do
# ???
end
# That bit's actually pretty easy. This final piece isn't.
# In the real implementation it will probably jsut be an anon fun
# in `validate_guard/` instead of being referenced as `&guard_validity_check/1`.
# Determines if an ast segment is eligable in a guard
@spec guard_validity_check(Macro.t) :: boolean
defp guard_validity_check(ast) do
# ???
end
# I go into this more in the markdown file. But first some simple tests.
end
defmodule GuardTest do
# First off, ensure that our re-implementation of Macro.validate/1 still passes.
test "validate" do
ref = make_ref()
assert Guard.validate(1) == :ok
assert Guard.validate(1.0) == :ok
assert Guard.validate(:foo) == :ok
assert Guard.validate("bar") == :ok
assert Guard.validate(<<0::8>>) == :ok
assert Guard.validate(self()) == :ok
assert Guard.validate({1, 2}) == :ok
assert Guard.validate({:foo, [], :baz}) == :ok
assert Guard.validate({:foo, [], []}) == :ok
assert Guard.validate([1, 2, 3]) == :ok
assert Guard.validate(<<0::4>>) == {:error, <<0::4>>}
assert Guard.validate(ref) == {:error, ref}
assert Guard.validate({1, ref}) == {:error, ref}
assert Guard.validate({ref, 2}) == {:error, ref}
assert Guard.validate([1, ref, 3]) == {:error, ref}
assert Guard.validate({:foo, [], 0}) == {:error, {:foo, [], 0}}
assert Guard.validate({:foo, 0, []}) == {:error, {:foo, 0, []}}
end
# Next: some high-level specs.
# I really should have documented more cases that caused my code to break
# during experimentation. I should flesh these out before continuing.
# Any contributions to them to help me hone in on the correct behaviour are
# wildly appreciated!
defmodule Remote do
def call, do: :ok
end
# We'll come back to this first one in particular.
test "`is_record(data)` is valid in guards" do
import Record
guard = Macro.expand (quote do: is_record(foo)), __ENV__
assert :ok == Macro.validate_guard(guard)
end
test "`1 + 1` is valid in guards" do
guard = Macro.expand (quote do: 1 + 1), __ENV__
assert :ok == Macro.validate_guard(guard)
end
test "`is_integer(1)` is valid in guards" do
guard = Macro.expand (quote do: is_integer(1)), __ENV__
assert :ok == Macro.validate_guard(guard)
end
test "`abs(-1)` is valid in guards" do
guard = Macro.expand (quote do: abs(-1)), __ENV__
assert :ok == Macro.validate_guard(guard)
end
test "`self()` is valid in guards" do
guard = Macro.expand (quote do: self()), __ENV__
assert :ok == Macro.validate_guard(guard)
end
test "`send(self, :data)` is not valid in guards" do
bad_guard = Macro.expand (quote do: send(self, :data)), __ENV__
refute :ok == Macro.validate_guard(bad_guard)
end
test "`Remote.call()` is not valid in guards" do
bad_guard = Macro.expand (quote do: Remote.call()), __ENV__
refute :ok == Macro.validate_guard(bad_guard)
end
test "`alias Supervisor.Spec` is not valid in guards" do
bad_guard = Macro.expand (quote do: alias Supervisor.Spec), __ENV__
refute :ok == Macro.validate_guard(bad_guard)
end
test "`import Map` is not valid in guards" do
bad_guard = Macro.expand (quote do: import Map), __ENV__
refute :ok == Macro.validate_guard(bad_guard)
end
end

Currently the implementation of is_record(data) is the original instance of code in Elixir core that we want defguard to generate, so it should be a good thing to tackle first. If we can use is_record(data) in a custom guard generated by defguard we're pretty much done. In fact, we can already generate the correct AST just fine in Macro.guard/2, but we need to raise errors if we happen to know the expansion of is_record(data) isn't valid for use in guards. To do this we need to implement the blank functions above:

  • Macro.expand_guard(Macro.t, Macro.Env.t) :: Macro.t to expand it until it can be validated
  • Macro.guard_validity_check(Macro.t) :: boolean to walk through the expanded ast making sure it all looks good.

Ideally, the implementations will be in terms of calls to :erlang and :elixir so that it evolves with the languages. That is, we want to ask erlang and Elixir what's allowed in a guard, instead of just telling the user what's allowed in guards.

This is non-trivial, especially the second one. So we'll do the expansion first.

Expansion

No expansion

Let's convince ourselves that we can't progress any further as is. We'll force the calling env to report as actually Macro.Env.in_guard?, but ignore it for the raw ast.

expand_guard = fn (ast, _env) -> ast end
import Record
env = Map.put(__ENV__, :context, :guard)

expand_guard.((quote do: is_record(data)), env)
#=> {:is_record, [context: Elixir, import: Record], [{:data, [], Elixir}]}

IO.puts Macro.to_string expand_guard.((quote do: is_record(data)), env)
#=> is_record(data)

Obviously we lack the information about is_record to write a guard_validity_check function.

Full expansion

Our options for expansion are:

  • Macro.expand_once(ast, env)
  • Macro.expand(ast, env)
  • Macro.prewalk(ast, &(Macro.expand &1, env))

The first will handle a top level AST transformations: imports, aliases, the like. The second will iteratively perform all possible transformation on the top-level piece of AST. The final one will do that, recursively, through the whole AST. This is the one we want.

expand_guard = fn (ast, env) -> Macro.prewalk(ast, &(Macro.expand &1, env)) end
import Record
env = Map.put(__ENV__, :context, :guard)

expand_guard.((quote do: is_record(data)), env) 
#=> {{:., [], [:erlang, :andalso]}, [],
#=>  [{{:., [], [:erlang, :andalso]}, [],
#=>    [{:is_tuple, [context: Record, import: Kernel], [{:data, [], Elixir}]},
#=>     {:>, [context: Record, import: Kernel],
#=>      [{:tuple_size, [context: Record, import: Kernel], [{:data, [], Elixir}]}, 0]}]},
#=>   {:is_atom, [context: Record, import: Kernel],
#=>    [{:elem, [context: Record, import: Kernel], [{:data, [], Elixir}, 0]}]}]}
   
IO.puts Macro.to_string expand_guard.((quote do: is_record(data)), env)
#=> :erlang.andalso(:erlang.andalso(is_tuple(data), tuple_size(data) > 0), is_atom(elem(data, 0)))

That looks fun! And it gives us our penultimate piece of the puzzle: how to expand the guard.

  @doc false
  # Expands a guard `expr` in `env` for evaluation in `Macro.validate_guard/1`.
  @spec expand_guard(Macro.t, Macro.Env.t) :: Macro.t
  defp expand_guard(expr, env) do
    Macro.prewalk(ast, &(Macro.expand &1, Map.put(env, :context, :guard)))
  end

But how do we tell if it's production ready? What is our Guard.guard_validity_check(Macro.t) :: bool? How can we walk through each segment of the ast and know if it's valid, before someone tries to use our guard?

Validation

From here on out we'll assume that the variable full_ast is set to:

import Record
full_ast = Macro.prewalk((quote do: Record.is_record(data)), &(Macro.expand &1, Map.put(__ENV__, :context, :guard)))
#=> {{:., [], [:erlang, :andalso]}, [],
#=>  [{{:., [], [:erlang, :andalso]}, [],
#=>    [{:is_tuple, [context: Record, import: Kernel], [{:data, [], Elixir}]},
#=>     {:>, [context: Record, import: Kernel],
#=>      [{:tuple_size, [context: Record, import: Kernel], [{:data, [], Elixir}]}, 0]}]},
#=>   {:is_atom, [context: Record, import: Kernel],
#=>    [{:elem, [context: Record, import: Kernel], [{:data, [], Elixir}, 0]}]}]}

We can see what the Erlang Expressions guide says is valid in guards:

  • The atom true
  • Other constants (terms and bound variables), all regarded as false
  • Calls to the BIFs specified in table Type Test BIFs below
  • Term comparisons
  • Arithmetic expressions
  • Boolean expressions
  • Short-circuit expressions (andalso/orelse)

Lets build up a series of function clauses that will tell if any piece of AST fits these categories.

Terms

These are said to be valid in guards:

  • The atom true
  • Other constants (terms and bound variables), all regarded as false

We can isolate terms through our Type Test BIFs and what we know about the AST: anything that isn't a tuple in a Macro.prewalk should be good.

  • term when not is_tuple(term) -> true
Variables

That doesn't cover variables, though.

  • Other constants (terms and bound variables), all regarded as false

When we generate our macro through Guard.guard/2 we're effectively eliminating unbound variables, that's the whole point of this. Any other unbound variables will show up as issue when that macro is compiled. That leaves us with bound variables. These are easy to cover, take our data example in full_ast from is_record(data):

  • {:data, [], Elixir}
  • {name, meta, atom} when is_atom(atom)

For our purposes any remaining bound variable reference is valid in a guard, even if the compiler chokes on it later:

  • {_, _, atom} when is_atom(atom) -> true
Remote calls

Let's look at our root expression in full_ast next:

  • {{:., [], [:erlang, :andalso]}, [], [...]}
  • {{:., dotmeta, [module, fun]}, meta, args} when is_list(args)

How do we decide if a remote call is valid? In a guard, they generally aren't, except for ones to :erlang mentioned in the Erlang Expressions guide. So we can target a subset of remote calls:

  • {{:., _, [:erlang, call]}, _, args} when is_list(args)

Frustratingly, :erl_internal almost gives us all the tools we need to evaluate these.

:erl_internal.op_type will help us find the candidates mentioned here:

The docs fail to mention the list expressions we can find in :erl_internal.list_op/2. And even the documentation for :erl_internal doesn't make it clear that there's a 5th op category, send operations, which are not allowed in guards.

Even with all that, we haven't covered the example from full_ast, which :erl_internal can't help us with:

  • Short-circuit expressions (andalso/orelse)

As it turns out, this is already taken care of for us in :elixir_utils.guard_op/2:

guard_op('andalso', 2) ->
  true;
guard_op('orelse', 2) ->
  true;
guard_op(Op, Arity) ->
  try erl_internal:op_type(Op, Arity) of
    arith -> true;
    list  -> true;
    comp  -> true;
    bool  -> true;
    send  -> false
  catch
    _:_ -> false
  end.

This leaves just one category left from the guard expressions docs not accounted for, which we can use :erl_internal.guard_bif/2 for:

This gives us two clauses, one to determine if a remote call is valid:

  • {{:., _, [:erlang, call]}, _, args} when is_list(args) -> :erl_internal.guard_bif(call, length(args)) or :elixir_utils.guard_op(call, length(args))

And one to let remote call heads through successfully, since their validity is determined at a higher level:

  • {:., _, [:erlang, _call]} -> true
Completion

We now have a full specification of what erlang knows is good in guards:

  @doc """
  Determines if an expanded guard `expr` is valid.
  """
  @spec validate_guard(Macro.t) :: :ok | {:error, term}
  def validate_guard(expr) do
    validate expr, fn
    # handle good remote calls to :erlang
      {{:., _, [:erlang, call]}, _, args} when is_list(args)
        -> :erl_internal.guard_bif(call, length(args)) or :elixir_utils.guard_op(call, length(args))
    # and their constituent call subexprs, if they're not valid they'll be caught during the walk above
      {:., _, [:erlang, _call]}
        -> true
    # let variables fly, the compiler will complain after this is all over if they're really no good
      {_, _, atom} when is_atom(atom)
        -> true
    # let general terms through for further iterations
      term when not is_tuple(term)
        -> true
    # anything else is invalid
      _ -> false
    end
  end

We should be finished, right? Not quite.

Rewriting

So far things are going pretty well. We're dipping into internal :erl_internal and :elixir_utils, but really that's a good thing--if any of our assumptions about AST change, they will be kept up to date as these modules will change upstream. No sketchy hard-coded AST checks, no error trying and catching! But there's still a big issue.

Even though we've expanded our AST, and encoded whether or not each element is valid in guards, most elements of our test case fail:

import Record
full_ast = Macro.prewalk((quote do: is_record(data)), &(Macro.expand &1, Map.put(__ENV__, :context, :guard)))
#=> {{:., [], [:erlang, :andalso]}, [],
#=>  [{{:., [], [:erlang, :andalso]}, [],
#=>    [{:is_tuple, [context: Record, import: Kernel], [{:data, [], Elixir}]},
#=>     {:>, [context: Record, import: Kernel],
#=>      [{:tuple_size, [context: Record, import: Kernel], [{:data, [], Elixir}]}, 0]}]},
#=>   {:is_atom, [context: Record, import: Kernel],
#=>    [{:elem, [context: Record, import: Kernel], [{:data, [], Elixir}, 0]}]}]}

Many of these terms are technically not valid according to erlang (ex. Elixir.Kernel.is_tuple/2) because the Elixir compiler will rewrite that under the covers into :erlang.is_tuple/2, which is pretty slick, but makes it hard to tell in userland if they are acceptible.

What we need is a way to expand these rewrites, so we need to teach Macro.expand/2 a new trick. Let's start by adding a flag to the expand family that will let us tell Elixir to handle rewrites:

module Guard do
  @doc """
  Macro.expand, with the option expand rewrites a la `rewrite: true`
  """
  def expand(tree, env, opts \\ [])

  def expand(tree, env, opts) do
    expand_until({tree, true}, env, opts)
  end

  defp expand_until({tree, true}, env, opts) do
    expand_until(do_expand_once(tree, env, opts), env, opts)
  end

  defp expand_until({tree, false}, _env, _opts) do
    tree
  end
  def expand_once(ast, env, opts \\ [])

  def expand_once(ast, env, opts) do
    elem(do_expand_once(ast, env, opts), 0)
  end
end

Again, we'll steal the tests from Macro.expand to ensure what we're doing doesn't break the previous behaviour.

I'm going to drop out of the markdown format and back into code files. First I'l post this updated spec file, then add the full and final code-commented version of our Guard spec.

You can sroll down to the bottom of zzz_guard.ex until you see

##############
# Resume here!
##############

This is where we will address our implementation of defp Guard.do_expand_one(Macro.t, Macro.Env.t, Keyword.t) :: Macro.t.

Code.require_file "test_helper.exs", __DIR__
defmodule Guard.ExternalTest do
defmacro external do
line = 18
file = __ENV__.file
^line = __CALLER__.line
^file = __CALLER__.file
^line = Macro.Env.location(__CALLER__)[:line]
^file = Macro.Env.location(__CALLER__)[:file]
end
defmacro oror(left, right) do
quote do: unquote(left) || unquote(right)
end
end
defmodule Guard.Remote do
def call, do: :ok
end
defmodule GuardTest do
use ExUnit.Case, async: true
# First off, ensure that our re-implementation of Macro.validate/1 still passes.
test "validate" do
ref = make_ref()
assert Guard.validate(1) == :ok
assert Guard.validate(1.0) == :ok
assert Guard.validate(:foo) == :ok
assert Guard.validate("bar") == :ok
assert Guard.validate(<<0::8>>) == :ok
assert Guard.validate(self()) == :ok
assert Guard.validate({1, 2}) == :ok
assert Guard.validate({:foo, [], :baz}) == :ok
assert Guard.validate({:foo, [], []}) == :ok
assert Guard.validate([1, 2, 3]) == :ok
assert Guard.validate(<<0::4>>) == {:error, <<0::4>>}
assert Guard.validate(ref) == {:error, ref}
assert Guard.validate({1, ref}) == {:error, ref}
assert Guard.validate({ref, 2}) == {:error, ref}
assert Guard.validate([1, ref, 3]) == {:error, ref}
assert Guard.validate({:foo, [], 0}) == {:error, {:foo, [], 0}}
assert Guard.validate({:foo, 0, []}) == {:error, {:foo, 0, []}}
end
# Also, ensure that our re-implementaion of Macro.expand/1 still passes.
import Guard.ExternalTest
test "expand once" do
assert {:||, _, _} = Guard.expand_once(quote(do: oror(1, false)), __ENV__)
end
test "expand once with raw atom" do
assert Guard.expand_once(quote(do: :foo), __ENV__) == :foo
end
test "expand once with current module" do
assert Guard.expand_once(quote(do: __MODULE__), __ENV__) == __MODULE__
end
test "expand once with main" do
assert Guard.expand_once(quote(do: Elixir), __ENV__) == Elixir
end
test "expand once with simple alias" do
assert Guard.expand_once(quote(do: Foo), __ENV__) == Foo
end
test "expand once with current module plus alias" do
assert Guard.expand_once(quote(do: __MODULE__.Foo), __ENV__) == __MODULE__.Foo
end
test "expand once with main plus alias" do
assert Guard.expand_once(quote(do: Elixir.Foo), __ENV__) == Foo
end
test "expand once with custom alias" do
alias Foo, as: Bar
assert Guard.expand_once(quote(do: Bar.Baz), __ENV__) == Foo.Baz
end
test "expand once with main plus custom alias" do
alias Foo, as: Bar, warn: false
assert Guard.expand_once(quote(do: Elixir.Bar.Baz), __ENV__) == Elixir.Bar.Baz
end
test "expand once with op" do
assert Guard.expand_once(quote(do: Foo.bar.Baz), __ENV__) == (quote do
Foo.bar.Baz
end)
end
test "expand once with Erlang" do
assert Guard.expand_once(quote(do: :foo), __ENV__) == :foo
end
test "expand once env" do
env = %{__ENV__ | line: 0}
assert Guard.expand_once(quote(do: __ENV__), env) == {:%{}, [], Map.to_list(env)}
assert Guard.expand_once(quote(do: __ENV__.file), env) == env.file
assert Guard.expand_once(quote(do: __ENV__.unknown), env) == quote(do: __ENV__.unknown)
end
defmacro local_macro do
:local_macro
end
test "expand once local macro" do
assert Guard.expand_once(quote(do: local_macro), __ENV__) == :local_macro
end
test "expand once checks vars" do
local_macro = 1
assert local_macro == 1
quote = {:local_macro, [], nil}
assert Guard.expand_once(quote, __ENV__) == quote
end
defp expand_once_and_clean(quoted, env) do
cleaner = &Keyword.drop(&1, [:counter])
quoted
|> Guard.expand_once(env)
|> Macro.prewalk(&Macro.update_meta(&1, cleaner))
end
test "expand once with imported macro" do
temp_var = {:x, [], Kernel}
assert expand_once_and_clean(quote(do: 1 || false), __ENV__) == (quote context: Kernel do
case 1 do
unquote(temp_var) when unquote(temp_var) in [false, nil] -> false
unquote(temp_var) -> unquote(temp_var)
end
end)
end
test "expand once with require macro" do
temp_var = {:x, [], Kernel}
assert expand_once_and_clean(quote(do: Kernel.||(1, false)), __ENV__) == (quote context: Kernel do
case 1 do
unquote(temp_var) when unquote(temp_var) in [false, nil] -> false
unquote(temp_var) -> unquote(temp_var)
end
end)
end
test "expand once with not expandable expression" do
expr = quote(do: other(1, 2, 3))
assert Guard.expand_once(expr, __ENV__) == expr
end
test "expand once does not expand module attributes" do
message = "could not call get_attribute on module #{inspect(__MODULE__)} " <>
"because it was already compiled"
assert_raise ArgumentError, message, fn ->
Guard.expand_once(quote(do: @foo), __ENV__)
end
end
defp expand_and_clean(quoted, env) do
cleaner = &Keyword.drop(&1, [:counter])
quoted
|> Guard.expand(env)
|> Macro.prewalk(&Macro.update_meta(&1, cleaner))
end
test "expand" do
temp_var = {:x, [], Kernel}
assert expand_and_clean(quote(do: oror(1, false)), __ENV__) == (quote context: Kernel do
case 1 do
unquote(temp_var) when unquote(temp_var) in [false, nil] -> false
unquote(temp_var) -> unquote(temp_var)
end
end)
end
defp local_call(), do: :ok
# Now for actually testing the our implementation.
# This is the tricky one!
test "`is_record(data)` is valid in guards" do
import Record
env = Map.put(__ENV__, :context, :guard)
guard = Guard.expand (quote do: is_record(foo)), env
assert :ok == Guard.validate_guard(guard, env)
end
test "`1 + 1` is valid in guards" do
env = __ENV__
guard = Guard.expand (quote do: 1 + 1), env
assert :ok == Guard.validate_guard(guard, env)
end
test "`is_integer(1)` is valid in guards" do
env = __ENV__
guard = Guard.expand (quote do: is_integer(1)), env
assert :ok == Guard.validate_guard(guard, env)
end
test "`abs(-1)` is valid in guards" do
env = __ENV__
guard = Guard.expand (quote do: abs(-1)), env
assert :ok == Guard.validate_guard(guard, env)
end
test "`self()` is valid in guards" do
env = __ENV__
guard = Guard.expand (quote do: self()), env
assert :ok == Guard.validate_guard(guard, env)
end
test "`send(self, :data)` is not valid in guards" do
env = __ENV__
bad_guard = Guard.expand (quote do: send(self, :data)), env
refute :ok == Guard.validate_guard(bad_guard, env)
end
test "`Remote.call()` is not valid in guards" do
env = __ENV__
bad_guard = Guard.expand (quote do: Guard.Remote.call()), env
refute :ok == Guard.validate_guard(bad_guard, env)
end
test "`local_call()` is not valid in guards" do
env = __ENV__
bad_guard = Guard.expand (quote do: local_call()), env
refute :ok == Guard.validate_guard(bad_guard, env)
end
test "`alias Supervisor.Spec` is not valid in guards" do
env = __ENV__
bad_guard = Guard.expand (quote do: alias Supervisor.Spec), env
refute :ok == Guard.validate_guard(bad_guard, env)
end
test "`import Map` is not valid in guards" do
env = __ENV__
bad_guard = Guard.expand (quote do: import Map), env
refute :ok == Guard.validate_guard(bad_guard, env)
end
end
defmodule Guard do
# These would go in Kernel
@doc """
Makes a macro suitable for use in guard expressions.
It raises at compile time if the definition uses expressions that aren't
allowed in guards, and otherwise creates a macro that can be used both inside
or outside guards, as per the requirements of `Macro.guard/3`.
## Example
defmodule Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end
defmodule IntegerUtils do
import Guards
def even_guard(value) when is_even(value), do: true
def even_guard(_), do: false
def even_func(value) do
if is_even(value), do: true, else: false
end
end
"""
defmacro defguard(guard) do
# `:elixir_utils.extract_guards` is new in 1.5.
# it can be done without it, but this is nicer
case :elixir_utils.extract_guards(guard) do
{_, []} -> raise ArgumentError, message: "defguard expects to define guard of format `name(args) when implementation`"
{definition, implementation} -> do_defguard definition, implementation, __CALLER__
end
end
defp do_defguard(definition, implementation, env) do
case validate_guard(implementation, env) do
:ok ->
vars = extract_varnames(elem(definition, 2))
quote do
defmacro unquote(definition) do
unquote(guard(implementation, vars))
end
end
{:error, remainder} ->
raise ArgumentError, "not allowed in guard expression: `#{Macro.to_string(remainder)}`"
end
end
defp extract_varnames(ast) do
{_ast, vars} = Macro.prewalk ast, [], fn
{token, _, atom} = ast, acc when is_atom(atom) -> {ast, [token | acc]}
ast, acc -> {ast, acc}
end
vars
end
# The rest would go under Macro
# The implementation for Macro.guard isn't too bad
@doc """
Rewrites an expression so it can be used both inside and outside a guard.
Take, for example, the expression:
```elixir
is_integer(value) and rem(value, 2) == 0
```
If we wanted to create a macro, `is_even`, from this expression, that could be
used in guards, we'd have to take several things into account.
First, if this expression is being used inside a guard, `value` needs to be
unquoted each place it occurs, since it has not yet been at that point in our
macro.
Secondly, if the expression is being used outside of a guard, we want to unquote
`value`––but only once, and then re-use the unquoted form throughout the expression.
This helper does exactly that: takes the AST for an expression and a list of
variable names it should be aware of, and rewrites it into a new expression that
checks for its presence in a guard, then unquotes the variable references as
appropriate.
The resulting transformation looks something like this:
> expr = quote do: is_integer(value) and rem(value, 2) == 0
> vars = [:value]
> Macro.to_string(Macro.guard(expr, vars))
case Macro.Env.in_guard? env do
true -> quote do
is_integer(unquote(value)) and rem(unquote(value), 2) == 0
end
false -> quote do
value = unquote(value)
is_integer(value) and rem(value, 2) == 0
end
end
Note that this function does nothing to ensure that the expression itself is
appropriate for use in a guard, it just rewrites the expression. To ensure that
the expression is suitable, first run it through `validate_guard/2`.
"""
@spec guard(Macro.t, list(Atom.t)) :: Macro.t | no_return
def guard(expr, vars) do
quote do
case Macro.Env.in_guard?(var!(:__CALLER__)) do
true -> unquote(quote_ast guard_quotation(expr, vars, in_guard: true))
false -> unquote(quote_ast guard_quotation(expr, vars, in_guard: false))
end
end
end
# Finds every reference to `vars` in `expr` and wraps them in an unquote.
defp guard_quotation(expr, vars, in_guard: true) do
Macro.postwalk expr, fn
{ token, meta, atom } when is_atom(atom) ->
case token in vars do
true -> unquote_ast({ token, meta, atom })
false -> { token, meta, atom }
end
node -> node
end
end
# Prefaces `expr` with unquoted versions of `vars`.
defp guard_quotation(expr, vars, in_guard: false) do
for var <- vars, ref = Macro.var(var, nil) do
quote do: unquote(ref) = unquote(unquote_ast(ref))
end ++ List.wrap(expr)
end
# It's hard to generate ast when the ast must be quoted, unquoted into,
# and the ast needs to contain quotes/unquotes. These can be used to insert
# quotes/unquotes into the final ast that won't be processed as a directive.
defp quote_ast(ast) do
{ :quote, [], [[do: { :__block__, [], List.wrap(ast)} ]] }
end
defp unquote_ast(ast) do
{ :unquote, [], List.wrap(ast) }
end
# Here's the real sticking point, though: guard detection.
# José mentioned:
# > On the positive side, the expansion can be done by calling Macro.traverse + Macro.expand.
# > Then you can use erl_internal to check if all entries are valid guard expressions or not.
# But it is more complicated than this.
# To build up to the issue, let's try re-implementing the existing `Macro.validate/1` in terms of
# a `Macro.validate/2` that allows us to pass in a custom validation function, so we can check for
# general macro validity in the same breath as our own custom critera without walking the tree
# multiple times.
@doc """
Validates that the given expressions are valid quoted expressions.
Checks the `t:Macro.t/0` for the specification of a valid
quoted expression.
It returns `:ok` if the expression is valid. Otherwise it returns a tuple in the form of
`{:error, remainder}` where `remainder` is the invalid part of the quoted expression.
## Examples
iex> Macro.validate({:two_element, :tuple})
:ok
iex> Macro.validate({:three, :element, :tuple})
{:error, {:three, :element, :tuple}}
iex> Macro.validate([1, 2, 3])
:ok
iex> Macro.validate([1, 2, 3, {4}])
{:error, {4}}
"""
@spec validate(term) :: :ok | {:error, term}
def validate(expr), do: do_validate(expr, false)
@doc """
Validates that the given expressions are valid, and satisfy a custom `validator` function.
The `validator` must accept any AST and return truthy if it is valid, or falsey otherwise.
This `validator` check is evaluated before the the AST itself or its contents are judged
to represent valid Elixir code.
## Examples
iex> Macro.validate({:two_element, :tuple}, &(&1!=1))
:ok
iex> Macro.validate({:three, :element, :tuple}, &(&1!=1))
{:error, {:three, :element, :tuple}}
iex> Macro.validate({:has_one, 1}, &(&1!=1))
{:error, 1}
iex> Macro.validate([2, 3], &(&1!=1))
:ok
iex> Macro.validate([2, 3, {4}], &(&1!=1))
{:error, {4}}
iex> Macro.validate([1, 2, 3], &(&1!=1))
{:error, 1}
"""
@spec validate(term, (term -> {term | nil})) :: :ok | {:error, term}
def validate(expr, validator) when is_function(validator), do: do_validate(expr, validator)
defp do_validate(expr, valid), do: find_invalid(expr, valid) || :ok
defp find_invalid(expr = {left, right}, valid), do:
is_valid?(expr, valid || true) || find_invalid(left, valid) || find_invalid(right, valid)
defp find_invalid(expr = {left, meta, right}, valid) when is_list(meta) and (is_atom(right) or is_list(right)), do:
is_valid?(expr, valid || true) || find_invalid(left, valid) || find_invalid(right, valid)
defp find_invalid(list, valid) when is_list(list), do:
is_valid?(list, valid || true) || Enum.find_value(list, &(find_invalid(&1, valid)))
defp find_invalid(pid, valid) when is_pid(pid), do: is_valid?(pid, valid || true) || nil
defp find_invalid(atm, valid) when is_atom(atm), do: is_valid?(atm, valid || true) || nil
defp find_invalid(num, valid) when is_number(num), do: is_valid?(num, valid || true) || nil
defp find_invalid(bin, valid) when is_binary(bin), do: is_valid?(bin, valid || true) || nil
defp find_invalid(fun, valid) when is_function(fun) do
unless :erlang.fun_info(fun, :env) == {:env, []} and
:erlang.fun_info(fun, :type) == {:type, :external} do
is_valid?(fun, valid || true)
end
end
defp find_invalid(ast, valid) do
is_valid?(ast, valid)
end
defp is_valid?(ast, valid) when is_function(valid) do
unless valid.(ast) do
{:error, ast}
end
end
defp is_valid?(ast, valid) when is_boolean(valid) do
unless valid do
{:error, ast}
end
end
# This gives us the tools to implement `Macro.validate_guard/2`, which will take
# ast and an env to expand it in.
@doc """
Expands a guard `expr` in `env` and determines if it is valid.
"""
@spec validate_guard(Macro.t, Macro.Env.t) :: :ok | {:error, term}
def validate_guard(expr, env) do
expr |> expand_guard(env) |> validate_guard
end
# So the question becomes: what do we have to expand an ast such that
# we can walk each node and determine if any of them wouldn't work
# in a guard expression?
@doc false
# Expands a guard `expr` in `env`.
@spec expand_guard(Macro.t, Macro.Env.t) :: Macro.t
defp expand_guard(expr, env) do
env = Map.put(env, :context, :guard)
Macro.prewalk expr, fn ast ->
Guard.expand(ast, env, rewrite: true)
end
end
# `Macro.validate_guard/1` just assumes a sufficiently expanded ast as not to need an env,
# and leverages a custom validator to `Macro.validate/1` to determine guard eligiblity.
@doc """
Determines if an expanded guard `expr` is valid.
"""
@spec validate_guard(Macro.t) :: :ok | {:error, term}
def validate_guard(expr) do
validate expr, fn
# handle good remote calls to :erlang
{{:., _, [:erlang, call]}, _, args} when is_list(args)
-> :erl_internal.guard_bif(call, length(args)) or :elixir_utils.guard_op(call, length(args))
# and their constituent call subexprs, if they're not valid they'll be caught during the walk above
{:., _, [:erlang, _call]}
-> true
# let variables fly, the compiler will complain after this is all over if they're really no good
{_, _, atom} when is_atom(atom)
-> true
# let general terms through for further iterations
term when not is_tuple(term)
-> true
# anything else is invalid
_ -> false
end
end
# Yep, we're going to re-implement Macro.expand.
# We want to add a custom flag to it that causes it to peek into rewrites that the Elixir compiler
# will do, and make it a part of the expansion.
@doc """
Receives an AST node and expands it until it can no longer
be expanded.
This function uses `expand_once/2` under the hood. Check
it out for more information and examples.
"""
def expand(tree, env, opts \\ [])
def expand(tree, env, opts) do
expand_until({tree, true}, env, opts)
end
defp expand_until({tree, true}, env, opts) do
expand_until(do_expand_once(tree, env, opts), env, opts)
end
defp expand_until({tree, false}, _env, _opts) do
tree
end
@doc """
Receives an AST node and expands it once.
The following contents are expanded:
* Macros (local or remote)
* Aliases are expanded (if possible) and return atoms
* Compilation environment macros (`__ENV__/0`, `__MODULE__/0` and `__DIR__/0`)
* Module attributes reader (`@foo`)
If the expression cannot be expanded, it returns the expression
itself. Notice that `expand_once/2` performs the expansion just
once and it is not recursive. Check `expand/2` for expansion
until the node can no longer be expanded.
Passing in the option `rewrite: true` will cause it to also epxand any rewrites that
the Elixir compiler would normally apply.
"""
def expand_once(ast, env, opts \\ [])
def expand_once(ast, env, opts) do
elem(do_expand_once(ast, env, opts), 0)
end
##############
# Resume here!
##############
# We're going to leave most of the original implementation in place.
# Expand aliases
defp do_expand_once({:__aliases__, _, _} = original, env, opts) do
case :elixir_aliases.expand(original, env.aliases, env.macro_aliases, env.lexical_tracker) do
receiver when is_atom(receiver) ->
:elixir_lexical.record_remote(receiver, env.function, env.lexical_tracker)
{receiver, true}
aliases ->
aliases = :lists.map(&elem(do_expand_once(&1, env, opts), 0), aliases)
case :lists.all(&is_atom/1, aliases) do
true ->
receiver = :elixir_aliases.concat(aliases)
:elixir_lexical.record_remote(receiver, env.function, env.lexical_tracker)
{receiver, true}
false ->
{original, false}
end
end
end
# Expand compilation environment macros
defp do_expand_once({:__MODULE__, _, atom}, env, _opts) when is_atom(atom),
do: {env.module, true}
defp do_expand_once({:__DIR__, _, atom}, env, _opts) when is_atom(atom),
do: {:filename.dirname(env.file), true}
defp do_expand_once({:__ENV__, _, atom}, env, _opts) when is_atom(atom),
do: {{:%{}, [], Map.to_list(env)}, true}
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env, _opts) when
is_atom(atom) and is_atom(field) do
if Map.has_key?(env, field) do
{Map.get(env, field), true}
else
{original, false}
end
end
# Expand possible macro import invocation
defp do_expand_once({atom, meta, context} = original, env, opts)
when is_atom(atom) and is_list(meta) and is_atom(context) do
if :lists.member({atom, Keyword.get(meta, :counter, context)}, env.vars) do
{original, false}
else
case do_expand_once({atom, meta, []}, env, opts) do
{_, true} = exp -> exp
{_, false} -> {original, false}
end
end
end
# Anything else is just returned
defp do_expand_once(other, _env, _opts), do: {other, false}
# Here's where the changes start.
# The only change we're making to this clause is the addition of `do_linify(receiver, quoted)`.
# This is because we will be calling it later on a few times.
# Expand possible macro require invocation
defp do_expand_once({{:., _dotmeta, [left, right]}, meta, args} = original, env, opts) when is_atom(right) do
{receiver, _} = do_expand_once(left, env, opts)
case is_atom(receiver) do
false -> {original, false}
true ->
expand = :elixir_dispatch.expand_require(meta, receiver, {right, length(args)}, args, env)
case expand do
{:ok, receiver, quoted} ->
do_linify(receiver, quoted)
:error ->
{original, false}
end
end
end
# Minor helper function addition.
defp do_linify(receiver, quoted) do
next = :erlang.unique_integer()
{:elixir_quote.linify_with_context_counter(0, {receiver, next}, quoted), true}
end
# Here's the final clause: expanding the general call syntax of `{atom, meta, args}`.
# And here's the meat of the issue: how can we get the normal call implementation to perform
# the rewrites in `:elixir_rewrite`?
# First, for reference, the original implementation, with the new `opts` passed in to it
# but unused, and the new do_linify() helper applied.
#
# defp do_expand_once({atom, meta, args} = original, env, opts)
# when is_atom(atom) and is_list(args) and is_list(meta) do
# arity = length(args)
#
# if :elixir_import.special_form(atom, arity) do
# {original, false}
# else
# module = env.module
# extra = if function_exported?(module, :__info__, 1) do
# [{module, module.__info__(:macros)}]
# else
# []
# end
#
# expand = :elixir_dispatch.expand_import(meta, {atom, length(args)}, args,
# env, extra, true)
#
# case expand do
# {:ok, receiver, quoted} ->
# do_linify(receiver, quoted)
# {:ok, _receiver, _name, _args} ->
# {original, false}
# :error ->
# {original, false}
# end
# end
# end
#
# We need to perform rewrites in the very last case statement, namely in the first two clauses that
# represent fully de-aliased, de-imported, non-special-form function calls. We need the opportunity
# to rewrite these expressions and pass them back through `do_expand_once` if they were rewritten.
#
# Our case statement will become:
# case expand do
# {:ok, receiver, quoted} ->
# do_expand_call(receiver, quoted, linify = true, env, opts)
# {:ok, receiver, name, args} ->
# do_expand_call(receiver, {name, [], args}, linify = false, env, opts)
# :error ->
# {original, false}
# end
defp do_expand_once({atom, meta, args} = original, env, opts)
when is_atom(atom) and is_list(args) and is_list(meta) do
arity = length(args)
if :elixir_import.special_form(atom, arity) do
{original, false}
else
module = env.module
extra = if function_exported?(module, :__info__, 1) do
[{module, module.__info__(:macros)}]
else
[]
end
expand = :elixir_dispatch.expand_import(meta, {atom, length(args)}, args, env, extra, true)
case expand do
{:ok, receiver, quoted} ->
do_expand_call(receiver, quoted, linify = true, env, opts)
{:ok, receiver, name, args} ->
do_expand_call(receiver, {name, [], args}, linify = false, env, opts)
:error ->
{original, false}
end
end
end
# Okay, now let's look at how to expand those calls, while preserving the behaviour of the
# origin case statement if `Keyword.get(opts, :rewrite)` if falsey.
# First off, we can only expand the `quoted` ast if it matches `{call, meta, args}`.
# If it does, and :rewrite is true, we can go to town on it with `do_rewrite_call`.
# Otherwise we need to figure out how to continue as planned.
defp do_expand_call(receiver, {call, meta, args}, linify, env, opts) do
if Keyword.get(opts, :rewrite) do
do_rewrite_call(receiver, {call, meta, args}, env, opts)
else
do_maybe_linify(receiver, {call, meta, args}, linify)
end
end
# Any AST that doesn't match {call, meta, args} should continue as planned.
defp do_expand_call(receiver, original, linify, _opts) do
do_maybe_linify(receiver, original, linify)
end
# Silly little linify helpers.
defp do_maybe_linify(receiver, original, _linify = true) do
do_linify(receiver, original)
end
defp do_maybe_linify(_, original, _linify = false) do
{original, false}
end
# And we've arrived: we've inserted an opportunity to rewrite calls into expansion.
# So how should we proceed? There are two exported functions in :elixir_rewrite we can make use of.
defp do_rewrite_call(receiver, {call, meta, args} = original, env, opts) do
# `:elixir_rewrite.inline` does simple module/fun rewrites.
case :elixir_rewrite.inline(receiver, call, length(args)) do
# For example, if it tells us to convert `List.to_atom/1` into `:erlang.list_to_atom`,
# we will perform the rewrite and convert this call into the inlined one, and send it
# back through do_expand_once since we have opted to replace the user's original call
# into a new one and haven't addressed the underlying question of how to expand that
# new call yet.
{receiver, call} ->
rewritten = {{:., [], [receiver, call]}, meta, args}
do_expand_once(rewritten, env, opts)
# If it returns false, we proceed onto the trickier `:elixir_rewrite.rewrite`
false ->
# `:elixir_rewrite.rewrite` does more involved rewrites that require eliminating
# dead code, or performing inline rewrites that require massaging the arguments
# into a new form.
case :elixir_rewrite.rewrite(receiver, [], call, meta, args) do
# If it gave us the same call back again, we abort the rewrite early and
# terminate the expansion with with the original.
{{:., _dotmeta, [^receiver, ^call]}, _meta, _args} ->
{original, false}
# If it gives us a new call, we can perform the rewrite and send it back through expansion.
{{:., _dotmeta, [_receiver, _call]}, _meta, _args} = rewritten ->
do_expand_once(rewritten, env, opts)
# If it gives us something else, it's performed some other transformation we'll honor.
other ->
{other, true}
end
end
end
# That concludes our updates to our Guard module. The actual version presents these function clauses
# in the appropriate order (rather than the narrative one).
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment