From 1738ed453fba49d811701e02c19eeacc4e2e5f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 22 Oct 2025 23:58:33 +0200 Subject: [PATCH] Do not include signatures twice in beam files --- lib/elixir/lib/module/parallel_checker.ex | 19 +++-- lib/elixir/lib/protocol.ex | 74 ++++++++++++------- lib/elixir/src/elixir_erl.erl | 24 ++++-- .../test/elixir/module/types/infer_test.exs | 8 +- 4 files changed, 78 insertions(+), 47 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 178f04e284..01d01f4024 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -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 diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index cf24cab42c..5ed6d58a3e 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -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 diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 9630c3db11..680a1852fe 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -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. diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs index 94c3b785c8..2394eb2501 100644 --- a/lib/elixir/test/elixir/module/types/infer_test.exs +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -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