Skip to content

Commit c7fa5f9

Browse files
committed
Add Macro.struct_info!
1 parent 90f504e commit c7fa5f9

17 files changed

+187
-176
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5459,7 +5459,7 @@ defmodule Kernel do
54595459
defmacro defstruct(fields) do
54605460
header =
54615461
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
5462-
{struct, derive, kv, body} =
5462+
{struct, derive, escaped_struct, kv, body} =
54635463
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
54645464

54655465
case derive do
@@ -5473,7 +5473,7 @@ defmodule Kernel do
54735473
# especially since they are often expanded at compile-time.
54745474
functions =
54755475
quote line: 0, unquote: false do
5476-
def __struct__(), do: @__struct__
5476+
def __struct__(), do: unquote(escaped_struct)
54775477
def __struct__(unquote(kv)), do: unquote(body)
54785478
end
54795479

lib/elixir/lib/kernel/typespec.ex

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -567,27 +567,23 @@ defmodule Kernel.Typespec do
567567
defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do
568568
case Macro.expand(name, %{caller | function: {:__info__, 1}}) do
569569
module when is_atom(module) ->
570-
struct =
571-
module
572-
|> Macro.struct!(caller)
573-
|> Map.delete(:__struct__)
574-
|> Map.to_list()
570+
struct_info = Macro.struct_info!(module, caller)
575571

576572
if not Keyword.keyword?(fields) do
577573
compile_error(caller, "expected key-value pairs in struct #{Macro.to_string(name)}")
578574
end
579575

580576
types =
581577
:lists.map(
582-
fn
583-
{:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)}
584-
{field, _} -> {field, Keyword.get(fields, field, quote(do: term()))}
578+
fn %{field: field} ->
579+
default_type = if field == :__exception__, do: true, else: quote(do: term())
580+
{field, Keyword.get(fields, field, default_type)}
585581
end,
586-
:lists.sort(struct)
582+
struct_info
587583
)
588584

