Skip to content

Commit c7f67bc

Browse files
author
José Valim
committed
Rewrite records accessors when possible
1 parent 82cf622 commit c7f67bc

File tree

8 files changed

+201
-49
lines changed

8 files changed

+201
-49
lines changed

lib/elixir/lib/kernel/record_rewriter.ex

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,75 @@ defmodule Kernel.RecordRewriter do
3838
optimize_body(t, new_dict, [new_expr|acc])
3939
end
4040

41+
## Record helpers
42+
43+
defp record_fields(record) do
44+
if Code.ensure_loaded?(record) && function_exported?(record, :__record__, 1) do
45+
try do
46+
fields = lc { k, _ } inlist record.__record__(:fields), do: k
47+
optimizable = record.__record__(:optimizable)
48+
{ fields, optimizable }
49+
rescue
50+
[UndefinedFunctionError, FunctionClauseError] -> { [], [] }
51+
end
52+
end
53+
end
54+
55+
defp record_field_info(function) do
56+
case atom_to_list(function) do
57+
'update_' ++ field -> { :update, list_to_atom(function) }
58+
_ -> { :accessor, function }
59+
end
60+
end
61+
62+
defp optimize_call(line, { record, _ } = res, left, { :atom, _, function }, args) do
63+
{ fields, optimizable } = record_fields(record)
64+
65+
if List.member?(optimizable, { function, length(args) + 1 }) do
66+
{ kind, field } = record_field_info(function)
67+
if index = Enum.find_index(fields, field == &1) do
68+
optimize_call(line, res, kind, index, left, args)
69+
end
70+
end
71+
end
72+
73+
defp optimize_call(_line, _res, _left, _right, _args) do
74+
nil
75+
end
76+
77+
defp optimize_call(line, _res, :accessor, index, left, []) do
78+
call = { :call, line,
79+
{ :remote, line, { :atom, 0, :erlang }, { :atom, 0, :element } },
80+
[{ :integer, 0, index + 2 }, left]
81+
}
82+
{ call, nil }
83+
end
84+
85+
defp optimize_call(line, res, :accessor, index, left, [arg]) do
86+
call = { :call, line,
87+
{ :remote, line, { :atom, 0, :erlang }, { :atom, 0, :setelement } },
88+
[{ :integer, 0, index + 2 }, left, arg]
89+
}
90+
{ call, res }
91+
end
92+
93+
defp optimize_call(_line, _res, :update, _index, _left, [_arg]) do
94+
nil
95+
end
96+
4197
## Expr
4298

4399
defp optimize_expr({ :call, call_line, { :remote, line, left, right }, args }, dict) do
44-
{ left, dict, _ } = optimize_expr(left, dict)
100+
{ left, dict, res } = optimize_expr(left, dict)
45101
{ right, dict, _ } = optimize_expr(right, dict)
46102
{ args, dict } = optimize_args(args, dict)
47-
{ { :call, call_line, { :remote, line, left, right }, args }, dict, nil }
103+
104+
case optimize_call(call_line, res, left, right, args) do
105+
{ call, call_res } ->
106+
{ call, dict, call_res }
107+
nil ->
108+
{ { :call, call_line, { :remote, line, left, right }, args }, dict, nil }
109+
end
48110
end
49111

50112
defp optimize_expr({ :call, line, expr, args }, dict) do
@@ -212,21 +274,19 @@ defmodule Kernel.RecordRewriter do
212274
end
213275

214276
defp assign_vars([key|t], dict, { value, _ } = res) when is_atom(key) and value != nil do
215-
if is_record?(value) do
216-
dict =
217-
case :orddict.find(key, dict) do
218-
{ :ok, ^res } ->
219-
dict
220-
{ :ok, { ^value, _ } } ->
221-
:orddict.store(key, { value, nil }, dict)
222-
{ :ok, _ } ->
223-
# We are overriding a type of an existing variable,
224-
# which means the source code is invalid.
225-
:orddict.store(key, nil, dict)
226-
:error ->
227-
:orddict.store(key, res, dict)
228-
end
229-
end
277+
dict =
278+
case :orddict.find(key, dict) do
279+
{ :ok, ^res } ->
280+
dict
281+
{ :ok, { ^value, _ } } ->
282+
:orddict.store(key, { value, nil }, dict)
283+
{ :ok, _ } ->
284+
# We are overriding a type of an existing variable,
285+
# which means the source code is invalid.
286+
:orddict.store(key, nil, dict)
287+
:error ->
288+
:orddict.store(key, res, dict)
289+
end
230290

231291
assign_vars t, dict, res
232292
end
@@ -305,11 +365,4 @@ defmodule Kernel.RecordRewriter do
305365
defp join_result([], res) do
306366
res
307367
end
308-
309-
## Record helpers
310-
311-
# TODO: Implement proper record check
312-
defp is_record?(_h) do
313-
true
314-
end
315368
end

lib/elixir/lib/macro/env.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defmodule Macro.Env do
1616
functions: functions, macros: macros]
1717

