Skip to content

Commit abb1ae9

Browse files
author
José Valim
committed
Add hygienic imports to macros
1 parent b3747a2 commit abb1ae9

File tree

8 files changed

+139
-43
lines changed

8 files changed

+139
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* [Kernel] More optimizations were added to Record handling
1313
* [Kernel] `?\x` and `?\` are now supported ways to retrieve a codepoint
1414
* [Kernel] Octal numbers can now be defined as `0777`
15+
* [Kernel] Improve macros hygiene regarding variables, aliases and imports
1516
* [Mix] Mix now starts the current application before run, iex, test and friends
1617
* [Mix] Mix now provides basic support for compiling `.erl` files
1718
* [Mix] `mix escriptize` only generates escript if necessary and accept `--force` and `--no-compile` as options

lib/elixir/include/elixir.hrl

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,27 @@
99
-define(line(Opts), elixir_tree_helpers:get_line(Opts)).
1010

1111
-record(elixir_scope, {
12-
context=nil, %% can be assign, guards or nil
13-
noname=false, %% when true, don't add new names (used by try)
14-
check_clauses=true, %% when true, check def clauses ordering
15-
super=false, %% when true, it means super was invoked
16-
caller=false, %% when true, it means caller was invoked
17-
name_args=false, %% when true, it means arguments should be named
18-
module=nil, %% the current module
19-
function=nil, %% the current function
20-
vars=[], %% a dict of defined variables and their alias
21-
temp_vars=[], %% a dict of all variables defined in a particular assign
22-
clause_vars=nil, %% a dict of all variables defined in a particular clause
23-
extra_guards=nil, %% extra guards from args expansion
24-
counter=[], %% a counter for the variables defined
25-
local=nil, %% the scope to evaluate local functions against
26-
scheduled=[], %% scheduled modules to be loaded
27-
file, %% the current scope filename
28-
aliases, %% an orddict with aliases by new -> old names
29-
requires, %% a set with modules required
30-
macros, %% a list with macros imported by module
31-
functions}). %% a list with functions imported by module
12+
context=nil, %% can be assign, guards or nil
13+
noname=false, %% when true, don't add new names (used by try)
14+
check_requires=true, %% when true, check requires
15+
check_clauses=true, %% when true, check def clauses ordering
16+
super=false, %% when true, it means super was invoked
17+
caller=false, %% when true, it means caller was invoked
18+
name_args=false, %% when true, it means arguments should be named
19+
module=nil, %% the current module
20+
function=nil, %% the current function
21+
vars=[], %% a dict of defined variables and their alias
22+
temp_vars=[], %% a dict of all variables defined in a particular assign
23+
clause_vars=nil, %% a dict of all variables defined in a particular clause
24+
extra_guards=nil, %% extra guards from args expansion
25+
counter=[], %% a counter for the variables defined
26+
local=nil, %% the scope to evaluate local functions against
27+
scheduled=[], %% scheduled modules to be loaded
28+
file, %% the current scope filename
29+
aliases, %% an orddict with aliases by new -> old names
30+
requires, %% a set with modules required
31+
macros, %% a list with macros imported by module
32+
functions}). %% a list with functions imported by module
3233

3334
-record(elixir_quote, {
3435
line=0,

lib/elixir/lib/kernel/special_forms.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ defmodule Kernel.SpecialForms do
356356
* `:location` - When set to `:keep`, keeps the current line and file on quotes.
357357
Read the Stacktrace information section below for more information;
358358
* `:expand_aliases` - When false, do not expand aliases;
359+
* `:expand_imports` - When false, do not expand imports;
359360
* `:var_context` - The context for quoted variables. Defaults to the current module;
360361
361362
## Macro literals
@@ -482,6 +483,55 @@ defmodule Kernel.SpecialForms do
482483
require NoHygiene
483484
NoHygiene.interference #=> UndefinedFunctionError
484485
486+
## Hygiene in imports
487+
488+
Similar to aliases, imports in Elixir hygienic. Consider the
489+
following code:
490+
491+
defmodule Hygiene do
492+
defmacrop get_size do
493+
quote do
494+
size("hello")
495+
end
496+
end
497+
498+
def return_size do
499+
import Kernel, except: [size: 1]
500+
get_size
501+
end
502+
end
503+
504+
Hygiene.return_size #=> 5
505+
506+
Notice how `return_size` returns 5 even though the `size/1`
507+
function is not imported.
508+
509+
Elixir is smart enough to delay the resolution to the latest
510+
moment possible. So, if you call `size("hello")` inside quote,
511+
but no `size/1` function is available, it is then expanded on
512+
the caller:
513+
514+
defmodule Lazy do
515+
defmacrop get_size do
516+
import Kernel, except: [size: 1]
517+
518+
quote do
519+
size([a: 1, b: 2])
520+
end
521+
end
522+
523+
def return_size do
524+
import Kernel, except: [size: 1]
525+
import Dict, only: [size: 1]
526+
get_size
527+
end
528+
end
529+
530+
Lazy.return_size #=> 2
531+
532+
As in aliases, imports expansion can be explicitly disabled
533+
via the `expand_imports` option.
534+
485535
## Stacktrace information
486536
487537
One of Elixir goals is to provide proper stacktrace whenever there is an

lib/elixir/src/elixir_quote.erl

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,17 @@ do_quote({ Left, Meta, nil }, Q, S) when is_atom(Left) ->
8383
] },
8484
{ Tuple, S };
8585

86-
% do_quote({ Name, Line, Args } = Tuple, #elixir_quote{expand_imports=true} = Q, S) when is_atom(Name), is_list(Args) ->
87-
% case elixir_dispatch:find_import(Line, Name, length(Args), S) of
88-
% false -> do_quote_tuple(Tuple, Q, S);
89-
% Receiver -> do_quote_tuple({ { '.', Line, [Receiver, Name] }, Line, Args }, Q, S)
90-
% end;
86+
do_quote({ Name, Meta, ArgsOrAtom } = Tuple, #elixir_quote{expand_imports=true} = Q, S) when is_atom(Name) ->
87+
Arity = case is_atom(ArgsOrAtom) of
88+
true -> 0;
89+
false -> length(ArgsOrAtom)
90+
end,
91+
92+
case (lists:keyfind(import, 1, Meta) == false) andalso
93+
elixir_dispatch:find_import(Meta, Name, Arity, S) of
94+
false -> do_quote_tuple(Tuple, Q, S);
95+
Receiver -> do_quote_tuple({ Name, [{import,Receiver}|Meta], ArgsOrAtom }, Q, S)
96+
end;
9197

9298
do_quote({ _, _, _ } = Tuple, Q, S) ->
9399
do_quote_tuple(Tuple, Q, S);

lib/elixir/src/elixir_translator.erl

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -413,19 +413,28 @@ translate_each({ Atom, Meta, Args } = Original, S) when is_atom(Atom) ->
413413
false ->
414414
case elixir_partials:handle(Original, S) of
415415
error ->
416-
Callback = fun() ->
417-
case S#elixir_scope.context of
418-
guard ->
419-
Arity = length(Args),
420-
File = S#elixir_scope.file,
421-
case Arity of
422-
0 -> syntax_error(Meta, File, "unknown variable ~s or cannot invoke local ~s/~B inside guard", [Atom, Atom, Arity]);
423-
_ -> syntax_error(Meta, File, "cannot invoke local ~s/~B inside guard", [Atom, Arity])
424-
end;
425-
_ -> translate_local(Meta, Atom, Args, S)
426-
end
427-
end,
428-
elixir_dispatch:dispatch_import(Meta, Atom, Args, S, Callback);
416+
case lists:keyfind(import, 1, Meta) of
417+
{ import, Receiver } ->
418+
{ TRes, TS } = translate_each({ { '.', Meta, [Receiver, Atom] }, Meta, Args },
419+
S#elixir_scope{check_requires=false}),
420+
{ TRes, TS#elixir_scope{check_requires=S#elixir_scope.check_requires} };
421+
false ->
422+
Callback = fun() ->
423+
case S#elixir_scope.context of
424+
guard ->
425+
Arity = length(Args),
426+
File = S#elixir_scope.file,
427+
case Arity of
428+
0 -> syntax_error(Meta, File, "unknown variable ~s or cannot invoke local ~s/~B inside guard", [Atom, Atom, Arity]);
429+
_ -> syntax_error(Meta, File, "cannot invoke local ~s/~B inside guard", [Atom, Arity])
430+
end;
431+
_ ->
432+
translate_local(Meta, Atom, Args, S)
433+
end
434+
end,
435+
436+
elixir_dispatch:dispatch_import(Meta, Atom, Args, S, Callback)
437+
end;
429438
Else -> Else
430439
end;
431440
Else -> Else

lib/elixir/test/elixir/code_test.exs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ defmodule CodeTest do
6161
end
6262

6363
test :string_to_ast do
64-
assert { :ok, quote line: 1, do: 1 + 2 } = Code.string_to_ast("1 + 2")
65-
assert { :ok, quote line: 1, do: (1 + 2; 3 + 4) } = Code.string_to_ast("1 + 2; 3 + 4")
64+
assert Code.string_to_ast("1 + 2") == { :ok, quote hygiene: false, line: 1, do: 1 + 2 }
65+
assert Code.string_to_ast("1 + 2; 3 + 4") == { :ok, quote hygiene: false, line: 1, do: (1 + 2; 3 + 4) }
6666
assert { :error, _ } = Code.string_to_ast("a.1")
6767
end
6868

@@ -72,7 +72,7 @@ defmodule CodeTest do
7272
end
7373

7474
test :string_to_ast! do
75-
assert Code.string_to_ast!("1 + 2") == quote line: 1, do: 1 + 2
75+
assert Code.string_to_ast!("1 + 2") == quote hygiene: false, line: 1, do: 1 + 2
7676

7777
assert_raise SyntaxError, fn ->
7878
Code.string_to_ast!("a.1")

lib/elixir/test/elixir/kernel/quote_test.exs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,32 @@ defmodule Kernel.QuoteTest.AliasHygieneTest do
152152
assert Code.eval_quoted(quote expand_aliases: false, do: Foo.Bar) == { Foo.Bar, [] }
153153
end
154154
end
155+
156+
defmodule Kernel.QuoteTest.ImportsHygieneTest do
157+
use ExUnit.Case, async: true
158+
159+
defmacrop get_bin_size do
160+
quote do
161+
size("hello")
162+
end
163+
end
164+
165+
test :expand_imports do
166+
import Kernel, except: [size: 1]
167+
assert get_bin_size == 5
168+
end
169+
170+
defmacrop get_dict_size do
171+
import Kernel, except: [size: 1]
172+
173+
quote do
174+
size([a: 1, b: 2])
175+
end
176+
end
177+
178+
test :lazy_expand_imports do
179+
import Kernel, except: [size: 1]
180+
import Dict, only: [size: 1]
181+
assert get_dict_size == 2
182+
end
183+
end

lib/elixir/test/erlang/module_test.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ dynamic_function_test() ->
3434

3535
quote_unquote_test() ->
3636
F = fun() ->
37-
eval("defmodule Foo.Bar.Baz do\ndefmacro sum(a, b), do: quote(do: unquote(a) + unquote(b))\nend"),
37+
eval("defmodule Foo.Bar.Baz do\ndefmacro sum(a, b), do: quote(hygiene: false, do: unquote(a) + unquote(b))\nend"),
3838
{'+',[],[1,2]} = 'Elixir.Foo.Bar.Baz':'MACRO-sum'(nil, 1, 2)
3939
end,
4040
test_helper:run_and_remove(F, ['Elixir.Foo.Bar.Baz']).
@@ -44,7 +44,7 @@ quote_unquote_splicing_test() ->
4444

4545
operator_macro_test() ->
4646
F = fun() ->
47-
eval("defmodule Foo.Bar.Baz do\ndefmacro :+.(a, b), do: quote(do: unquote(a) - unquote(b))\nend"),
47+
eval("defmodule Foo.Bar.Baz do\ndefmacro :+.(a, b), do: quote(hygiene: false, do: unquote(a) - unquote(b))\nend"),
4848
{'-',[],[1,2]} = 'Elixir.Foo.Bar.Baz':'MACRO-+'(nil, 1, 2)
4949
end,
5050
test_helper:run_and_remove(F, ['Elixir.Foo.Bar.Baz']).

0 commit comments

Comments
 (0)