Do not include signatures twice in beam files

This commit is contained in:
José Valim
2025-10-22 23:58:33 +02:00
parent 22b47d7f48
commit d18088a3b4
4 changed files with 78 additions and 47 deletions
+11 -8
View File
@@ -4,6 +4,7 @@
defmodule Module.ParallelChecker do
@moduledoc false
@elixir_checker_version :elixir_erl.checker_version()
import Kernel, except: [spawn: 3]
@@ -96,10 +97,12 @@ defmodule Module.ParallelChecker do
end
with {:ok, binary} <- File.read(location),
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
cache_from_module_map(table, module_map)
{:ok,
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
else
_ -> {:not_found, nil}
end
@@ -177,9 +180,9 @@ defmodule Module.ParallelChecker do
end
@doc """
Receives pairs of module maps and BEAM binaries. In parallel it verifies
the modules and adds the ExCk chunk to the binaries. Returns the updated
list of warnings from the verification.
Receives pairs of module maps and BEAM binaries.
Returns the updated list of warnings from the verification.
"""
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
def verify({checker, table}, runtime_files) do
@@ -423,7 +426,7 @@ defmodule Module.ParallelChecker do
mode =
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v3, contents} <- :erlang.binary_to_term(chunk) do
{@elixir_checker_version, contents} <- :erlang.binary_to_term(chunk) do
# The chunk has more information, so that's our preference
cache_chunk(table, module, contents)
else
+46 -28
View File
@@ -267,6 +267,8 @@ defmodule Protocol do
@optional_callbacks __deriving__: 2
@elixir_checker_version :elixir_erl.checker_version()
@doc false
defmacro def(signature)
@@ -564,31 +566,34 @@ defmodule Protocol do
# Ensure the types are sorted so the compiled beam is deterministic
types = Enum.sort(types)
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
{:ok, definitions, signatures} <-
consolidate(protocol, any, definitions, signatures, types),
do: compile(definitions, signatures, compile_info)
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
{:ok, definitions, checker} <-
consolidate(protocol, any, definitions, checker, types),
do: compile(definitions, checker, compile_info)
end
defp beam_protocol(protocol) do
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
opts = [:allow_missing_chunks]
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
%{attributes: attributes, definitions: definitions} = module_map
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
# TODO: Remove this on Elixir v1.23.
signatures = Map.get(module_map, :signatures, %{})
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
case attributes[:__protocol__] do
[fallback_to_any: any] ->
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
checker =
with true <- is_binary(checker),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
contents
else
_ -> nil
end
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
{:ok, any, definitions, checker, {module_map, specs, chunks}}
_ ->
{:error, :not_a_protocol}
@@ -607,7 +612,7 @@ defmodule Protocol do
end
# Consolidate the protocol for faster implementations and fine-grained type information.
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
case List.keytake(definitions, {:__protocol__, 1}, 0) do
{protocol_def, definitions} ->
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
@@ -623,11 +628,24 @@ defmodule Protocol do
protocol_def = change_protocol(protocol_def, types)
impl_for = change_impl_for(impl_for, protocol, types)
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
new_signatures = new_signatures(definitions, protocol_funs, protocol, types)
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
signatures = Enum.into(new_signatures, signatures)
{:ok, definitions, signatures}
checker =
if checker do
update_in(checker.exports, fn exports ->
signatures = new_signatures(definitions, protocol_funs, protocol, types)
for {fun, info} <- exports do
if sig = Map.get(signatures, fun) do
{fun, %{info | sig: sig}}
else
{fun, info}
end
end
end)
end
{:ok, definitions, checker}
nil ->
{:error, :not_a_protocol}
@@ -680,10 +698,12 @@ defmodule Protocol do
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
end
[
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
] ++ new_signatures
Map.new(
[
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
] ++ new_signatures
)
end
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
@@ -752,11 +772,9 @@ defmodule Protocol do
end
# Finally compile the module and emit its bytecode.
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
# TODO: Remove this on Elixir v1.23.
module_map = %{module_map | definitions: definitions} |> Map.put(:signatures, signatures)
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
module_map = %{module_map | definitions: definitions}
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
end
## Definition callbacks
+17 -7
View File
@@ -5,11 +5,14 @@
%% Compiler backend to Erlang.
-module(elixir_erl).
-export([elixir_to_erl/1, elixir_to_erl/2, definition_to_anonymous/5, compile/1, consolidate/3,
get_ann/1, debug_info/4, scope/2, format_error/1]).
-export([elixir_to_erl/1, elixir_to_erl/2, definition_to_anonymous/5, compile/1, consolidate/4,
get_ann/1, debug_info/4, scope/2, checker_version/0, format_error/1]).
-include("elixir.hrl").
-define(typespecs, 'Elixir.Kernel.Typespec').
checker_version() ->
elixir_checker_v3.
%% debug_info callback
debug_info(elixir_v1, _Module, none, _Opts) ->
@@ -132,9 +135,9 @@ scope(_Meta, ExpandCaptures) ->
%% Static compilation hook, used in protocol consolidation
consolidate(Map, TypeSpecs, DocsChunk) ->
{Prefix, Forms, Def, _Defmacro, _Macros} = dynamic_form(Map, nil),
CheckerChunk = checker_chunk(Map, Def, chunk_opts(Map)),
consolidate(Map, Checker, TypeSpecs, DocsChunk) ->
{Prefix, Forms, _Def, _Defmacro, _Macros} = dynamic_form(Map, nil),
CheckerChunk = checker_chunk(Checker, chunk_opts(Map)),
load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk).
%% Dynamic compilation hook, used in regular compiler
@@ -516,8 +519,10 @@ load_form(#{file := File, compile_opts := Opts} = Map, Prefix, Forms, Specs, Chu
Binary.
debug_opts(Map, Specs, Opts) ->
%% Signatures are moved to ExCk, no need to duplicate in chunks
Keys = [signatures],
case take_debug_opts(Opts) of
{true, Rest} -> [{debug_info, {?MODULE, {elixir_v1, Map, Specs}}} | Rest];
{true, Rest} -> [{debug_info, {?MODULE, {elixir_v1, maps:without(Keys, Map), Specs}}} | Rest];
{false, Rest} -> [{debug_info, {?MODULE, none}} | Rest]
end.
@@ -639,6 +644,11 @@ signature_to_binary(_, Name, Signature) ->
Doc = 'Elixir.Inspect.Algebra':format('Elixir.Code':quoted_to_algebra(Quoted), infinity),
'Elixir.IO':iodata_to_binary(Doc).
checker_chunk(nil, _ChunkOpts) ->
[];
checker_chunk(Contents, ChunkOpts) ->
[{<<"ExCk">>, term_to_binary({checker_version(), Contents}, ChunkOpts)}].
checker_chunk(Map, Def, ChunkOpts) ->
#{deprecated := Deprecated, defines_behaviour := DefinesBehaviour,
signatures := Signatures, attributes := Attributes} = Map,
@@ -662,7 +672,7 @@ checker_chunk(Map, Def, ChunkOpts) ->
end
},
[{<<"ExCk">>, term_to_binary({elixir_checker_v3, Contents}, ChunkOpts)}].
checker_chunk(Contents, ChunkOpts).
prepend_behaviour_info(true, Def) -> [{{behaviour_info, 1}, []} | Def];
prepend_behaviour_info(false, Def) -> Def.
@@ -23,10 +23,10 @@ defmodule Module.Types.InferTest do
[]
)
{:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
{:debug_info_v1, backend, data} = chunk
{:ok, %{signatures: signatures}} = backend.debug_info(:elixir_v1, module, data, [])
signatures
version = :elixir_erl.checker_version()
{:ok, {_, [{~c"ExCk", chunk}]}} = :beam_lib.chunks(binary, [~c"ExCk"])
{^version, data} = :erlang.binary_to_term(chunk)
for {fun, %{sig: sig}} <- data.exports, into: %{}, do: {fun, sig}
end
test "infer types from patterns", config do