1818
Record.deffunctions(fields, __MODULE__)
19-
Record.deftypes(fields, types, __ENV__)
19+
Record.deftypes(fields, types, __MODULE__)
2020

2121
@moduledoc """
2222
A record that contains compile time environment information,

lib/elixir/lib/module.ex

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
defmodule Module do
22
require :ets, as: ETS
33

4+
defmacrop is_env(env) do
5+
quote do
6+
is_tuple(unquote(env)) and size(unquote(env)) > 1 and elem(unquote(env), 0) == Macro.Env
7+
end
8+
end
9+
410
@moduledoc """
511
This module provides many functions to deal with modules during
612
compilation time. It allows a developer to dynamically attach
@@ -51,11 +57,11 @@ defmodule Module do
5157
"""
5258
def eval_quoted(module, quoted, binding // [], opts // [])
5359

54-
def eval_quoted(Macro.Env[module: module] = env, quoted, binding, opts) do
55-
eval_quoted(module, quoted, binding, Keyword.merge(env.to_keywords, opts))
60+
def eval_quoted(env, quoted, binding, opts) when is_env(env) do
61+
eval_quoted(env.module, quoted, binding, Keyword.merge(env.to_keywords, opts))
5662
end
5763

58-
def eval_quoted(module, quoted, binding, Macro.Env[] = env) do
64+
def eval_quoted(module, quoted, binding, env) when is_env(env) do
5965
eval_quoted(module, quoted, binding, env.to_keywords)
6066
end
6167

@@ -95,12 +101,12 @@ defmodule Module do
95101
"""
96102
def create(module, quoted, opts // [])
97103

98-
def create(module, quoted, Macro.Env[] = env) do
104+
def create(module, quoted, env) when is_env(env) do
99105
create(module, quoted, env.to_keywords)
100106
end
101107

102108
def create(module, quoted, opts) when is_atom(module) do
103-
line = opts[:line] || 1
109+
line = Keyword.get(opts, :line, 1)
104110
:elixir_module.compile(line, module, quoted, [], :elixir.scope_for_eval(opts))
105111
end
106112

@@ -578,7 +584,7 @@ defmodule Module do
578584
atom
579585
end
580586

581-
defp normalize_attribute(:file, Macro.Env[file: file, line: line]), do: { binary_to_list(file), line}
587+
defp normalize_attribute(:file, env) when is_env(env), do: { binary_to_list(env.file), env.line}
582588
defp normalize_attribute(:file, { binary, line }) when is_binary(binary), do: { binary_to_list(binary), line }
583589
defp normalize_attribute(:file, other) when not is_tuple(other), do: normalize_attribute(:file, { other, 1 })
584590

lib/elixir/lib/record.ex

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,18 @@ defmodule Record do
7373
reflection(escaped),
7474
initializer(escaped),
7575
indexes(escaped),
76-
accessors(values, 1, []),
7776
conversions(values),
7877
updater(values),
79-
extensions(values, 1, [], Record.Extensions)
78+
extensions(values, 1, [], Record.Extensions),
79+
accessors(values),
8080
]
8181

82+
contents = [quote(do: @__record__ unquote(escaped))|contents]
83+
8284
# Special case for bootstraping purposes
8385
if env == Macro.Env do
84-
:elixir_module.eval_quoted(env, contents, [], [])
86+
Module.eval_quoted(env, contents, [], [])
8587
else
86-
contents = [quote(do: @__record__ unquote(escaped))|contents]
8788
Module.eval_quoted(env.module, contents, [], env.location)
8889
end
8990
end
@@ -103,8 +104,9 @@ defmodule Record do
103104
accessor_specs(values, 1, [])
104105
]
105106

106-
# Special case for bootstraping purposes
107-
if :erlang.function_exported(Module, :eval_quoted, 2) do
107+
if env == Macro.Env do
108+
Module.eval_quoted(env, contents, [], [])
109+
else
108110
Module.eval_quoted(env.module, contents, [], env.location)
109111
end
110112
end
@@ -152,6 +154,34 @@ defmodule Record do
152154
Module.eval_quoted(env.module, contents, [], env.location)
153155
end
154156

157+
## Callbacks
158+
159+
# Store all optimizable fields in the record as well
160+
@doc false
161+
defmacro __before_compile__(_) do
162+
quote do
163+
def __record__(:optimizable), do: @record_optimizable
164+
end
165+
end
166+
167+
# Store fields that can be optimized and that cannot be
168+
# optimized as they are overriden
169+
@doc false
170+
def __on_definition__(env, kind, name, args, _guards, _body) do
171+
tuple = { name, length(args) }
172+
module = env.module
173+
functions = Module.get_attribute(module, :record_optimizable)
174+
175+
functions =
176+
if kind in [:def] and Module.get_attribute(module, :record_optimized) do
177+
[tuple|functions]
178+
else
179+
List.delete(functions, tuple)
180+
end
181+
182+
Module.put_attribute(module, :record_optimizable, functions)
183+
end
184+
155185
# Implements the access macro used by records.
156186
# It returns a quoted expression that defines
157187
# a record or a match in case the record is
@@ -414,11 +444,20 @@ defmodule Record do
414444
# setelem(record, 2, callback.(elem(record, 2)))
415445
# end
416446
#
417-
defp accessors([{ :__exception__, _ }|t], 1, acc) do
418-
accessors(t, 2, acc)
447+
defp accessors(values) do
448+
[ quote do
449+
@record_optimized true
450+
@record_optimizable []
451+
@before_compile { unquote(__MODULE__), :__before_compile__ }
452+
@on_definition { unquote(__MODULE__), :__on_definition__ }
453+
end | accessors(values, 1) ]
454+
end
455+
456+
defp accessors([{ :__exception__, _ }|t], 1) do
457+
accessors(t, 2)
419458
end
420459

