Skip to content

Commit f1e2fcf

Browse files
author
José Valim
committed
Same module variables must be explicitly shared
Prior to this commit, the following code would work: defmodule Hello do defmacro write do quote do a = 1 end end defmacro read do quote do a end end end require Hello Hello.write Hello.read Although this is convenient, it has many issues: 1. It is not clear at any point the intent of sharing the variables in between the macros 2. Whenever macros from the same module are nested, there is a chance a variable used before and after the nesting to be overriden We address this issue by only allowing variables explicitly marked with var! to be shared. Here is how the example above should be updated: defmodule Hello do defmacro write do quote do var!(a, Hello) = 1 end end defmacro read do quote do var!(a, Hello) end end end require Hello Hello.write Hello.read
1 parent dd47d09 commit f1e2fcf

File tree

7 files changed

+121
-54
lines changed

7 files changed

+121
-54
lines changed

lib/elixir/include/elixir.hrl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
local=nil, %% the scope to evaluate local functions against
2626
context_modules=[], %% modules defined in the current context
2727
macro_aliases=[], %% keep aliases defined inside a macro
28+
macro_counter=0, %% macros expansions counter
2829
aliases, %% an orddict with aliases by new -> old names
2930
file, %% the current scope filename
3031
requires, %% a set with modules required

lib/elixir/lib/kernel/special_forms.ex

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ defmodule Kernel.SpecialForms do
524524
525525
## Hygiene and context
526526
527-
Elixir macros are hygienic via means of deferred resolution.
527+
Elixir macros are hygienic by means of deferred resolution.
528528
This means variables, aliases and imports defined inside the
529529
quoted refer to the context that defined the macro and not
530530
the context where the macro is expanded.
@@ -544,7 +544,10 @@ defmodule Kernel.SpecialForms do
544544
Notice how the third element of the returned tuple is the
545545
module name. This means that the variable is associated to the
546546
`ContextSample` module and only code generated by this module
547-
will be able to access that particular `world` variable.
547+
will be able to access that particular `world` variable. Not
548+
only variables hygiene, but also imports and aliases hygiene
549+
are delimited by context.
550+
548551
While this means macros from the same module could have
549552
conflicting variables, it also allows different quotes from
550553
the same module to access them.
@@ -588,31 +591,47 @@ defmodule Kernel.SpecialForms do
588591
NoHygiene.interference
589592
a #=> 1
590593
591-
It is important to understand that quoted variables are scoped
592-
to the module they are defined. That said, even if two modules
593-
define the same quoted variable `a`, their values are going
594-
to be independent:
594+
Note that you cannot even access variables defined by the same
595+
module unless you explicitly give it a context:
595596
596-
defmodule Hygiene1 do
597-
defmacro var1 do
598-
quote do: a = 1
597+
defmodule Hygiene do
598+
defmacro write do
599+
quote do
600+
a = 1
601+
end
599602
end
600-
end
601603
602-
defmodule Hygiene2 do
603-
defmacro var2 do
604-
quote do: a = 2
604+
defmacro read do
605+
quote do
606+
a
607+
end
605608
end
606609
end
607610
608-
Calling macros `var1` and `var2` are not going to change their
609-
each other values for `a`. This is useful because quoted
610-
variables from different modules cannot conflict. If you desire
611-
to explicitly access a variable from another module, we can once
612-
again use `var!` macro, but explicitly passing a second argument:
611+
Hygiene.write
612+
Hygiene.read
613+
#=> no variable a
613614
614-
# Access the variable a from Hygiene1
615-
quote do: var!(a, Hygiene1) = 2
615+
For such, you can explicitly pass the current module scope as
616+
argument:
617+
618+
defmodule ContextHygiene do
619+
defmacro write do
620+
quote do
621+
var!(a, ContextHygiene) = 1
622+
end
623+
end
624+
625+
defmacro read do
626+
quote do
627+
var!(a, ContextHygiene)
628+
end
629+
end
630+
end
631+
632+
ContextHygiene.write
633+
ContextHygiene.read
634+
#=> 1
616635
617636
Hygiene for variables can be disabled overall as:
618637
@@ -669,7 +688,6 @@ defmodule Kernel.SpecialForms do
669688
#=> "world"
670689
end
671690
672-
673691
## Hygiene in imports
674692
675693
Similar to aliases, imports in Elixir are hygienic. Consider the

