Skip to content

Commit 845a3d1

Browse files
author
José Valim
committed
Make defstruct work on quoted expressions, closes #2156
1 parent 8b70e07 commit 845a3d1

File tree

8 files changed

+109
-38
lines changed

8 files changed

+109
-38
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3197,33 +3197,45 @@ defmodule Kernel do
31973197
31983198
To define a struct, a developer needs to only define
31993199
a function named `__struct__/0` that returns a map with the
3200-
structs field. This macro is simply a convenience for doing so.
3201-
3202-
Finally, this macro also defines a type t in the current
3203-
module unless one was previously defined.
3200+
structs field. This macro is a convenience for doing such
3201+
function and a type `t` in one pass.
32043202
32053203
## Examples
32063204
32073205
defmodule MyRange do
32083206
defstruct first: nil, last: nil
32093207
end
32103208
3209+
Notice `defstruct` requires a keyword list of fields and values
3210+
at expansion time. In other words, `defstruct` works with quoted
3211+
expressions. In other words, a struct defined like:
3212+
3213+
defmodule MyRange do
3214+
defstruct first: nil, last: 1 + 1
3215+
end
3216+
3217+
Will execute `1 + 1` every time the struct is built. This also
3218+
implies the following leads to an error:
3219+
3220+
defmodule MyRange do
3221+
my_fields = [first: nil, last: nil]
3222+
defstruct my_fields
3223+
end
3224+
32113225
"""
3212-
defmacro defstruct(opts) do
3213-
quote bind_quoted: [opts: opts] do
3214-
opts = Enum.map(opts, fn
3215-
{ key, _ } = pair when is_atom(key) -> pair
3216-
key when is_atom(key) -> { key, nil }
3217-
other -> raise ArgumentError, message: "struct fields must be atoms, got: #{inspect other}"
3218-
end)
3226+
defmacro defstruct(kv) do
3227+
kv = Macro.escape(kv, unquote: true)
3228+
quote bind_quoted: [kv: kv] do
3229+
# TODO: Use those types once we support maps typespecs.
3230+
{ fields, _types } = Record.Backend.split_fields_and_types(:defstruct, kv)
32193231

32203232
if :code.ensure_loaded(Kernel.Typespec) == { :module, Kernel.Typespec } and
32213233
not Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
32223234
@type t :: map
32233235
end
32243236

32253237
def __struct__() do
3226-
%{ unquote_splicing(Macro.escape(opts)), __struct__: __MODULE__ }
3238+
%{ unquote_splicing(fields), __struct__: __MODULE__ }
32273239
end
32283240
end
32293241
end

lib/elixir/lib/record.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ defmodule Record do
172172
173173
"""
174174
def extract(name, opts) do
175-
Record.Extractor.retrieve(name, opts)
175+
Record.Extractor.extract(name, opts)
176176
end
177177

178178
@doc false

lib/elixir/lib/record/backend.ex

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
defmodule Record.Backend do
2+
# Callback functions invoked by defrecord, defrecordp and friends.
3+
@moduledoc false
4+
5+
@doc """
6+
Splits a keywords list into fields and types.
7+
8+
This logic is shared by records and structs.
9+
"""
10+
def split_fields_and_types(tag, kv) when is_list(kv) do
11+
split_fields_and_types(tag, kv, [], [])
12+
end
13+
14+
def split_fields_and_types(tag, other) do
15+
raise ArgumentError, message: "#{tag} fields must be a keyword list, got: #{Macro.to_string other}"
16+
end
17+
18+
defp split_fields_and_types(tag, [{ field, { :::, _, [default, type] }}|t], fields, types) do
19+
split_fields_and_types(tag, t, [{ field, default }|fields], [{ field, type }|types])
20+
end
21+
22+
defp split_fields_and_types(tag, [{ field, default }|t], fields, types) when is_atom(field) do
23+
split_fields_and_types(tag, t, [{ field, default }|fields], [{ field, quote(do: term) }|types])
24+
end
25+
26+
defp split_fields_and_types(tag, [field|t], fields, types) when is_atom(field) do
27+
split_fields_and_types(tag, t, [{ field, nil }|fields], [{ field, quote(do: term) }|types])
28+
end
29+
30+
defp split_fields_and_types(tag, [other|_], _fields, _types) do
31+
raise ArgumentError, message: "#{tag} fields must be atoms, got: #{Macro.to_string other}"
32+
end
33+
34+
defp split_fields_and_types(_tag, [], fields, types) do
35+
{ :lists.reverse(fields), :lists.reverse(types) }
36+
end
37+
end

lib/elixir/lib/record/extractor.ex

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Record.Extractor do
33

44
# Retrieve a record definition from an Erlang file using
55
# the same lookup as the *include* attribute from Erlang modules.
6-
def retrieve(name, from: file) when is_binary(file) do
6+
def extract(name, from: file) when is_binary(file) do
77
file = String.to_char_list!(file)
88

99
realfile =
@@ -12,35 +12,35 @@ defmodule Record.Extractor do
1212
realfile -> realfile
1313
end
1414

15-
retrieve_record(name, realfile)
15+
extract_record(name, realfile)
1616
end
1717

1818
# Retrieve a record definition from an Erlang file using
1919
# the same lookup as the *include_lib* attribute from Erlang modules.
20-
def retrieve(name, from_lib: file) when is_binary(file) do
20+
def extract(name, from_lib: file) when is_binary(file) do
2121
[app|path] = :filename.split(String.to_char_list!(file))
2222