589585
fun = fn {field, _} ->
590-
if not Keyword.has_key?(struct, field) do
586+
if not Enum.any?(struct_info, &(&1.field == field)) do
591587
compile_error(
592588
caller,
593589
"undefined field #{inspect(field)} on struct #{inspect(module)}"
@@ -596,7 +592,7 @@ defmodule Kernel.Typespec do
596592
end
597593

598594
:lists.foreach(fun, fields)
599-
typespec({:%{}, meta, [__struct__: module] ++ types}, vars, caller, state)
595+
typespec({:%{}, meta, [__struct__: module] ++ :lists.usort(types)}, vars, caller, state)
600596

601597
_ ->
602598
compile_error(

lib/elixir/lib/kernel/utils.ex

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ defmodule Kernel.Utils do
103103
def defstruct(module, fields, bootstrapped?, env) do
104104
{set, bag} = :elixir_module.data_tables(module)
105105

106-
if :ets.member(set, :__struct__) do
106+
if :ets.member(set, {:elixir, :struct}) do
107107
raise ArgumentError,
108108
"defstruct has already been called for " <>
109109
"#{Kernel.inspect(module)}, defstruct can only be called once per module"
@@ -119,6 +119,8 @@ defmodule Kernel.Utils do
119119

120120
mapper = fn
121121
{key, val} when is_atom(key) ->
122+
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
123+
122124
try do
123125
:elixir_quote.escape(val, :none, false)
124126
rescue
@@ -129,6 +131,7 @@ defmodule Kernel.Utils do
129131
end
130132

131133
key when is_atom(key) ->
134+
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
132135
{key, nil}
133136

134137
other ->
@@ -163,23 +166,24 @@ defmodule Kernel.Utils do
163166
end
164167

165168
:lists.foreach(foreach, enforce_keys)
166-
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
169+
struct = :maps.from_list([__struct__: module] ++ fields)
170+
escaped_struct = :elixir_quote.escape(struct, :none, false)
167171

168172
body =
169173
case bootstrapped? do
170174
true ->
171175
case enforce_keys do
172176
[] ->
173177
quote do
174-
Enum.reduce(kv, @__struct__, fn {key, val}, map ->
178+
Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map ->
175179
%{map | key => val}
176180
end)
177181
end
178182

179183
_ ->
180184
quote do
181185
{map, keys} =
182-
Enum.reduce(kv, {@__struct__, unquote(enforce_keys)}, fn
186+
Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn
183187
{key, val}, {map, keys} ->
184188
{%{map | key => val}, List.delete(keys, key)}
185189
end)
@@ -200,25 +204,21 @@ defmodule Kernel.Utils do
200204
quote do
201205
:lists.foldl(
202206
fn {key, val}, acc -> %{acc | key => val} end,
203-
@__struct__,
207+
unquote(escaped_struct),
204208
kv
205209
)
206210
end
207211
end
208212

209213
case enforce_keys -- :maps.keys(struct) do
210214
[] ->
211-
# The __struct__ attribute is during expansion and for loading remote structs
212-
:ets.insert(set, {:__struct__, struct, nil, []})
213-
214-
# The complete metadata goes into __info__(:struct)
215215
mapper = fn {key, val} ->
216216
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
217217
end
218218

219219
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
220220
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
221-
{struct, :lists.reverse(derive), quote(do: kv), body}
221+
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
222222

223223
error_keys ->
224224
raise ArgumentError,

lib/elixir/lib/macro.ex

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -827,30 +827,36 @@ defmodule Macro do
827827
:elixir_quote.escape(expr, kind, unquote)
828828
end
829829

830+
# TODO: Deprecate me on Elixir v1.22
831+
@doc false
832+
def struct!(module, env) when is_atom(module) do
833+
pairs =
834+
for %{field: field, default: default} <- struct_info!(module, env), do: {field, default}
835+
836+
:maps.from_list([__struct__: module] ++ pairs)
837+
end
838+
830839
@doc """
831-
Expands the struct given by `module` in the given `env`.
840+
Extracts the struct information (equivalent to calling
841+
`module.__info__(:struct)`).
832842
833843
This is useful when a struct needs to be expanded at
834844
compilation time and the struct being expanded may or may
835845
not have been compiled. This function is also capable of
836846
expanding structs defined under the module being compiled.
847+
Calling this function also adds an export dependency on the
848+
given struct.
837849
838-
It will raise `CompileError` if the struct is not available.
839-
From Elixir v1.12, calling this function also adds an export
840-
dependency on the given struct.
850+
It will raise `ArgumentError` if the struct is not available.
841851
"""
842-
@doc since: "1.8.0"
843-
@spec struct!(module, Macro.Env.t()) ::
844-
%{required(:__struct__) => module, optional(atom) => any}
845-
when module: module()
846-
def struct!(module, env) when is_atom(module) do
847-
if module == env.module do
848-
Module.get_attribute(module, :__struct__)
849-
end ||
850-
case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do
851-
{:ok, struct} -> struct
852-
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
853-
end
852+
@doc since: "1.18.0"
853+
@spec struct_info!(module(), Macro.Env.t()) ::
854+
[%{field: atom(), required: boolean(), default: term()}]
855+
def struct_info!(module, env) when is_atom(module) do
856+
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], env) do
857+
{:ok, info} -> info
858+
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
859+
end
854860
end
855861

856862
@doc """

lib/elixir/lib/macro/env.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ defmodule Macro.Env do
102102
]
103103

104104
# Define the __struct__ callbacks by hand for bootstrap reasons.
105-
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
106-
def __struct__(), do: unquote(:elixir_quote.escape(struct, :none, false))
105+
{_struct, [], escaped_struct, kv, body} =
106+
Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
107+
108+
def __struct__(), do: unquote(escaped_struct)
107109
def __struct__(unquote(kv)), do: unquote(body)
108110

109111
@doc """

lib/elixir/lib/module.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1944,7 +1944,7 @@ defmodule Module do
19441944

19451945
_ ->
19461946
message =
1947-
case :ets.lookup(set, :__struct__) do
1947+
case :ets.lookup(set, {:elixir, :struct}) do
19481948
[] ->
19491949
"warning: module attribute @derive was set but never used (it must come before defstruct)"
19501950

lib/elixir/src/elixir.hrl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-define(key(M, K), maps:get(K, M)).
1+
-define(key(M, K), map_get(K, M)).
22
-define(ann(Meta), elixir_erl:get_ann(Meta)).
33
-define(line(Meta), elixir_utils:get_line(Meta)).
44
-define(generated(Meta), elixir_utils:generated(Meta)).

