Skip to content

Commit 485ca72

Browse files
authored
Infer types and use them across remote calls (#13981)
This also unifies handling of deterministic builds with Erlang/OTP as well as warnings_as_errors.
1 parent b35b0ef commit 485ca72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+833
-474
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ permissions:
1818

1919
jobs:
2020
test_linux:
21-
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}
21+
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}
2222
strategy:
2323
fail-fast: false
2424
matrix:
2525
include:
26+
- otp_version: "27.1"
27+
deterministic: true
2628
- otp_version: "27.1"
2729
otp_latest: true
28-
erlc_opts: "warnings_as_errors"
2930
- otp_version: "27.0"
30-
erlc_opts: "warnings_as_errors"
3131
- otp_version: "26.0"
3232
- otp_version: "25.3"
3333
- otp_version: "25.0"
@@ -36,18 +36,16 @@ jobs:
3636
- otp_version: maint
3737
development: true
3838
runs-on: ubuntu-24.04
39-
# Earlier Erlang/OTP versions ignored compiler directives
40-
# when using warnings_as_errors. So we only set ERLC_OPTS
41-
# from Erlang/OTP 27+.
42-
env:
43-
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
4439
steps:
4540
- uses: actions/checkout@v4
4641
with:
4742
fetch-depth: 50
4843
- uses: erlef/setup-beam@v1
4944
with:
5045
otp-version: ${{ matrix.otp_version }}
46+
- name: Set ERL_COMPILER_OPTIONS
47+
if: ${{ matrix.deterministic }}
48+
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
5149
- name: Compile Elixir
5250
run: |
5351
make compile
@@ -72,12 +70,12 @@ jobs:
7270
cd ../elixir/
7371
make docs
7472
- name: Check reproducible builds
73+
if: ${{ matrix.deterministic }}
7574
run: |
7675
rm -rf .git
7776
# Recompile System without .git
7877
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
7978
taskset 1 make check_reproducible
80-
if: ${{ matrix.otp_latest }}
8179
8280
test_windows:
8381
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ You may also prefer to write using guards:
8383
* [Kernel] Track the type of tuples in patterns and inside `elem/2`
8484
* [Kernel] Perform validation of root AST nodes in `unquote` and `unquote_splicing` to catch bugs earlier
8585
* [Kernel] Add source, behaviour, and record information to Docs chunk metadata
86+
* [Kernel] Support deterministic builds in tandem with Erlang by setting `ERL_COMPILER_OPTIONS=deterministic`. Keep in mind deterministic builds strip source and other compile time information, which may be relevant for programs
8687
* [List] Add `List.ends_with?/2`
8788
* [Macro] Improve `dbg` handling of `if/2`, `with/1` and of code blocks
8889
* [Macro] Add `Macro.struct_info!/2` to return struct information mirroring `mod.__info__(:struct)`
@@ -156,6 +157,7 @@ You may also prefer to write using guards:
156157

157158
#### Elixir
158159

160+
* [Code] Setting `:warnings_as_errors` is deprecated via `Code.put_compiler_option/2`. This must not affect developers, as the `:warnings_as_errors` option is managed by Mix tasks, and not directly used via the `Code` module
159161
* [Enumerable] Deprecate returning a two-arity function in `Enumerable.slice/1`
160162
* [List] `List.zip/1` is deprecated in favor of `Enum.zip/1`
161163
* [Module] Deprecate `Module.eval_quoted/3` in favor of `Code.eval_quoted/3`

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ MAN_PREFIX ?= $(SHARE_PREFIX)/man
55
CANONICAL := main/
66
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
77
ERLC := erlc -I lib/elixir/include
8-
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
8+
ERL_MAKE := erl -make
99
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
1010
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
1111
VERSION := $(strip $(shell cat VERSION))

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
103103
In case you want to use this Elixir version as your system version,
104104
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
105105

106-
Additionally, you may choose to run the test suite with `make clean test`.
106+
When updating the repository, you may want to run `make clean` before
107+
recompiling. For deterministic builds, you should set the environment
108+
variable `ERL_COMPILER_OPTIONS=deterministic`.
107109

108110
## Contributing
109111

lib/elixir/Emakefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
warn_deprecated_function,
1010
warn_obsolete_guard,
1111
warn_exported_vars,
12-
%% warn_missing_spec,
13-
%% warn_untyped_record,
12+
%% Enable this when we require Erlang/OTP 27+
13+
%% warnings_as_errors,
1414
debug_info,
1515
{outdir, "ebin/"}
1616
]}.

lib/elixir/lib/code.ex

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ defmodule Code do
249249
:debug_info,
250250
:ignore_already_consolidated,
251251
:ignore_module_conflict,
252-
:relative_paths,
253-
:warnings_as_errors
252+
:infer_signatures,
253+
:relative_paths
254254
]
255255

