Skip to content

Commit 46fae17

Browse files
author
José Valim
committed
Allow &() as a general capture mechanism
1 parent fe9ae95 commit 46fae17

File tree

6 files changed

+269
-53
lines changed

6 files changed

+269
-53
lines changed

lib/elixir/lib/kernel/special_forms.ex

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,57 @@ defmodule Kernel.SpecialForms do
869869
870870
&local_function/1
871871
872+
A capture also allows the captured functions to be partially
873+
applied, for example:
874+
875+
iex> fun = &atom_to_binary(&1, :utf8)
876+
iex> fun.(:hello)
877+
"hello"
878+
879+
In the example above, we use &1 as a placeholder, generating
880+
a function with one argument. We could also use `&2` and `&3`
881+
to delimit more arguments:
882+
883+
iex> fun = &atom_to_binary(&1, &2)
884+
iex> fun.(:hello, :utf8)
885+
"hello"
886+
887+
Since operators are calls, they are also supported, although
888+
they require explicit parentheses around:
889+
890+
iex> fun = &(&1 + &2)
891+
iex> fun.(1, 2)
892+
3
893+
894+
And even more complex call expressions:
895+
896+
iex> fun = &(&1 + &2 + &3)
897+
iex> fun.(1, 2, 3)
898+
6
899+
900+
Remember tuple and lists are represented as calls in the AST and
901+
therefore are also allowed:
902+
903+
iex> fun = &{&1, &2}
904+
iex> fun.(1, 2)
905+
{ 1, 2 }
906+
907+
iex> fun = &[&1|&2]
908+
iex> fun.(1, 2)
909+
[1|2]
910+
911+
Anything that is not a call is not allowed though, examples:
912+
913+
# An atom is not a call
914+
&:foo
915+
916+
# A var is not a call
917+
var = 1
918+
&var
919+
920+
# A block expression is not a call
921+
&(foo(&1, &2); &3 + &4)
922+
872923
"""
873924
name = :&
874925
defmacro unquote(name)(expr)

lib/elixir/src/elixir_dispatch.erl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ import_function(Meta, Name, Arity, S) ->
4646
{ import, Receiver } ->
4747
require_function(Meta, Receiver, Name, Arity, S);
4848
nomatch ->
49-
elixir_tracker:record_local(Tuple, S#elixir_scope.module, S#elixir_scope.function),
50-
{ { 'fun', ?line(Meta), { function, Name, Arity } }, S }
49+
case elixir_import:special_form(Name, Arity) of
50+
true -> false;
51+
false ->
52+
elixir_tracker:record_local(Tuple, S#elixir_scope.module, S#elixir_scope.function),
53+
{ { 'fun', ?line(Meta), { function, Name, Arity } }, S }
54+
end
5155
end.
5256

5357
require_function(Meta, Receiver, Name, Arity, S) ->

lib/elixir/src/elixir_fn.erl

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,101 @@ capture(Meta, { '/', _, [{ F, _, C }, A] }, S) when is_atom(F), is_integer(A), i
4242
Else -> Else
4343
end;
4444

45+
capture(Meta, { { '.', _, [Left, Right] }, RemoteMeta, Args } = Expr, S) when is_atom(Right), is_list(Args) ->
46+
{ Mod, SE } = 'Elixir.Macro':expand_all(Left, elixir_scope:to_ex_env({ ?line(Meta), S }), S),
47+
48+
case is_atom(Mod) andalso is_sequential(Args) andalso
49+
elixir_dispatch:require_function(RemoteMeta, Mod, Right, length(Args), SE) of
50+
false -> do_capture(Meta, Expr, S);
51+
Else -> Else
52+
end;
53+
54+
capture(Meta, { '__block__', _, _ } = Expr, S) ->
55+
Message = "invalid args for &, block expressions are not allowed, got: ~ts",
56+
syntax_error(Meta, S#elixir_scope.file, Message, ['Elixir.Macro':to_string(Expr)]);
57+
58+
capture(Meta, { Atom, ImportMeta, Args } = Expr, S) when is_atom(Atom), is_list(Args) ->
59+
case is_sequential(Args) andalso
60+
elixir_dispatch:import_function(ImportMeta, Atom, length(Args), S) of
61+
false -> do_capture(Meta, Expr, S);
62+
Else -> Else
63+
end;
64+
65+
capture(Meta, { Left, Right }, S) ->
66+
capture(Meta, { '{}', Meta, [Left, Right] }, S);
67+
68+
capture(Meta, List, S) when is_list(List) ->
69+
capture(Meta, { '[]', Meta, List }, S);
70+
71+
capture(Meta, Arg, S) when is_integer(Arg) ->
72+
compile_error(Meta, S#elixir_scope.file, "unhandled &~B outside of a capture", [Arg]);
73+
4574
capture(Meta, Arg, S) ->
46-
syntax_error(Meta, S#elixir_scope.file,
47-
"invalid args for &: ~ts", ['Elixir.Macro':to_string(Arg)]).
75+
invalid_capture(Meta, Arg, S).
76+
77+
%% Helpers
78+
79+
do_capture(Meta, Expr, S) ->
80+
case do_escape(Expr, S, []) of
81+
{ _, _, [] } ->
82+
invalid_capture(Meta, Expr, S);
83+
{ TExpr, TS, TDict } ->
84+
TVars = validate(Meta, TDict, 1, S),
85+
fn(Meta, [{ TVars, Meta, TExpr }], TS)
86+
end.
87+
88+
invalid_capture(Meta, Arg, S) ->
89+
Message = "invalid args for &, expected an expression in the format of &Mod.fun/arity, "
90+
"&local/arity or a capture containing at least one argument as &1, got: ~ts",
91+
syntax_error(Meta, S#elixir_scope.file, Message, ['Elixir.Macro':to_string(Arg)]).
92+
93+
validate(Meta, [{ Pos, Var }|T], Pos, S) ->
94+
[Var|validate(Meta, T, Pos + 1, S)];
95+
96+
validate(Meta, [{ Pos, _ }|_], Expected, S) ->
97+
compile_error(Meta, S#elixir_scope.file, "capture &~B cannot be defined without &~B", [Pos, Expected]);
98+
99+
validate(_Meta, [], _Pos, _S) ->
100+
[].
101+
102+
do_escape({ '&', Meta, [Pos] }, S, Dict) when is_integer(Pos) ->
103+
case orddict:find(Pos, Dict) of
104+
{ ok, Var } ->
105+
{ Var, S, Dict };
106+
error ->
107+
{ Var, SC } = elixir_scope:build_ex_var(?line(Meta), S),
108+
{ Var, SC, orddict:store(Pos, Var, Dict) }
109+
end;
110+
111+
do_escape({ '&', Meta, _ } = Arg, S, _Dict) ->
112+
Message = "nested captures via & are not allowed: ~ts",
113+
compile_error(Meta, S#elixir_scope.file, Message, ['Elixir.Macro':to_string(Arg)]);
114+
115+
do_escape({ Left, Meta, Right }, S0, Dict0) ->
116+
{ TLeft, S1, Dict1 } = do_escape(Left, S0, Dict0),
117+
{ TRight, S2, Dict2 } = do_escape(Right, S1, Dict1),
118+
{ { TLeft, Meta, TRight }, S2, Dict2 };
119+
120+
do_escape({ Left, Right }, S0, Dict0) ->
121+
{ TLeft, S1, Dict1 } = do_escape(Left, S0, Dict0),
122+
{ TRight, S2, Dict2 } = do_escape(Right, S1, Dict1),
123+
{ { TLeft, TRight }, S2, Dict2 };
124+
125+
do_escape(List, S, Dict) when is_list(List) ->
126+
do_escape_list(List, S, Dict, []);
127+
128+
do_escape(Other, S, Dict) ->
129+
{ Other, S, Dict }.
130+
131+
do_escape_list([H|T], S, Dict, Acc) ->
132+
{ TH, TS, TDict } = do_escape(H, S, Dict),
133+
do_escape_list(T, TS, TDict, [TH|Acc]);
134+
135+
do_escape_list([], S, Dict, Acc) ->
136+
{ lists:reverse(Acc), S, Dict }.
137+
138+
is_sequential(List) -> is_sequential(List, 1).
139+
is_sequential([{ '&', _, [Int] }|T], Int) ->
140+
is_sequential(T, Int + 1);
141+
is_sequential([], Int) when Int > 1 -> true;
142+
is_sequential(_, _Int) -> false.

lib/elixir/src/elixir_import.erl

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
%% in between local functions and imports.
33
%% For imports dispatch, please check elixir_dispatch.
44
-module(elixir_import).
5-
-export([import/5, format_error/1]).
5+
-export([import/5, special_form/2, format_error/1]).
66
-include("elixir.hrl").
77

88
%% IMPORT HELPERS
@@ -139,11 +139,7 @@ get_optional_macros(Module) ->
139139
%% VALIDATION HELPERS
140140

141141
ensure_no_special_form_conflict(Meta, File, Key, [{Name,Arity}|T]) ->
142-
Values = lists:filter(fun({X,Y}) ->
143-
(Name == X) andalso ((Y == '*') orelse (Y == Arity))
144-
end, special_form()),
145-
146-
case Values /= [] of
142+
case special_form(Name, Arity) of
147143
true ->
148144
Tuple = { special_form_conflict, { Key, Name, Arity } },
149145
elixir_errors:form_error(Meta, File, ?MODULE, Tuple);
@@ -198,44 +194,42 @@ remove_internals(Set) ->
198194
ordsets:del_element({ module_info, 1 },
199195
ordsets:del_element({ module_info, 0 }, Set)).
200196

201-
%% Macros implemented in Erlang that are not importable.
202-
203-
special_form() ->
204-
[
205-
{'&',1},
206-
{'^',1},
207-
{'=',2},
208-
{'__op__',2},
209-
{'__op__',3},
210-
{'__scope__',2},
211-
{'__block__','*'},
212-
{'->','*'},
213-
{'<<>>','*'},
214-
{'{}','*'},
215-
{'[]','*'},
216-
{'alias',1},
217-
{'alias',2},
218-
{'require',1},
219-
{'require',2},
220-
{'import',1},
221-
{'import',2},
222-
{'import',3},
223-
{'__ENV__',0},
224-
{'__CALLER__',0},
225-
{'__MODULE__',0},
226-
{'__FILE__',0},
227-
{'__DIR__',0},
228-
{'__aliases__','*'},
229-
{'quote',1},
230-
{'quote',2},
231-
{'unquote',1},
232-
{'unquote_splicing',1},
233-
{'fn','*'},
234-
{'super','*'},
235-
{'super?',0},
236-
{'bc','*'},
237-
{'lc','*'},
238-
{'var!',1},
239-
{'var!',2},
240-
{'alias!',1}
241-
].
197+
%% Special forms
198+
199+
special_form('&',1) -> true;
200+
special_form('^',1) -> true;
201+
special_form('=',2) -> true;
202+
special_form('__op__',2) -> true;
203+
special_form('__op__',3) -> true;
204+
special_form('__scope__',2) -> true;
205+
special_form('__block__',_) -> true;
206+
special_form('->',_) -> true;
207+
special_form('<<>>',_) -> true;
208+
special_form('{}',_) -> true;
209+
special_form('[]',_) -> true;
210+
special_form('alias',1) -> true;
211+
special_form('alias',2) -> true;
212+
special_form('require',1) -> true;
213+
special_form('require',2) -> true;
214+
special_form('import',1) -> true;
215+
special_form('import',2) -> true;
216+
special_form('import',3) -> true;
217+
special_form('__ENV__',0) -> true;
218+
special_form('__CALLER__',0) -> true;
219+
special_form('__MODULE__',0) -> true;
220+
special_form('__FILE__',0) -> true;
221+
special_form('__DIR__',0) -> true;
222+
special_form('__aliases__',_) -> true;
223+
special_form('quote',1) -> true;
224+
special_form('quote',2) -> true;
225+
special_form('unquote',1) -> true;
226+
special_form('unquote_splicing',1) -> true;
227+
special_form('fn',_) -> true;
228+
special_form('super',_) -> true;
229+
special_form('super?',0) -> true;
230+
special_form('bc',_) -> true;
231+
special_form('lc',_) -> true;
232+
special_form('var!',1) -> true;
233+
special_form('var!',2) -> true;
234+
special_form('alias!',1) -> true;
235+
special_form(_, _) -> false.

lib/elixir/test/elixir/kernel/fn_test.exs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,98 @@ Code.require_file "../test_helper.exs", __DIR__
22

33
defmodule Kernel.FnTest do
44
use ExUnit.Case, async: true
5+
import CompileAssertion
56

67
test "capture remote" do
78
assert (&:erlang.atom_to_list/1).(:a) == 'a'
89
assert (&Kernel.atom_to_list/1).(:a) == 'a'
910

1011
assert (&List.flatten/1).([[0]]) == [0]
1112
assert (&(List.flatten/1)).([[0]]) == [0]
13+
assert (&List.flatten(&1)).([[0]]) == [0]
14+
assert &List.flatten(&1) == &List.flatten/1
1215
end
1316

1417
test "capture local" do
1518
assert (&atl/1).(:a) == 'a'
1619
assert (&(atl/1)).(:a) == 'a'
20+
assert (&atl(&1)).(:a) == 'a'
1721
end
1822

1923
test "capture imported" do
2024
assert (&atom_to_list/1).(:a) == 'a'
2125
assert (&(atom_to_list/1)).(:a) == 'a'
26+
assert (&atom_to_list(&1)).(:a) == 'a'
27+
assert &atom_to_list(&1) == &atom_to_list/1
28+
end
29+
30+
test "local partial application" do
31+
assert (&atb(&1, :utf8)).(:a) == "a"
32+
assert (&atb(list_to_atom(&1), :utf8)).('a') == "a"
33+
end
34+
35+
test "imported partial application" do
36+
assert (&atom_to_binary(&1, :utf8)).(:a) == "a"
37+
assert (&atom_to_binary(list_to_atom(&1), :utf8)).('a') == "a"
38+
end
39+
40+
test "remote partial application" do
41+
assert (&:erlang.atom_to_binary(&1, :utf8)).(:a) == "a"
42+
assert (&:erlang.atom_to_binary(list_to_atom(&1), :utf8)).('a') == "a"
43+
end
44+
45+
test "capture and partially apply tuples" do
46+
assert (&{ &1, &2 }).(1, 2) == { 1, 2 }
47+
assert (&{ &1, &2, &3 }).(1, 2, 3) == { 1, 2, 3 }
48+
49+
assert (&{ 1, &1 }).(2) == { 1, 2 }
50+
assert (&{ 1, &1, &2 }).(2, 3) == { 1, 2, 3 }
51+
end
52+
53+
test "capture and partially apply lists" do
54+
assert (&[ &1, &2 ]).(1, 2) == [ 1, 2 ]
55+
assert (&[ &1, &2, &3 ]).(1, 2, 3) == [ 1, 2, 3 ]
56+
57+
assert (&[ 1, &1 ]).(2) == [ 1, 2 ]
58+
assert (&[ 1, &1, &2 ]).(2, 3) == [ 1, 2, 3 ]
59+
60+
assert (&[&1|&2]).(1, 2) == [1|2]
61+
end
62+
63+
test "failure on non-continuous" do
64+
assert_compile_fail CompileError, "nofile:1: capture &2 cannot be defined without &1", "&(&2)"
65+
end
66+
67+
test "failure on integers" do
68+
assert_compile_fail CompileError, "nofile:1: unhandled &1 outside of a capture", "&1"
69+
end
70+
71+
test "failure on block" do
72+
assert_compile_fail SyntaxError,
73+
"nofile:1: invalid args for &, block expressions " <>
74+
"are not allowed, got: (\n 1\n 2\n)",
75+
"&(1;2)"
76+
end
77+
78+
test "failure on other types" do
79+
assert_compile_fail SyntaxError,
80+
"nofile:1: invalid args for &, expected an expression in the format of &Mod.fun/arity, " <>
81+
"&local/arity or a capture containing at least one argument as &1, got: :foo",
82+
"&:foo"
83+
end
84+
85+
test "failure when no captures" do
86+
assert_compile_fail SyntaxError,
87+
"nofile:1: invalid args for &, expected an expression in the format of &Mod.fun/arity, " <>
88+
"&local/arity or a capture containing at least one argument as &1, got: foo()",
89+
"&foo()"
2290
end
2391

2492
defp atl(arg) do
2593
:erlang.atom_to_list arg
2694
end
27-
end
95+
96+
defp atb(arg, encoding) do
97+
:erlang.atom_to_binary(arg, encoding)
98+
end
99+
end

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ defmodule Kernel.QuoteTest.ImportsHygieneTest do
298298

299299
defmacrop get_bin_size_with_partial do
300300
quote do
301-
size(&1).("hello")
301+
(&size(&1)).("hello")
302302
end
303303
end
304304

0 commit comments

Comments
 (0)