lib/elixir/src/elixir_erl.erl

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,21 @@ elixir_to_erl(Tree, Ann) when is_binary(Tree) ->
9090
%% considers a string in a binary to be encoded in latin1, so the bytes
9191
%% are not changed in any fashion.
9292
{bin, Ann, [{bin_element, Ann, {string, Ann, binary_to_list(Tree)}, default, default}]};
93-
elixir_to_erl(Pid, Ann) when is_pid(Pid) ->
94-
?remote(Ann, erlang, binary_to_term, [elixir_to_erl(term_to_binary(Pid), Ann)]);
95-
elixir_to_erl(_Other, _Ann) ->
96-
error(badarg).
93+
elixir_to_erl(Tree, Ann) when is_function(Tree) ->
94+
case (erlang:fun_info(Tree, type) == {type, external}) andalso
95+
(erlang:fun_info(Tree, env) == {env, []}) of
96+
true ->
97+
{module, Module} = erlang:fun_info(Tree, module),
98+
{name, Name} = erlang:fun_info(Tree, name),
99+
{arity, Arity} = erlang:fun_info(Tree, arity),
100+
{'fun', Ann, {function, {atom, Ann, Module}, {atom, Ann, Name}, {integer, Ann, Arity}}};
101+
false ->
102+
error(badarg, [Tree, Ann])
103+
end;
104+
elixir_to_erl(Tree, Ann) when is_pid(Tree); is_port(Tree); is_reference(Tree) ->
105+
?remote(Ann, erlang, binary_to_term, [elixir_to_erl(term_to_binary(Tree), Ann)]);
106+
elixir_to_erl(Tree, Ann) ->
107+
error(badarg, [Tree, Ann]).
97108

98109
elixir_to_erl_cons([H | T], Ann) -> {cons, Ann, elixir_to_erl(H, Ann), elixir_to_erl_cons(T, Ann)};
99110
elixir_to_erl_cons(T, Ann) -> elixir_to_erl(T, Ann).
@@ -296,8 +307,7 @@ macros_info(Defmacro) ->
296307
struct_info(nil) ->
297308
{clause, 0, [{atom, 0, struct}], [], [{atom, 0, nil}]};
298309
struct_info(Fields) ->
299-
FieldsWithoutDefault = [maps:remove(default, FieldInfo) || FieldInfo <- Fields],
300-
{clause, 0, [{atom, 0, struct}], [], [elixir_to_erl(FieldsWithoutDefault)]}.
310+
{clause, 0, [{atom, 0, struct}], [], [elixir_to_erl(Fields)]}.
301311

302312
get_module_info(Module, Key) ->
303313
Call = ?remote(0, erlang, get_module_info, [{atom, 0, Module}, {var, 0, 'Key'}]),

lib/elixir/src/elixir_expand.erl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,16 @@ expand(List, S, E) when is_list(List) ->
485485

486486
{EArgs, elixir_env:close_write(SE, S), EE};
487487

488+
expand(Zero, S, #{context := match} = E) when is_float(Zero), Zero == 0.0 ->
489+
elixir_errors:file_warn([], E, ?MODULE, invalid_match_on_zero_float),
490+
{Zero, S, E};
491+
492+
expand(Other, S, E) when is_number(Other); is_atom(Other); is_binary(Other) ->
493+
{Other, S, E};
494+
488495
expand(Function, S, E) when is_function(Function) ->
489496
case (erlang:fun_info(Function, type) == {type, external}) andalso
490-
(erlang:fun_info(Function, env) == {env, []}) of
497+
(erlang:fun_info(Function, env) == {env, []}) of
491498
true ->
492499
{elixir_quote:fun_to_quoted(Function), S, E};
493500
false ->
@@ -504,13 +511,6 @@ expand(Pid, S, E) when is_pid(Pid) ->
504511
{Pid, S, E}
505512
end;
506513

507-
expand(Zero, S, #{context := match} = E) when is_float(Zero), Zero == 0.0 ->
508-
elixir_errors:file_warn([], E, ?MODULE, invalid_match_on_zero_float),
509-
{Zero, S, E};
510-
511-
expand(Other, S, E) when is_number(Other); is_atom(Other); is_binary(Other) ->
512-
{Other, S, E};
513-
514514
expand(Other, _S, E) ->
515515
file_error([{line, 0}], ?key(E, file), ?MODULE, {invalid_quoted_expr, Other}).
516516

0 commit comments

Comments
 (0)