2323
case :code.lib_dir(list_to_atom(app)) do
2424
{ :error, _ } ->
2525
raise ArgumentError, message: "lib file #{file} could not be found"
2626
libpath ->
27-
retrieve_record name, :filename.join([libpath|path])
27+
extract_record name, :filename.join([libpath|path])
2828
end
2929
end
3030

3131
# Retrieve the record with the given name from the given file
32-
defp retrieve_record(name, file) do
32+
defp extract_record(name, file) do
3333
form = read_file(file)
34-
records = retrieve_records(form)
34+
records = extract_records(form)
3535
if record = List.keyfind(records, name, 0) do
3636
parse_record(record, form)
3737
else
3838
raise ArgumentError, message: "no record #{name} found at #{file}"
3939
end
4040
end
4141

42-
# Parse the given file and retrieve all existent records.
43-
defp retrieve_records(form) do
42+
# Parse the given file and extract all existent records.
43+
defp extract_records(form) do
4444
for { :attribute, _, :record, record } <- form, do: record
4545
end
4646

lib/elixir/src/elixir_compiler.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ core_main() ->
175175
<<"lib/elixir/lib/module.ex">>,
176176
<<"lib/elixir/lib/list.ex">>,
177177
<<"lib/elixir/lib/record.ex">>,
178+
<<"lib/elixir/lib/record/backend.ex">>,
178179
<<"lib/elixir/lib/macro.ex">>,
179180
<<"lib/elixir/lib/macro/env.ex">>,
180181
<<"lib/elixir/lib/exception.ex">>,

lib/elixir/src/elixir_map.erl

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ translate_map(Meta, Args, S) ->
5252

5353
translate_struct(Meta, Name, { '%{}', MapMeta, Args }, S) ->
5454
{ Assocs, TUpdate, US } = extract_assoc_update(Args, S),
55-
Struct = load_struct(Meta, Name),
55+
Struct = load_struct(Meta, Name, S),
5656

5757
case is_map(Struct) of
5858
true ->
@@ -89,24 +89,31 @@ translate_struct(Meta, Name, { '%{}', MapMeta, Args }, S) ->
8989

9090
%% Helpers
9191

92-
load_struct(Meta, Name) ->
92+
load_struct(Meta, Name, S) ->
9393
Local =
9494
elixir_module:is_open(Name) andalso
9595
(case lists:keyfind(struct, 1, Meta) of
9696
{ struct, context } -> true;
9797
_ -> wait_for_struct(Name)
9898
end),
9999

100-
case Local of
101-
true ->
102-
try
103-
(elixir_locals:local_for(Name, '__struct__', 0, def))()
104-
catch
105-
error:undef -> Name:'__struct__'();
106-
error:badarg -> Name:'__struct__'()
107-
end;
108-
false ->
109-
Name:'__struct__'()
100+
try
101+
case Local of
102+
true ->
103+
try
104+
(elixir_locals:local_for(Name, '__struct__', 0, def))()
105+
catch
106+
error:undef -> Name:'__struct__'();
107+
error:badarg -> Name:'__struct__'()
108+
end;
109+
false ->
110+
Name:'__struct__'()
111+
end
112+
catch
113+
error:undef ->
114+
Inspected = elixir_aliases:inspect(Name),
115+
compile_error(Meta, S#elixir_scope.file, "~ts.__struct__/0 is undefined, "
116+
"cannot expand struct ~ts", [Inspected, Inspected])
110117
end.
111118

112119
wait_for_struct(Module) ->

lib/elixir/test/elixir/kernel/errors_test.exs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ defmodule Kernel.ErrorsTest do
195195
'%{ :a, :b }{ a: :b }'
196196
end
197197

198+
test :struct_fields_on_defstruct do
199+
assert_compile_fail ArgumentError,
200+
"defstruct fields must be a keyword list, got: my_fields",
201+
'''
202+
defmodule TZ do
203+
defstruct my_fields
204+
end
205+
'''
206+
end
207+
198208
test :struct_access_on_body do
199209
assert_compile_fail CompileError,
200210
"nofile:3: cannot access struct TZ in body of the module that defines it " <>
@@ -218,8 +228,8 @@ defmodule Kernel.ErrorsTest do
218228
end
219229

220230
test :struct_errors do
221-
assert_compile_fail UndefinedFunctionError,
222-
"undefined function: BadStruct.__struct__/0",
231+
assert_compile_fail CompileError,
232+
"nofile:1: BadStruct.__struct__/0 is undefined, cannot expand struct BadStruct",
223233
'%BadStruct{}'
224234

225235
defmodule BadStruct do

lib/elixir/test/elixir/map_test.exs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,11 @@ defmodule MapTest do
125125
end
126126

127127
defmodule LocalUser do
128-
defstruct name: "josé"
128+
defmodule NestedUser do
129+
defstruct []
130+
end
131+
132+
defstruct name: "josé", nested: %NestedUser{}
129133

130134
def new do
131135
%LocalUser{}
@@ -139,8 +143,8 @@ defmodule MapTest do
139143
end
140144

141145
test "local user" do
142-
assert LocalUser.new == %LocalUser{name: "josé"}
143-
assert LocalUser.Context.new == %LocalUser{name: "josé"}
146+
assert LocalUser.new == %LocalUser{ name: "josé", nested: %LocalUser.NestedUser{} }
147+
assert LocalUser.Context.new == %LocalUser{ name: "josé", nested: %LocalUser.NestedUser{} }
144148
end
145149

146150
defmodule NilUser do

0 commit comments

Comments
 (0)