256256
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@@ -1562,8 +1562,8 @@ defmodule Code do
15621562
15631563
## Examples
15641564
1565-
Code.compiler_options(warnings_as_errors: true)
1566-
#=> %{warnings_as_errors: false}
1565+
Code.compiler_options(infer_signatures: false)
1566+
#=> %{infer_signatures: true}
15671567
15681568
"""
15691569
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1592,6 +1592,12 @@ defmodule Code do
15921592
:elixir_config.get(key)
15931593
end
15941594

1595+
# TODO: Remove me in Elixir v2.0
1596+
def get_compiler_option(:warnings_as_errors) do
1597+
IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1")
1598+
:ok
1599+
end
1600+
15951601
@doc """
15961602
Returns a list with all available compiler options.
15971603
@@ -1620,16 +1626,19 @@ defmodule Code do
16201626
Defaults to `true`.
16211627
16221628
* `:debug_info` - when `true`, retains debug information in the compiled
1623-
module. Defaults to `true`.
1624-
This enables static analysis tools as it allows developers to
1625-
partially reconstruct the original source code. Therefore, disabling
1629+
module. This option can also be overridden per module using the `@compile`
1630+
directive. Defaults to `true`.
1631+
1632+
This enables tooling to partially reconstruct the original source code,
1633+
for instance, to perform static analysis of code. Therefore, disabling
16261634
`:debug_info` is not recommended as it removes the ability of the
16271635
Elixir compiler and other tools to provide feedback. If you want to
16281636
remove the `:debug_info` while deploying, tools like `mix release`
16291637
already do such by default.
1630-
Additionally, `mix test` disables it via the `:test_elixirc_options`
1631-
project configuration option.
1632-
This option can also be overridden per module using the `@compile` directive.
1638+
1639+
Other environments, such as `mix test`, automatically disables this
1640+
via the `:test_elixirc_options` project configuration, as there is
1641+
typically no need to store debug chunks for test files.
16331642
16341643
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
16351644
when a protocol has already been consolidated and a new implementation is added.
@@ -1638,13 +1647,19 @@ defmodule Code do
16381647
* `:ignore_module_conflict` - when `true`, does not warn when a module has
16391648
already been defined. Defaults to `false`.
16401649
1650+
* `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local
1651+
signature inference used when type checking remote calls to the compiled
1652+
module. Type checking will be executed regardless of this value of this option.
1653+
Defaults to `true`.
1654+
1655+
`mix test` automatically disables this option via the `:test_elixirc_options`
1656+
project configuration, as there is typically no need to store infer signatures
1657+
for test files.
1658+
16411659
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
16421660
warnings, and errors generated by the compiler. Note disabling this option
16431661
won't affect runtime warnings and errors. Defaults to `true`.
16441662
1645-
* `:warnings_as_errors` - causes compilation to fail when warnings are
1646-
generated. Defaults to `false`.
1647-
16481663
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
16491664
tuples that will not emit warnings that the module or function does not exist
16501665
at compilation time. Pass atom `:all` to skip warning for all undefined
@@ -1690,6 +1705,16 @@ defmodule Code do
16901705
:ok
16911706
end
16921707

1708+
# TODO: Remove me in Elixir v2.0
1709+
def put_compiler_option(:warnings_as_errors, _value) do
1710+
IO.warn(
1711+
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <>
1712+
"pass it as option to Kernel.ParallelCompiler instead"
1713+
)
1714+
1715+
:ok
1716+
end
1717+
16931718
def put_compiler_option(:no_warn_undefined, value) do
16941719
if value != :all and not is_list(value) do
16951720
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
@@ -1719,7 +1744,7 @@ defmodule Code do
17191744
:ok
17201745
end
17211746

1722-
# TODO: Make this option have no effect on Elixir v2.0
1747+
# TODO: Remove this option on Elixir v2.0
17231748
# TODO: Warn if mode is :warn on Elixir v1.19
17241749
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
17251750
:elixir_config.put(:on_undefined_variable, value)

lib/elixir/lib/kernel/cli.ex

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Kernel.CLI do
99
compile: [],
1010
no_halt: false,
1111
compiler_options: [],
12+
warnings_as_errors: false,
1213
errors: [],
1314
verbose_compile: false,
1415
profile: nil,
@@ -315,8 +316,7 @@ defmodule Kernel.CLI do
315316
end
316317

317318
defp parse_argv([~c"--warnings-as-errors" | t], %{mode: :elixirc} = config) do
318-
compiler_options = [{:warnings_as_errors, true} | config.compiler_options]
319-
parse_argv(t, %{config | compiler_options: compiler_options})
319+
parse_argv(t, %{config | warnings_as_errors: true})
320320
end
321321

322322
defp parse_argv([~c"--verbose" | t], %{mode: :elixirc} = config) do
@@ -499,15 +499,11 @@ defmodule Kernel.CLI do
499499
]
500500
end
501501

502-
profile_opts =
503-
if config.profile do
504-
[profile: config.profile]
505-
else
506-
[]
507-
end
508-
509502
output = IO.chardata_to_string(config.output)
510-
opts = verbose_opts ++ profile_opts
503+
504+
opts =
505+
verbose_opts ++
506+
[profile: config.profile, warnings_as_errors: config.warnings_as_errors]
511507

512508
case Kernel.ParallelCompiler.compile_to_path(files, output, opts) do
513509
{:ok, _, _} -> :ok

lib/elixir/lib/kernel/parallel_compiler.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ defmodule Kernel.ParallelCompiler do
258258
{status, modules_or_errors, info} =
259259
try do
260260
outcome = spawn_workers(schedulers, checker, files, output, options)
261-
{outcome, Code.get_compiler_option(:warnings_as_errors)}
261+
{outcome, Keyword.get(options, :warnings_as_errors, false)}
262262
else
263263
{{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true}
264264
when r_warnings != [] or c_warnings != [] ->

lib/elixir/lib/macro.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ defmodule Macro do
854854
@spec struct_info!(module(), Macro.Env.t()) ::
855855
[%{field: atom(), required: boolean(), default: term()}]
856856
def struct_info!(module, env) when is_atom(module) do
857-
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], env) do
857+
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
858858
{:ok, info} -> info
859859
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
860860
end

lib/elixir/lib/module/parallel_checker.ex

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ defmodule Module.ParallelChecker do
191191
or if the function does not exist return `{:error, :function}`.
192192
"""
193193
@spec fetch_export(cache(), module(), atom(), arity()) ::
194-
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
194+
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
195+
| {:error, :function | :module}
195196
def fetch_export({server, ets}, module, fun, arity) do
196197
case :ets.lookup(ets, module) do
197198
[] ->
@@ -203,7 +204,7 @@ defmodule Module.ParallelChecker do
203204

204205
[{_key, mode}] ->
205206
case :ets.lookup(ets, {module, {fun, arity}}) do
206-
[{_key, reason}] -> {:ok, mode, reason}
207+
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
207208
[] -> {:error, :function}
208209
end
209210
end
@@ -369,13 +370,13 @@ defmodule Module.ParallelChecker do
369370
true ->
370371
{mode, exports} = info_exports(module)
371372
deprecated = info_deprecated(module)
372-
cache_info(ets, module, exports, deprecated, mode)
373+
cache_info(ets, module, exports, deprecated, %{}, mode)
373374

374375
false ->
375376
# Or load exports from chunk
376377
with {^module, binary, _filename} <- object_code,
377378
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
378-
cache_info(ets, module, exports, %{}, :erlang)
379+
cache_info(ets, module, exports, %{}, %{}, :erlang)
379380
else
380381
_ ->
381382
:ets.insert(ets, {module, false})
@@ -417,25 +418,28 @@ defmodule Module.ParallelChecker do
417418
behaviour_exports(map) ++
418419
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
419420

420-
deprecated = Map.new(map.deprecated)
421-
cache_info(ets, map.module, exports, deprecated, :elixir)
421+
cache_info(ets, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
422422
end
423423

424-
defp cache_info(ets, module, exports, deprecated, mode) do
425-
Enum.each(exports, fn {fun, arity} ->
426-
reason = Map.get(deprecated, {fun, arity})
427-
:ets.insert(ets, {{module, {fun, arity}}, reason})
424+
defp cache_info(ets, module, exports, deprecated, sigs, mode) do
425+
Enum.each(exports, fn fa ->
426+
reason = Map.get(deprecated, fa)
427+
:ets.insert(ets, {{module, fa}, reason, Map.get(sigs, fa, :none)})
428428
end)
429429

430430
:ets.insert(ets, {module, mode})
431431
end
432432

433433
defp cache_chunk(ets, module, exports) do
434434
Enum.each(exports, fn {{fun, arity}, info} ->
435-
:ets.insert(ets, {{module, {fun, arity}}, Map.get(info, :deprecated)})
435+
# TODO: Match on signature directly in Elixir v1.22+
436+
:ets.insert(
437+
ets,
438+
{{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)}
439+
)
436440
end)
437441

438-
:ets.insert(ets, {{module, {:__info__, 1}}, nil})
442+
:ets.insert(ets, {{module, {:__info__, 1}}, nil, :none})
439443
:ets.insert(ets, {module, :elixir})
440444
end
441445

0 commit comments

Comments
 (0)