diff --git a/lib/elixir/include/elixir.hrl b/lib/elixir/include/elixir.hrl index 8f850509f5..2452a13dc4 100644 --- a/lib/elixir/include/elixir.hrl +++ b/lib/elixir/include/elixir.hrl @@ -31,7 +31,9 @@ -record(elixir_quote, { line=0, - marker=nil, + var_context=nil, + expand_aliases=true, + expand_imports=true, unquote=true }). diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 34cc8af2b3..dcea28101c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -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`. diff --git a/lib/elixir/lib/kernel/special_forms.ex b/lib/elixir/lib/kernel/special_forms.ex index 481ce1850c..7a7bc76acc 100644 --- a/lib/elixir/lib/kernel/special_forms.ex +++ b/lib/elixir/lib/kernel/special_forms.ex @@ -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. diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 72aa57ab75..b3f0e25ed1 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -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 diff --git a/lib/elixir/src/elixir_aliases.erl b/lib/elixir/src/elixir_aliases.erl index 960403bffb..69eab249d7 100644 --- a/lib/elixir/src/elixir_aliases.erl +++ b/lib/elixir/src/elixir_aliases.erl @@ -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; diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index 2b75ba71ff..2170bd0d74 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -372,7 +372,5 @@ in_erlang_macros() -> {'or',2}, {'receive',1}, {'try',1}, - {'var!',1}, - {'var!',2}, {'xor',2} ]. \ No newline at end of file diff --git a/lib/elixir/src/elixir_import.erl b/lib/elixir/src/elixir_import.erl index f2f2e489c9..407131c4d4 100644 --- a/lib/elixir/src/elixir_import.erl +++ b/lib/elixir/src/elixir_import.erl @@ -318,5 +318,8 @@ non_overridable_macros() -> {'super','*'}, {'super?',0}, {'bc','*'}, - {'lc','*'} + {'lc','*'}, + {'var!',1}, + {'var!',2}, + {'alias!',1} ]. diff --git a/lib/elixir/src/elixir_macros.erl b/lib/elixir/src/elixir_macros.erl index 796d1162ce..bb279fc617 100644 --- a/lib/elixir/src/elixir_macros.erl +++ b/lib/elixir/src/elixir_macros.erl @@ -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; diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index b717610390..886741fec5 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -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 }; diff --git a/lib/elixir/src/elixir_scope.erl b/lib/elixir/src/elixir_scope.erl index 4c5776b35a..ec56d94b3a 100644 --- a/lib/elixir/src/elixir_scope.erl +++ b/lib/elixir/src/elixir_scope.erl @@ -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) -> diff --git a/lib/elixir/src/elixir_translator.erl b/lib/elixir/src/elixir_translator.erl index 6e32b84fb8..f9885a4549 100644 --- a/lib/elixir/src/elixir_translator.erl +++ b/lib/elixir/src/elixir_translator.erl @@ -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 diff --git a/lib/elixir/test/elixir/kernel/quote_test.exs b/lib/elixir/test/elixir/kernel/quote_test.exs index 34fabf83db..4bfa89cceb 100644 --- a/lib/elixir/test/elixir/kernel/quote_test.exs +++ b/lib/elixir/test/elixir/kernel/quote_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs index bcaf6e4a46..c61c7a4eaf 100644 --- a/lib/elixir/test/elixir/module_test.exs +++ b/lib/elixir/test/elixir/module_test.exs @@ -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) diff --git a/lib/elixir/test/elixir/typespec_test.exs b/lib/elixir/test/elixir/typespec_test.exs index b4b6df7854..b81d34cf2d 100644 --- a/lib/elixir/test/elixir/typespec_test.exs +++ b/lib/elixir/test/elixir/typespec_test.exs @@ -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