lib/elixir/src/elixir_dispatch.erl

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ dispatch_import(Meta, Name, Args, S, Callback) ->
8989
{ error, internal } ->
9090
elixir_tracker:record_import(Tuple, ?builtin, Module, S#elixir_scope.function),
9191
elixir_macros:translate({ Name, Meta, Args }, S);
92-
{ ok, _Receiver, Tree } ->
93-
translate_expansion(Meta, Tree, S)
92+
{ ok, Receiver, Tree } ->
93+
translate_expansion(Meta, Receiver, Tree, S)
9494
end
9595
end.
9696

@@ -111,8 +111,8 @@ dispatch_require(Meta, Receiver, Name, Args, S, Callback) ->
111111
{ error, internal } ->
112112
elixir_tracker:record_remote(Tuple, ?builtin, S#elixir_scope.module, S#elixir_scope.function),
113113
elixir_macros:translate({ Name, Meta, Args }, S);
114-
{ ok, _Receiver, Tree } ->
115-
translate_expansion(Meta, Tree, S)
114+
{ ok, Receiver, Tree } ->
115+
translate_expansion(Meta, Receiver, Tree, S)
116116
end
117117
end.
118118

@@ -222,13 +222,14 @@ expand_macro_named(Meta, Receiver, Name, Arity, Args, Module, S) ->
222222
Fun = fun Receiver:ProperName/ProperArity,
223223
expand_macro_fun(Meta, Fun, Receiver, Name, Args, Module, S).
224224

225-
translate_expansion(Meta, Tree, S) ->
225+
translate_expansion(Meta, Receiver, Tree, S) ->
226226
Line = ?line(Meta),
227+
New = S#elixir_scope.macro_counter + 1,
227228

228229
try
229230
elixir_translator:translate_each(
230-
elixir_quote:linify(Line, Tree),
231-
S
231+
elixir_quote:linify(Line, { Receiver, New }, Tree),
232+
S#elixir_scope{macro_counter=New}
232233
)
233234
catch
234235
Kind:Reason ->

lib/elixir/src/elixir_quote.erl

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
%% Implements Elixir quote.
22
-module(elixir_quote).
33
-export([escape/2, erl_escape/3, erl_quote/4,
4-
linify/2, unquote/4, tail_join/3, join/2]).
4+
linify/2, linify/3, unquote/4, tail_join/3, join/2]).
55
-include("elixir.hrl").
66

77
%% Apply the line from site call on quoted contents.
88
linify(Line, Exprs) when is_integer(Line) ->
9-
do_linify(Line, Exprs).
9+
do_linify(Line, nil, Exprs).
1010

11-
do_linify(Line, { Left, Meta, Right }) when is_list(Meta) ->
12-
NewMeta = case ?line(Meta) of
13-
0 -> keystore(line, Meta, Line);
14-
_ -> Meta
11+
linify(Line, Var, Exprs) when is_integer(Line) ->
12+
do_linify(Line, Var, Exprs).
13+
14+
do_linify(Line, { Receiver, Counter } = Var, { Left, Meta, Receiver }) when is_atom(Left), is_list(Meta) ->
15+
NewMeta = case keyfind(counter, Meta) of
16+
{ counter, _ } -> Meta;
17+
_ -> keystore(counter, Meta, Counter)
1518
end,
16-
{ do_linify(Line, Left), NewMeta, do_linify(Line, Right) };
19+
do_tuple_linify(Line, Var, NewMeta, Left, Receiver);
20+
21+
do_linify(Line, Var, { Left, Meta, Right }) when is_list(Meta) ->
22+
do_tuple_linify(Line, Var, Meta, Left, Right);
1723

18-
do_linify(Line, { Left, Right }) ->
19-
{ do_linify(Line, Left), do_linify(Line, Right) };
24+
do_linify(Line, Var, { Left, Right }) ->
25+
{ do_linify(Line, Var, Left), do_linify(Line, Var, Right) };
2026

21-
do_linify(Line, List) when is_list(List) ->
22-
[do_linify(Line, X) || X <- List];
27+
do_linify(Line, Var, List) when is_list(List) ->
28+
[do_linify(Line, Var, X) || X <- List];
2329

24-
do_linify(_, Else) -> Else.
30+
do_linify(_, _, Else) -> Else.
31+
32+
do_tuple_linify(Line, Var, Meta, Left, Right) ->
33+
NewMeta = case ?line(Meta) of
34+
0 -> keystore(line, Meta, Line);
35+
_ -> Meta
36+
end,
37+
{ do_linify(Line, Var, Left), NewMeta, do_linify(Line, Var, Right) }.
2538

2639
%% Some expressions cannot be unquoted at compilation time.
2740
%% This function is responsible for doing runtime unquoting.

lib/elixir/src/elixir_scope.erl

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
]).
1111
-include("elixir.hrl").
1212

