Expand aliases inside quote

This commit is contained in:
José Valim
2013-01-11 09:55:38 +01:00
parent eaa9714b3a
commit 4e651c8aae
14 changed files with 210 additions and 131 deletions
+3 -1
View File
@@ -31,7 +31,9 @@
-record(elixir_quote, {
line=0,
marker=nil,
var_context=nil,
expand_aliases=true,
expand_imports=true,
unquote=true
}).
-7
View File
@@ -282,13 +282,6 @@ defmodule Kernel do
"""
defmacro left !== right
@doc """
When used inside quoting, marks that the variable should not
be hygienezed. Check `Kernel.SpecialForms.quote/1` for more
information.
"""
defmacro var!(var)
@doc """
Returns an integer or float which is the arithmetical absolute value of `number`.
+74 -21
View File
@@ -328,11 +328,11 @@ defmodule Kernel.SpecialForms do
quote do: sum(1, 2, 3)
#=> { :sum, 0, [1, 2, 3] }
## Homoiconicity
## Explanation
Elixir is an homoiconic language. Any Elixir program can be
represented using its own data structures. The building block
of Elixir homoiconicity is a tuple with three elements, for example:
Any Elixir code can be represented using Elixir data structures.
The building block of Elixir homoiconicity is a tuple with three
elements, for example:
{ :sum, 1, [1, 2, 3] }
@@ -344,8 +344,18 @@ defmodule Kernel.SpecialForms do
* The second element of the tuple is always an integer
representing the line number;
* The third element of the tuple are the arguments for the
function call. The third argument may be an atom, meaning
that it may be a variable.
function call. The third argument may be an atom, which is
usually a variable (or a local call);
## Options
* `:hygiene` - When false, disables hygiene for variables, aliases and imports;
* `:unquote` - When false, disables unquoting. Useful when you have a quote
inside another quote and want to control which quote is
able to unquote;
* `:location` - When set to `:keep`, keeps the current line and file on quotes.
Read the Stacktrace information section below for more information;
* `:expand_aliases` - When false, do not expand aliases;
## Macro literals
@@ -361,9 +371,17 @@ defmodule Kernel.SpecialForms do
## Hygiene
Elixir macros are hygienic regarding to variables. This means
a variable defined in a macro cannot affect the scope where
the macro is included. Consider the following example:
Elixir macros are hygienic. This means aliases and imports
defined inside the quoted often refer to the context that
defined the macro.
Furthermore, variables inside quote are also hygienic. That
said, a variable defined in a macro cannot affect the scope
where the macro is included.
### Hygiene in variables
Consider the following example:
defmodule Hygiene do
defmacro no_interference do
@@ -395,22 +413,38 @@ defmodule Kernel.SpecialForms do
NoHygiene.interference
a #=> 1
Notice that aliases are not hygienic in Elixir, ambiguity
must be solved by prepending Elixir:
### Hygiene in aliases
quote do
Elixir.Foo #=> Access the root Foo
Foo #=> Access the Foo alias in the current module
(if any is set), then fallback to Elixir.Foo
Aliases inside quote are expanded by default.
Consider the following example:
defmodule Hygiene do
alias OrdDict, as: D
defmacro no_interference do
quote do: D.new
end
end
## Options
require Hygiene
Hygiene.no_interference #=> { OrdDict, [] }
* `:hygiene` - When false, disables hygiene;
* `:unquote` - When false, disables unquoting. Useful when you have a quote
inside another quote and want to control which quote is able to unquote;
* `:location` - When set to `:keep`, keeps the current line and file on quotes.
Read the Stacktrace information section below for more information;
Notice that, even though the alias `D` is not available
in the context the macro is expanded, the code above works
because `D` was expanded when the quote was generated.
There are two ways to disable this behaviour. By giving
the `expand_aliases` equals to false to quote or by using
the `alias!` macro inside the quote:
defmodule NoHygiene do
defmacro interference do
quote do: alias!(D).new
end
end
require NoHygiene
NoHygiene.interference #=> UndefinedFunctionError
## Stacktrace information
@@ -446,6 +480,25 @@ defmodule Kernel.SpecialForms do
"""
defmacro quote(opts, do: contents)
@doc """
When used inside quoting, marks that the variable should not
be hygienezed. Check `quote/2` for more information.
"""
defmacro var!(var)
@doc """
Defines a variable in the given context.
Check `quote/2` for more information.
"""
defmacro var!(var, context)
@doc """
When used inside quoting, marks that the alias should not
be hygienezed. This means the alias will be expanded when
the macro is expanded.
"""
defmacro alias!(alias)
@doc """
Unquotes the given expression from inside a macro.
+9 -26
View File
@@ -442,28 +442,16 @@ defmodule Macro do
"""
def expand(aliases, env)
# The first case we handle is __aliases__. In case
# aliases just contain one item, we are sure it is
# an atom, so we just expand it based on the aliases
# dict.
def expand({ :__aliases__, _, [h] }, env) when h != Elixir do
expand_alias(h, env)
end
def expand({ :__aliases__, _, _ } = original, env) do
case :elixir_aliases.expand(original, env.aliases) do
atom when is_atom(atom) -> atom
aliases ->
aliases = lc alias inlist aliases, do: expand(alias, env)
# In case aliases contains more than one item, we need
# to loop them checking if they are all atoms or not.
# Macros and pseudo-variables are then expanded.
def expand({ :__aliases__, _, [h|t] } = original, env) do
aliases = case h do
x when is_atom(x) and x != Elixir -> [expand_alias(x, env)|t]
_ -> [h|t]
end
aliases = lc alias inlist aliases, do: expand(alias, env)
case :lists.all(is_atom(&1), aliases) do
true -> :elixir_aliases.concat(aliases)
false -> original
case :lists.all(is_atom(&1), aliases) do
true -> :elixir_aliases.concat(aliases)
false -> original
end
end
end
@@ -524,11 +512,6 @@ defmodule Macro do
:lists.any(match?({ :&, _, [_] }, &1), args)
end
defp expand_alias(h, env) do
atom = list_to_atom('Elixir-' ++ atom_to_list(h))
:elixir_aliases.lookup(atom, env.aliases)
end
@doc """
Recurs the quoted expression checking if all sub terms are
safe (i.e. they represented data structured and don't actually
+20 -1
View File
@@ -1,10 +1,29 @@
-module(elixir_aliases).
-export([first/1, last/1, concat/1, safe_concat/1, lookup/2,
format_error/1, ensure_loaded/3]).
format_error/1, ensure_loaded/3, expand/2]).
-include("elixir.hrl").
-compile({parse_transform, elixir_transform}).
%% Expand an alias returning an atom if possible.
%% Otherwise returns the list of aliases args.
expand({ '__aliases__', _Line, [H] }, Aliases) when H /= 'Elixir' ->
expand_alias(H, Aliases);
expand({ '__aliases__', _Line, [H|T] }, Aliases) when is_atom(H) ->
concat(case H of
'Elixir' -> [H|T];
_ -> [expand_alias(H, Aliases)|T]
end);
expand({ '__aliases__', _Line, List }, _Aliases) ->
List.
expand_alias(H, Aliases) ->
lookup(list_to_atom("Elixir-" ++ atom_to_list(H)), Aliases).
%% Ensure a module is loaded before its usage.
ensure_loaded(_Line, 'Elixir.Kernel', _S) ->
ok;
-2
View File
@@ -372,7 +372,5 @@ in_erlang_macros() ->
{'or',2},
{'receive',1},
{'try',1},
{'var!',1},
{'var!',2},
{'xor',2}
].
+4 -1
View File
@@ -318,5 +318,8 @@ non_overridable_macros() ->
{'super','*'},
{'super?',0},
{'bc','*'},
{'lc','*'}
{'lc','*'},
{'var!',1},
{'var!',2},
{'alias!',1}
].
+8 -24
View File
@@ -232,10 +232,10 @@ translate({Kind, Line, [Call, Expr]}, S) when ?FUNS(Kind) ->
assert_no_aliases_name(Line, Name, Args, S),
TName = elixir_tree_helpers:abstract_syntax(Name),
TArgs = elixir_tree_helpers:abstract_syntax(Args),
TGuards = elixir_tree_helpers:abstract_syntax(Guards),
TExpr = elixir_tree_helpers:abstract_syntax(Expr),
TName = elixir_tree_helpers:abstract_syntax(Name),
TArgs = elixir_tree_helpers:abstract_syntax(Args),
TGuards = elixir_tree_helpers:abstract_syntax(Guards),
TExpr = elixir_tree_helpers:abstract_syntax(Expr),
{ elixir_def:wrap_definition(Kind, Line, TName, TArgs, TGuards, TExpr, S), S };
@@ -257,23 +257,7 @@ translate({ apply, Line, [Left, Right, Args] }, S) when is_list(Args) ->
translate({ apply, Line, Args }, S) ->
{ TArgs, NS } = translate_args(Args, S),
{ ?ELIXIR_WRAP_CALL(Line, erlang, apply, TArgs), NS };
%% Handle forced variables
translate({ 'var!', Line, [Arg] }, S) ->
translate({ 'var!', Line, [Arg, nil] }, S);
translate({ 'var!', Line, [{Name, _, Atom}, Kind] }, S) when is_atom(Name), is_atom(Atom) ->
Expanded = elixir_scope:expand_var_context(Line, Kind, "invalid second argument for var!", S),
elixir_scope:translate_var(Line, Name, Expanded, S);
translate({ 'var!', Line, [Name, Kind] }, S) when is_atom(Name) ->
Expanded = elixir_scope:expand_var_context(Line, Kind, "invalid second argument for var!", S),
elixir_scope:translate_var(Line, Name, Expanded, S);
translate({ 'var!', Line, [_, _] }, S) ->
syntax_error(Line, S#elixir_scope.file, "invalid first argument for var!, expected a var or an atom").
{ ?ELIXIR_WRAP_CALL(Line, erlang, apply, TArgs), NS }.
%% Helpers
@@ -344,13 +328,13 @@ rewrite_case_clauses([{do,[{in,_,[{'_',_,_},[false,nil]]}],False},{do,[{'_',_,_}
rewrite_case_clauses(Clauses) ->
Clauses.
module_ref(_Raw, Module, nil) ->
module_ref(Raw, Module, Nesting) when is_atom(Raw); Nesting == nil ->
Module;
module_ref({ '__aliases__', _, ['Elixir'|_]}, Module, _Nesting) ->
module_ref({ '__aliases__', _, ['Elixir'|_] }, Module, _Nesting) ->
Module;
module_ref(_F, Module, Nesting) ->
module_ref(_Raw, Module, Nesting) ->
elixir_aliases:concat([Nesting, Module]).
is_reserved_data(moduledoc) -> true;
+13 -1
View File
@@ -45,6 +45,18 @@ quote(Else, Q, S) ->
do_quote({ unquote, _Line, [Expr] }, #elixir_quote{unquote=true}, S) ->
elixir_translator:translate_each(Expr, S);
do_quote({ 'alias!', _Line, [Expr] }, Q, S) ->
do_quote(Expr, Q#elixir_quote{expand_aliases=false}, S);
do_quote({ '__aliases__', Line, [H|_] = Aliases }, #elixir_quote{expand_aliases=true} = Q, S) when is_atom(H) and H /= 'Elixir' ->
{ TAliases, SA } = do_quote(['Elixir'|Aliases], Q, S),
{ { tuple, Line, [
{ atom, Line, '__aliases__' },
line(Line, Q),
TAliases
] }, SA };
do_quote({ { { '.', Line, [Left, unquote] }, _, [Expr] }, _, Args }, #elixir_quote{unquote=true} = Q, S) ->
All = [Left, { unquote, Line, [Expr] }, Args, S#elixir_scope.file],
{ TAll, TS } = lists:mapfoldl(fun(X, Acc) -> do_quote(X, Q, Acc) end, S, All),
@@ -59,7 +71,7 @@ do_quote({ Left, Line, nil }, Q, S) when is_atom(Left) ->
Tuple = { tuple, Line, [
{ atom, Line, Left },
line(Line, Q),
{ atom, Line, Q#elixir_quote.marker }
{ atom, Line, Q#elixir_quote.var_context }
] },
{ Tuple, S };
+1 -14
View File
@@ -8,8 +8,7 @@
serialize/1, deserialize/1,
serialize_with_vars/2, deserialize_with_vars/2,
to_erl_env/1, to_ex_env/1, filename/1,
umergev/2, umergec/2, merge_clause_vars/2,
expand_var_context/4
umergev/2, umergec/2, merge_clause_vars/2
]).
-include("elixir.hrl").
-compile({parse_transform, elixir_transform}).
@@ -80,18 +79,6 @@ build_ex_var(Line, Key, Name, S) ->
Var = { ?ELIXIR_ATOM_CONCAT([Name, "@", Counter]), Line, Context },
{ Var, NS }.
% Handle var contexts
expand_var_context(_Line, Atom, _Msg, _S) when is_atom(Atom) -> Atom;
expand_var_context(Line, Else, Msg, S) ->
Kind = 'Elixir.Macro':expand(Else, to_ex_env({ Line, S })),
case is_atom(Kind) of
true -> Kind;
false -> elixir_errors:syntax_error(Line, S#elixir_scope.file,
"~ts, expected an atom or an alias", [Msg])
end.
% Handle Macro.Env conversion
to_erl_env(Scope) ->
+63 -28
View File
@@ -212,28 +212,20 @@ translate_each({ '__CALLER__', Line, Atom }, S) when is_atom(Atom) ->
%% Aliases
translate_each({ '__aliases__', Line, [H] }, S) when H /= 'Elixir' ->
Atom = list_to_atom("Elixir-" ++ atom_to_list(H)),
{ { atom, Line, elixir_aliases:lookup(Atom, S#elixir_scope.aliases) }, S };
translate_each({ '__aliases__', Line, _ } = Alias, S) ->
case elixir_aliases:expand(Alias, S#elixir_scope.aliases) of
Atom when is_atom(Atom) -> { { atom, Line, Atom}, S };
Aliases ->
{ TAliases, SA } = translate_args(Aliases, S),
translate_each({ '__aliases__', Line, [H|T] }, S) ->
Aliases = if
is_atom(H) andalso (H /= 'Elixir') ->
Atom = list_to_atom("Elixir-" ++ atom_to_list(H)),
[elixir_aliases:lookup(Atom, S#elixir_scope.aliases)|T];
true ->
[H|T]
end,
{ TAliases, SA } = translate_args(Aliases, S),
case lists:all(fun is_atom_tuple/1, TAliases) of
true ->
Atoms = [Atom || { atom, _, Atom } <- TAliases],
{ { atom, Line, elixir_aliases:concat(Atoms) }, SA };
false ->
Args = [elixir_tree_helpers:build_simple_list(Line, TAliases)],
{ ?ELIXIR_WRAP_CALL(Line, elixir_aliases, concat, Args), SA }
case lists:all(fun is_atom_tuple/1, TAliases) of
true ->
Atoms = [Atom || { atom, _, Atom } <- TAliases],
{ { atom, Line, elixir_aliases:concat(Atoms) }, SA };
false ->
Args = [elixir_tree_helpers:build_simple_list(Line, TAliases)],
{ ?ELIXIR_WRAP_CALL(Line, elixir_aliases, concat, Args), SA }
end
end;
%% Quoting
@@ -252,11 +244,11 @@ translate_each({ quote, GivenLine, [T] }, S) when is_list(T) ->
syntax_error(GivenLine, S#elixir_scope.file, "invalid args for quote")
end,
Marker = case lists:keyfind(var_context, 1, T) of
Context = case lists:keyfind(var_context, 1, T) of
{ var_context, VarContext } ->
elixir_scope:expand_var_context(GivenLine, VarContext,
expand_var_context(GivenLine, VarContext,
"invalid argument given for var_context in quote", S);
_ ->
false ->
case lists:keyfind(hygiene, 1, T) of
{ hygiene, false } -> nil;
_ ->
@@ -267,14 +259,24 @@ translate_each({ quote, GivenLine, [T] }, S) when is_list(T) ->
end
end,
Aliases = case lists:keyfind(expand_aliases, 1, T) of
{ expand_aliases, Bool } when is_boolean(Bool) ->
Bool;
false ->
case lists:keyfind(hygiene, 1, T) of
{ hygiene, Bool } when is_boolean(Bool) -> Bool;
false -> true
end
end,
{ DefaultLine, DefaultFile } = case lists:keyfind(location, 1, T) of
{ location, keep } -> { keep, keep };
_ -> { 0, nil }
false -> { 0, nil }
end,
Line = case lists:keyfind(line, 1, T) of
{ line, LineValue } -> LineValue;
_ -> DefaultLine
false -> DefaultLine
end,
{ TLine, SL } = case Line of
@@ -284,7 +286,7 @@ translate_each({ quote, GivenLine, [T] }, S) when is_list(T) ->
File = case lists:keyfind(file, 1, T) of
{ file, FileValue } -> FileValue;
_ -> DefaultFile
false -> DefaultFile
end,
TFile = case File of
@@ -306,11 +308,31 @@ translate_each({ quote, GivenLine, [T] }, S) when is_list(T) ->
_ -> true
end,
elixir_quote:quote(TExprs, #elixir_quote{marker=Marker, line=TLine, unquote=Unquote}, SL);
Q = #elixir_quote{var_context=Context, line=TLine, unquote=Unquote,
expand_aliases=Aliases, expand_imports=true},
elixir_quote:quote(TExprs, Q, SL);
translate_each({ quote, GivenLine, [_] }, S) ->
syntax_error(GivenLine, S#elixir_scope.file, "invalid args for quote");
translate_each({ 'alias!', Line, [Arg] }, S) ->
translate_each(Arg, S);
translate_each({ 'var!', Line, [Arg] }, S) ->
translate_each({ 'var!', Line, [Arg, nil] }, S);
translate_each({ 'var!', Line, [{Name, _, Atom}, Kind] }, S) when is_atom(Name), is_atom(Atom) ->
Expanded = expand_var_context(Line, Kind, "invalid second argument for var!", S),
elixir_scope:translate_var(Line, Name, Expanded, S);
translate_each({ 'var!', Line, [Name, Kind] }, S) when is_atom(Name) ->
Expanded = expand_var_context(Line, Kind, "invalid second argument for var!", S),
elixir_scope:translate_var(Line, Name, Expanded, S);
translate_each({ 'var!', Line, [_, _] }, S) ->
syntax_error(Line, S#elixir_scope.file, "invalid first argument for var!, expected a var or an atom");
%% Functions
translate_each({ fn, Line, [[{do, { '->', _, Pairs }}]] }, S) ->
@@ -491,6 +513,19 @@ translate_local(Line, Name, Args, S) ->
},
{ { call, Line, Remote, TArgs }, NS }.
expand_var_context(_Line, Atom, _Msg, _S) when is_atom(Atom) -> Atom;
expand_var_context(Line, { '__aliases__', _, _ } = Alias, Msg, S) ->
case translate_each(Alias, S) of
{ { atom, _, Atom }, _ } ->
Atom;
_ ->
syntax_error(Line, S#elixir_scope.file, "~ts, expected a compile time available alias", [Msg])
end;
expand_var_context(Line, _, Msg, S) ->
syntax_error(Line, S#elixir_scope.file, "~ts, expected an atom or an alias", [Msg]).
%% Translate args
%% Variables in arguments are not propagated from one
+12 -2
View File
@@ -54,8 +54,8 @@ defmodule Kernel.QuoteTest do
assert quote(do: foo.unquote(:bar)(1)) == quote(do: foo.bar(1))
assert quote(do: foo.unquote(:bar)(1) do 2 + 3 end) == quote(do: foo.bar(1) do 2 + 3 end)
assert Code.eval_quoted(quote(do: Foo.unquote(Bar))) == { Foo.Bar, [] }
assert Code.eval_quoted(quote(do: Foo.unquote(quote do: Bar))) == { Foo.Bar, [] }
assert Code.eval_quoted(quote(do: Foo.unquote(Bar))) == { Elixir.Foo.Bar, [] }
assert Code.eval_quoted(quote(do: Foo.unquote(quote do: Bar))) == { Elixir.Foo.Bar, [] }
assert_raise SyntaxError, fn ->
quote(do: foo.unquote(1))
@@ -142,3 +142,13 @@ defmodule Kernel.QuoteTest.VarHygieneTest do
read_interference
end
end
defmodule Kernel.QuoteTest.AliasHygieneTest do
use ExUnit.Case, async: true
test :expand_aliases do
assert Code.eval_quoted(quote do: Foo.Bar) == { Elixir.Foo.Bar, [] }
assert Code.eval_quoted(quote do: alias!(Foo.Bar)) == { Foo.Bar, [] }
assert Code.eval_quoted(quote expand_aliases: false, do: Foo.Bar) == { Foo.Bar, [] }
end
end
+1 -1
View File
@@ -57,7 +57,7 @@ defmodule ModuleTest do
Module.eval_quoted __MODULE__, contents, [], file: "sample.ex", line: 13
defmacrop in_module(block) do
quote do
quote expand_aliases: false do
defmodule Temp, unquote(block)
:code.purge(Temp)
:code.delete(Temp)
+2 -2
View File
@@ -1,13 +1,13 @@
Code.require_file "../test_helper.exs", __FILE__
defmodule Typespec.Test.Type do
defmodule Typespec.TypeTest do
use ExUnit.Case, async: true
# This macro allows us to focus on the result of the
# definition and not on the hassles of handling test
# module
defmacrop test_module([{:do, block}]) do
quote do
quote expand_aliases: false do
{ :module, T, _binary, result } = defmodule T do
unquote(block)
end