Skip to content

Commit 4b43c7a

Browse files
author
José Valim
committed
Do not change code invocation semantics in guards
1 parent ca49a53 commit 4b43c7a

File tree

4 files changed

+49
-61
lines changed

4 files changed

+49
-61
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4570,25 +4570,33 @@ defmodule Kernel do
45704570

45714571
defp define_guard(kind, guard, env) do
45724572
case :elixir_utils.extract_guards(guard) do
4573-
{call, impl} when length(impl) < 2 ->
4573+
{call, [_, _ | _]} ->
4574+
raise ArgumentError,
4575+
"invalid syntax in defguard #{Macro.to_string(call)}, " <>
4576+
"only a single when clause is allowed"
4577+
4578+
{call, impls} ->
45744579
case Macro.decompose_call(call) do
45754580
{_name, args} ->
45764581
validate_variable_only_args!(call, args)
45774582

4578-
quoted =
4579-
quote do
4580-
require Kernel.Utils
4581-
Kernel.Utils.defguard(unquote(args), unquote(impl))
4582-
end
4583+
case impls do
4584+
[] ->
4585+
define(kind, call, nil, env)
4586+
4587+
[guard] ->
4588+
quoted =
4589+
quote do
4590+
require Kernel.Utils
4591+
Kernel.Utils.defguard(unquote(args), unquote(guard))
4592+
end
45834593

4584-
define(kind, call, [do: quoted], env)
4594+
define(kind, call, [do: quoted], env)
4595+
end
45854596

45864597
_invalid_definition ->
45874598
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
45884599
end
4589-
4590-
{call, _multiple_impls} ->
4591-
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
45924600
end
45934601
end
45944602

lib/elixir/lib/kernel/utils.ex

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,9 @@ defmodule Kernel.Utils do
208208
end)
209209
end
210210

