Skip to content

Commit bc542dc

Browse files
committed
Hoist expressions when creating records, closes #10313
1 parent 5e5f00c commit bc542dc

File tree

2 files changed

+65
-26
lines changed

2 files changed

+65
-26
lines changed

lib/elixir/lib/record.ex

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ defmodule Record do
401401
# Using {} here is safe, since it's not valid AST
402402
default = if Macro.Env.in_match?(caller), do: {:_, [], nil}, else: {}
403403
{default, keyword} = Keyword.pop(keyword, :_, default)
404+
{keyword, exprs} = hoist_expressions(keyword, caller)
404405

405406
{elements, remaining} =
406407
Enum.map_reduce(fields, keyword, fn {key, field_default}, remaining ->
@@ -413,6 +414,7 @@ defmodule Record do
413414
case remaining do
414415
[] ->
415416
quote(do: {unquote(tag), unquote_splicing(elements)})
417+
|> maybe_prepend_reversed_exprs(exprs)
416418

417419
[{key, _} | _] ->
418420
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
@@ -425,27 +427,45 @@ defmodule Record do
425427
raise ArgumentError, "cannot invoke update style macro inside match"
426428
end
427429

430+
{keyword, exprs} = hoist_expressions(keyword, caller)
431+
428432
if Keyword.has_key?(keyword, :_) do
429433
message = "updating a record with a default (:_) is equivalent to creating a new record"
430434
IO.warn(message, Macro.Env.stacktrace(caller))
431435
create(tag, fields, keyword, caller)
432436
else
433-
case build_updates(keyword, fields, [], [], []) do
434-
{updates, [], []} ->
435-
build_update(updates, var)
436-
437-
{updates, vars, exprs} ->
438-
quote do
439-
{unquote_splicing(:lists.reverse(vars))} = {unquote_splicing(:lists.reverse(exprs))}
440-
unquote(build_update(updates, var))
437+
updates =
438+
Enum.map(keyword, fn {key, value} ->
439+
if index = find_index(fields, key, 2) do
440+
{index, value}
441+
else
442+
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
441443
end
444+
end)
442445

443-
{:error, key} ->
444-
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
445-
end
446+
build_update(updates, var) |> maybe_prepend_reversed_exprs(exprs)
446447
end
447448
end
448449

450+
defp hoist_expressions(keyword, %{context: nil}) do
451+
Enum.map_reduce(keyword, [], fn {key, expr}, acc ->
452+
if simple_argument?(expr) do
453+
{{key, expr}, acc}
454+
else
455+
var = Macro.var(key, __MODULE__)
456+
{{key, var}, [{:=, [], [var, expr]} | acc]}
457+
end
458+
end)
459+
end
460+
461+
defp hoist_expressions(keyword, _), do: {keyword, []}
462+
463+
defp maybe_prepend_reversed_exprs(expr, []),
464+
do: expr
465+
466+
defp maybe_prepend_reversed_exprs(expr, exprs),
467+
do: {:__block__, [], :lists.reverse([expr | exprs])}
468+
449469
defp build_update(updates, initial) do
450470
updates
451471
|> Enum.sort(fn {left, _}, {right, _} -> right <= left end)
@@ -454,21 +474,6 @@ defmodule Record do
454474
end)
455475
end
456476

457-
defp build_updates([{key, value} | rest], fields, updates, vars, exprs) do
458-
if index = find_index(fields, key, 2) do
459-
if simple_argument?(value) do
460-
build_updates(rest, fields, [{index, value} | updates], vars, exprs)
461-
else
462-
var = Macro.var(key, __MODULE__)
463-
build_updates(rest, fields, [{index, var} | updates], [var | vars], [value | exprs])
464-
end
465-
else
466-
{:error, key}
467-
end
468-
end
469-
470-
defp build_updates([], _fields, updates, vars, exprs), do: {updates, vars, exprs}
471-
472477
defp simple_argument?({name, _, ctx}) when is_atom(name) and is_atom(ctx), do: true
473478
defp simple_argument?(other), do: Macro.quoted_literal?(other)
474479

lib/elixir/test/elixir/record_test.exs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,40 @@ defmodule RecordTest do
125125
assert match?(user(_: _), user())
126126
end
127127

128+
test "records preserve side-effects order" do
129+
user =
130+
user(
131+
age: send(self(), :age),
132+
name: send(self(), :name)
133+
)
134+
135+
assert Process.info(self(), :messages) == {:messages, [:age, :name]}
136+
137+
_ =
138+
user(user,
139+
age: send(self(), :update_age),
140+
name: send(self(), :update_name)
141+
)
142+
143+
assert Process.info(self(), :messages) ==
144+
{:messages, [:age, :name, :update_age, :update_name]}
145+
end
146+
147+
test "nested records preserve side-effects order" do
148+
user =
149+
user(
150+
age:
151+
user(
152+
age: send(self(), :inner_age),
153+
name: send(self(), :inner_name)
154+
),
155+
name: send(self(), :name)
156+
)
157+
158+
assert user == {RecordTest, :name, {RecordTest, :inner_name, :inner_age}}
159+
assert Process.info(self(), :messages) == {:messages, [:inner_age, :inner_name, :name]}
160+
end
161+
128162
Record.defrecord(
129163
:defaults,
130164
struct: ~D[2016-01-01],

0 commit comments

Comments
 (0)