421-
defp accessors([{ key, _default }|t], i, acc) do
460+
defp accessors([{ key, _default }|t], i) do
422461
update = binary_to_atom "update_" <> atom_to_binary(key)
423462

424463
contents = quote do
@@ -436,10 +475,12 @@ defmodule Record do
436475
end
437476
end
438477

439-
accessors(t, i + 1, [contents | acc])
478+
[contents|accessors(t, i + 1)]
440479
end
441480

442-
defp accessors([], _i, acc), do: acc
481+
defp accessors([], _i) do
482+
[quote do: @record_optimized false]
483+
end
443484

444485
# Define an updater method that receives a
445486
# keyword list and updates the record.

lib/elixir/src/elixir_compiler.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ core_main() ->
159159
"lib/elixir/lib/keyword.ex",
160160
"lib/elixir/lib/list.ex",
161161
"lib/elixir/lib/kernel/typespec.ex",
162+
"lib/elixir/lib/module.ex",
162163
"lib/elixir/lib/record.ex",
163164
"lib/elixir/lib/record/extractor.ex",
164165
"lib/elixir/lib/macro.ex",
165166
"lib/elixir/lib/macro/env.ex",
166-
"lib/elixir/lib/module.ex",
167167
"lib/elixir/lib/code.ex",
168168
"lib/elixir/lib/protocol.ex",
169169
"lib/elixir/lib/enum.ex",

lib/elixir/src/elixir_translator.erl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -500,11 +500,9 @@ translate_apply(Line, TLeft, TRight, Args, S, SL, SR) ->
500500
Optimize = case (Args == []) orelse lists:last(Args) of
501501
{ '|', _, _ } -> false;
502502
_ ->
503-
case { TLeft, TRight } of
504-
{ { Kind, _, _ }, { atom, _, _ } } when Kind == var; Kind == tuple; Kind == atom ->
505-
true;
506-
_ ->
507-
false
503+
case TRight of
504+
{ atom, _, _ } -> true;
505+
_ -> false
508506
end
509507
end,
510508

lib/elixir/test/elixir/kernel/record_rewriter_test.exs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
Code.require_file "../../test_helper.exs", __FILE__
22

3+
defrecord BadRange, first: 0, last: 0 do
4+
defoverridable [first: 1]
5+
6+
def first(_) do
7+
:not_optimizable
8+
end
9+
end
10+
311
defmodule Kernel.RecordRewriterTest do
412
use ExUnit.Case, async: true
513

@@ -21,7 +29,7 @@ defmodule Kernel.RecordRewriterTest do
2129
{ clause, dict, res }
2230
end
2331

24-
## Dictionary tests
32+
## Inference tests
2533

2634
test "simple atom" do
2735
clause = clause(fn -> :foo end)
@@ -198,4 +206,37 @@ defmodule Kernel.RecordRewriterTest do
198206
clause = clause(fn -> try do x = Macro.Env[]; x; after x = Macro.Env[]; end end)
199207
assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }
200208
end
209+
210+
## Rewrite tests
211+
212+
test "getter call is rewriten" do
213+
{ clause, rewriten } =
214+
{ clause(fn(x = Range[]) -> x.first end), clause(fn(x = Range[]) -> :erlang.element(2, x) end) }
215+
216+
assert optimize_clause(clause) == { rewriten, [x: Range], nil }
217+
end
218+
219+
test "setter call is rewriten" do
220+
{ clause, rewriten } =
221+
{ clause(fn(x = Range[]) -> x.first(:first) end), clause(fn(x = Range[]) -> :erlang.setelement(2, x, :first) end) }
222+
223+
assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
224+
end
225+
226+
test "nested setter call is rewriten" do
227+
{ clause, rewriten } =
228+
{ clause(fn(x = Range[]) -> x.first(:first).last(:last) end), clause(fn(x = Range[]) -> :erlang.setelement(3, :erlang.setelement(2, x, :first), :last) end) }
229+
230+
assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
231+
end
232+
233+
test "noop for unknown fields" do
234+
clause = clause(fn(x = Range[]) -> x.unknown end)
235+
assert optimize_clause(clause) == { clause, [x: Range], nil }
236+
end
237+
238+
test "noop for rewriten fields" do
239+
clause = clause(fn(x = BadRange[]) -> x.first end)
240+
assert optimize_clause(clause) == { clause, [x: BadRange], nil }
241+
end
201242
end

0 commit comments

Comments
 (0)