211-
# Finds every reference to `refs` in `expr` and wraps them in an unquote.
212-
defp unquote_every_ref(expr, refs) do
213-
Macro.postwalk(expr, fn
211+
# Finds every reference to `refs` in `guard` and wraps them in an unquote.
212+
defp unquote_every_ref(guard, refs) do
213+
Macro.postwalk(guard, fn
214214
{ref, _meta, context} = var when is_atom(ref) and is_atom(context) ->
215215
case {ref, context} in refs do
216216
true -> literal_unquote(var)
@@ -222,10 +222,10 @@ defmodule Kernel.Utils do
222222
end)
223223
end
224224

225-
# Prefaces `expr` with unquoted versions of `refs`.
226-
defp unquote_refs_once(expr, refs) do
227-
{^expr, used_refs} =
228-
Macro.postwalk(expr, [], fn
225+
# Prefaces `guard` with unquoted versions of `refs`.
226+
defp unquote_refs_once(guard, refs) do
227+
{_, used_refs} =
228+
Macro.postwalk(guard, [], fn
229229
{ref, _meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
230230
case {ref, context} in refs and {ref, context} not in acc do
231231
true -> {var, [{ref, context} | acc]}
@@ -236,14 +236,17 @@ defmodule Kernel.Utils do
236236
{node, acc}
237237
end)
238238

239-
for {ref, context} <- :lists.reverse(used_refs) do
240-
var = {ref, [], context}
241-
quote do: unquote(var) = unquote(literal_unquote(var))
242-
end ++ List.wrap(expr)
239+
vars = for {ref, context} <- :lists.reverse(used_refs), do: {ref, [], context}
240+
exprs = for var <- vars, do: literal_unquote(var)
241+
242+
quote do
243+
{unquote_splicing(vars)} = {unquote_splicing(exprs)}
244+
unquote(guard)
245+
end
243246
end
244247

245248
defp literal_quote(ast) do
246-
{:quote, [], [[do: {:__block__, [], List.wrap(ast)}]]}
249+
{:quote, [], [[do: ast]]}
247250
end
248251

249252
defp literal_unquote(ast) do

lib/elixir/test/elixir/kernel/guard_test.exs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ defmodule Kernel.GuardTest do
2121

2222
defmodule Guards.In.Funs do
2323
defguard is_foo(atom) when atom == :foo
24+
defguard is_equal(foo, bar) when foo == bar
2425

2526
def is_foobar(atom) when is_foo(atom) do
2627
is_foo(atom)
@@ -33,6 +34,13 @@ defmodule Kernel.GuardTest do
3334
refute Guards.In.Funs.is_foo(:bar)
3435
end
3536

37+
test "guards do not change code evaluation semantics" do
38+
require Guards.In.Funs
39+
x = 1
40+
assert Guards.In.Funs.is_equal(x = 2, x) == false
41+
assert x == 2
42+
end
43+
3644
defmodule Macros.In.Guards do
3745
defmacro is_foo(atom) do
3846
quote do
@@ -365,9 +373,7 @@ defmodule Kernel.GuardTest do
365373
:erlang.+(:erlang.+(unquote(foo), unquote(bar)), unquote(baz))
366374
end
367375
false -> quote do
368-
foo = unquote(foo)
369-
bar = unquote(bar)
370-
baz = unquote(baz)
376+
{foo, bar, baz} = {unquote(foo), unquote(bar), unquote(baz)}
371377
:erlang.+(:erlang.+(foo, bar), baz)
372378
end
373379
end
@@ -387,8 +393,7 @@ defmodule Kernel.GuardTest do
387393
:erlang.+(unquote(foo), unquote(bar))
388394
end
389395
false -> quote do
390-
foo = unquote(foo)
391-
bar = unquote(bar)
396+
{foo, bar} = {unquote(foo), unquote(bar)}
392397
:erlang.+(foo, bar)
393398
end
394399
end
@@ -410,9 +415,7 @@ defmodule Kernel.GuardTest do
410415
end
411416
false ->
412417
quote() do
413-
foo = unquote(foo)
414-
bar = unquote(bar)
415-
baz = unquote(baz)
418+
{foo, bar, baz} = {unquote(foo), unquote(bar), unquote(baz)}
416419
:erlang.+(:erlang.+(:erlang.+(foo, foo), bar), baz)
417420
end
418421
end

lib/elixir/test/elixir/kernel/warning_test.exs

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ defmodule Kernel.WarningTest do
10921092
end
10931093

10941094
test "unused variable in defguard" do
1095-
capture_err(fn ->
1095+
assert capture_err(fn ->
10961096
Code.eval_string("""
10971097
defmodule Sample do
10981098
defguard foo(bar, baz) when bar
@@ -1129,7 +1129,7 @@ defmodule Kernel.WarningTest do
11291129
end
11301130

11311131
test "defguard overriding defmacro" do
1132-
capture_err(fn ->
1132+
assert capture_err(fn ->
11331133
Code.eval_string("""
11341134
defmodule Sample do
11351135
defmacro foo(bar), do: bar == :bar
@@ -1142,7 +1142,7 @@ defmodule Kernel.WarningTest do
11421142
end
11431143

11441144
test "defmacro overriding defguard" do
1145-
capture_err(fn ->
1145+
assert capture_err(fn ->
11461146
Code.eval_string("""
11471147
defmodule Sample do
11481148
defguard foo(baz) when baz == :baz
@@ -1154,40 +1154,14 @@ defmodule Kernel.WarningTest do
11541154
purge(Sample)
11551155
end
11561156

1157-
test "defguardp overriding defmacrop" do
1158-
capture_err(fn ->
1159-
Code.eval_string("""
1160-
defmodule Sample do
1161-
defmacrop foo(bar), do: bar == :bar
1162-
defguardp foo(baz) when baz == :baz
1163-
end
1164-
""")
1165-
end) =~ "this clause cannot match because a previous clause at line 2 always matches"
1166-
after
1167-
purge(Sample)
1168-
end
1169-
1170-
test "defmacrop overriding defguardp" do
1171-
capture_err(fn ->
1172-
Code.eval_string("""
1173-
defmodule Sample do
1174-
defguardp foo(baz) when baz == :baz
1175-
defmacrop foo(bar), do: bar == :bar
1176-
end
1177-
""")
1178-
end) =~ "this clause cannot match because a previous clause at line 2 always matches"
1179-
after
1180-
purge(Sample)
1181-
end
1182-
11831157
test "defguard needs an implementation" do
1184-
capture_err(fn ->
1158+
assert capture_err(fn ->
11851159
Code.eval_string("""
11861160
defmodule Sample do
11871161
defguard foo(bar)
11881162
end
11891163
""")
1190-
end) =~ "implementation not provided for predefined defguard"
1164+
end) =~ "implementation not provided for predefined defmacro foo/1"
11911165
after
11921166
purge(Sample)
11931167
end

0 commit comments

Comments
 (0)