13-
translate_var(Meta, Name, Kind, S, Callback) ->
13+
translate_var(Meta, Name, Kind, S, Callback) when is_atom(Kind); is_integer(Kind) ->
1414
Line = ?line(Meta),
1515
Vars = S#elixir_scope.vars,
1616
Tuple = { Name, Kind },
@@ -95,7 +95,7 @@ to_ex_env({ Line, #elixir_scope{module=Module,file=File,
9595
Context, Requires, Functions, Macros, ContextModules, MacroAliases,
9696
list_vars(Vars) }.
9797

98-
list_vars(Vars) -> [K || { K, _ } <- Vars].
98+
list_vars(Vars) -> [Pair || { { _, K } = Pair, _ } <- Vars, is_atom(K)].
9999

100100
% Provides a tuple with only the scope information we want to serialize.
101101

@@ -109,9 +109,14 @@ serialize(S) ->
109109

110110
serialize_with_vars(Line, S) when is_integer(Line) ->
111111
{ Vars, _ } = orddict:fold(fun({ Key, Kind }, Value, { Acc, Counter }) ->
112+
KindKey = if
113+
is_atom(Kind) -> atom;
114+
is_integer(Kind) -> integer
115+
end,
116+
112117
{ { cons, Line, { tuple, Line, [
113118
{ atom, Line, Key },
114-
{ atom, Line, Kind },
119+
{ KindKey, Line, Kind },
115120
{ atom, Line, ?atom_concat(["_@", Counter]) },
116121
{ var, Line, Value }
117122
] }, Acc }, Counter + 1 }
@@ -157,6 +162,7 @@ umergev(S1, S2) ->
157162
umergec(S1, S2) ->
158163
S1#elixir_scope{
159164
counter=S2#elixir_scope.counter,
165+
macro_counter=S2#elixir_scope.macro_counter,
160166
extra_guards=S2#elixir_scope.extra_guards,
161167
super=S2#elixir_scope.super,
162168
caller=S2#elixir_scope.caller

lib/elixir/src/elixir_translator.erl

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,9 @@ translate_each({ 'super?', Meta, [] }, S) ->
323323

324324
%% Variables
325325

326-
translate_each({ '^', Meta, [ { Name, _, Kind } = Var ] },
326+
translate_each({ '^', Meta, [ { Name, VarMeta, Kind } = Var ] },
327327
#elixir_scope{extra=fn_match, extra_guards=Extra} = S) when is_atom(Name), is_atom(Kind) ->
328-
case orddict:find({ Name, Kind }, S#elixir_scope.backup_vars) of
328+
case orddict:find({ Name, var_kind(VarMeta, Kind) }, S#elixir_scope.backup_vars) of
329329
{ ok, Value } ->
330330
Line = ?line(Meta),
331331
{ TVar, TS } = translate_each(Var, S),
@@ -335,8 +335,8 @@ translate_each({ '^', Meta, [ { Name, _, Kind } = Var ] },
335335
compile_error(Meta, S#elixir_scope.file, "unbound variable ^~ts", [Name])
336336
end;
337337

338-
translate_each({ '^', Meta, [ { Name, _, Kind } ] }, #elixir_scope{context=match} = S) when is_atom(Name), is_atom(Kind) ->
339-
case orddict:find({ Name, Kind }, S#elixir_scope.backup_vars) of
338+
translate_each({ '^', Meta, [ { Name, VarMeta, Kind } ] }, #elixir_scope{context=match} = S) when is_atom(Name), is_atom(Kind) ->
339+
case orddict:find({ Name, var_kind(VarMeta, Kind) }, S#elixir_scope.backup_vars) of
340340
{ ok, Value } ->
341341
{ { var, ?line(Meta), Value }, S };
342342
error ->
@@ -352,7 +352,7 @@ translate_each({ '^', Meta, [ Expr ] }, S) ->
352352
"the unary operator ^ can only be used with variables, invalid expression ^~ts", ['Elixir.Macro':to_string(Expr)]);
353353

354354
translate_each({ Name, Meta, Kind }, S) when is_atom(Name), is_atom(Kind) ->
355-
elixir_scope:translate_var(Meta, Name, Kind, S, fun() ->
355+
elixir_scope:translate_var(Meta, Name, var_kind(Meta, Kind), S, fun() ->
356356
translate_each({ Name, Meta, [] }, S)
357357
end);
358358

@@ -444,6 +444,12 @@ translate_each(Literal, S) ->
444444

445445
%% Helpers
446446

447+
var_kind(Meta, Kind) ->
448+
case lists:keyfind(counter, 1, Meta) of
449+
{ counter, Counter } -> Counter;
450+
false -> Kind
451+
end.
452+
447453
%% Opts
448454

449455
translate_opts(Meta, Kind, Allowed, Opts, S) ->

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,22 @@ defmodule Kernel.QuoteTest.VarHygieneTest do
236236
quote do: var!(a, __MODULE__)
237237
end
238238

239+
defmacrop nested(var, do: block) do
240+
quote do
241+
var = unquote(var)
242+
unquote(block)
243+
var
244+
end
245+
end
246+
247+
defmacrop hat do
248+
quote do
249+
var = 1
250+
^var = 1
251+
var
252+
end
253+
end
254+
239255
test :no_interference do
240256
a = 10
241257
no_interference
@@ -247,12 +263,6 @@ defmodule Kernel.QuoteTest.VarHygieneTest do
247263
assert a == 1
248264
end
249265

250-
test :cross_module_no_interference do
251-
cross_module_no_interference
252-
no_interference
253-
assert read_cross_module == 10
254-
end
255-
256266
test :cross_module_interference do
257267
cross_module_no_interference
258268
cross_module_interference
@@ -268,6 +278,18 @@ defmodule Kernel.QuoteTest.VarHygieneTest do
268278
a = 10
269279
read_interference
270280
end
281+
282+
test :nested do
283+
assert (nested 1 do
284+
nested 2 do
285+
:ok
286+
end
287+
end) == 1
288+
end
289+
290+
test :hat do
291+
assert hat == 1
292+
end
271293
end
272294

273295
defmodule Kernel.QuoteTest.AliasHygiene do

0 commit comments

Comments
 (0)