From 0e9b5ec0704c21ba63b205c149bbb2373dd0c1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 10 Dec 2024 15:01:39 +0100 Subject: [PATCH 001/128] Branch out v1.18 --- Makefile | 2 +- SECURITY.md | 5 ++--- .../pages/references/compatibility-and-deprecations.md | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index b6f51b1ffbc..0a4620bc733 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PREFIX ?= /usr/local TEST_FILES ?= "*_test.exs" SHARE_PREFIX ?= $(PREFIX)/share MAN_PREFIX ?= $(SHARE_PREFIX)/man -CANONICAL := main/ +CANONICAL := v1.18/ ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ERLC := erlc -I lib/elixir/include ERL_MAKE := erl -make diff --git a/SECURITY.md b/SECURITY.md index d035b75db8f..f436cf62934 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.18 | Development -1.17 | Bug fixes and security patches +1.18 | Bug fixes and security patches +1.17 | Security patches only 1.16 | Security patches only 1.15 | Security patches only 1.14 | Security patches only -1.13 | Security patches only ## Announcements diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 4b53ae59c01..1519ebe6b96 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -8,12 +8,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.18 | Development -1.17 | Bug fixes and security patches +1.18 | Bug fixes and security patches +1.17 | Security patches only 1.16 | Security patches only 1.15 | Security patches only 1.14 | Security patches only -1.13 | Security patches only New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date). From 3bdf5a912c3dbbe8ac382172a03b1e9075b0354d Mon Sep 17 00:00:00 2001 From: Daniel Gomez de Souza Date: Tue, 10 Dec 2024 11:34:32 -0300 Subject: [PATCH 002/128] Fix typo (#14046) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06439826071..2835253afa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The most exciting change in Elixir v1.18 is type checking of function calls, alo defmodule User do defstruct [:age, :car_choice] - def drive(%User{age: age, car_choice: car}, cars_choices) when age >= 18 do + def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do if car in car_choices do {:ok, car} else From 31d8da6c770d09f2bf070f1436cb2b0a7f7c3653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 10 Dec 2024 15:41:05 +0100 Subject: [PATCH 003/128] Avoid mtime races on compile.app_test.exs --- lib/mix/test/mix/tasks/compile.app_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/mix/test/mix/tasks/compile.app_test.exs b/lib/mix/test/mix/tasks/compile.app_test.exs index 53909254607..943a1876dd6 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -256,6 +256,8 @@ defmodule Mix.Tasks.Compile.AppTest do test ".app contains description and registered (as required by systools)" do in_fixture("no_mixfile", fn -> Mix.Project.push(MixTest.Case.Sample) + # Pre-create the compilation path to avoid mtime races + File.mkdir_p(Mix.Project.compile_path()) Mix.Tasks.Compile.Elixir.run([]) assert Mix.Tasks.Compile.App.run([]) == {:ok, []} From 66c5908619f2ee9b4b1113e0302b00b5a59a5abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 10 Dec 2024 18:32:26 +0100 Subject: [PATCH 004/128] Release v1.18.0-rc.0 --- CHANGELOG.md | 4 ++-- VERSION | 2 +- bin/elixir | 2 +- bin/elixir.bat | 2 +- bin/elixir.ps1 | 2 +- lib/elixir/pages/references/compatibility-and-deprecations.md | 1 + 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2835253afa9..f5cdd890899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -182,7 +182,7 @@ If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON: ```elixir - @derive {JSON.Encoder, only: [....]} + @derive {JSON.Encoder, only: [...]} defstruct ... ``` @@ -219,7 +219,7 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z -## v1.18.0-dev +## v1.18.0-rc.0 (2024-12-10) ### 1. Enhancements diff --git a/VERSION b/VERSION index ee017091ff3..ca117a054fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.0-dev +1.18.0-rc.0 diff --git a/bin/elixir b/bin/elixir index 79b74deb9e2..23c07afe136 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.18.0-dev +ELIXIR_VERSION=1.18.0-rc.0 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index 448e22f4fd3..d4c66c8ab6e 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @echo off -set ELIXIR_VERSION=1.18.0-dev +set ELIXIR_VERSION=1.18.0-rc.0 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 index 90048705549..b9b7e7f5a0d 100755 --- a/bin/elixir.ps1 +++ b/bin/elixir.ps1 @@ -1,6 +1,6 @@ #!/usr/bin/env pwsh -$ELIXIR_VERSION = "1.18.0-dev" +$ELIXIR_VERSION = "1.18.0-rc.0" $scriptPath = Split-Path -Parent $PSCommandPath $erlExec = "erl" diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 1519ebe6b96..11a37689306 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -42,6 +42,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea Elixir version | Supported Erlang/OTP versions :------------- | :------------------------------- +1.18 | 25 - 27 1.17 | 25 - 27 1.16 | 24 - 26 1.15 | 24 - 26 From da7e04a540d4a05616429af4fcb36287fb88fa1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 10 Dec 2024 19:16:09 +0100 Subject: [PATCH 005/128] Improve docs around auto_reloading --- CHANGELOG.md | 10 ++++++++++ lib/iex/lib/iex.ex | 26 +++++++++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5cdd890899..1ed9f9ae596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,6 +199,16 @@ The default decoding rules are applied as follows: | String | `binary()` | | Object | `%{binary() => _}` | +## Language server listeners + +4 months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means. + +At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project. + +This release address by introducing a compiler lock, ensuring that only a single operating system running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other. + +These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE. + ## Potential incompatibilities This release no longer supports WERL (a graphical user interface on Windows used by Erlang 25 and earlier). For a better user experience on Windows terminals, use Erlang/OTP 26+ (this is also the last Elixir release to support Erlang/OTP 25). diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index 68a3dd31e72..222679237b1 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -391,16 +391,17 @@ defmodule IEx do The supported options are: + * `:auto_reload` + * `:alive_continuation_prompt` + * `:alive_prompt` * `:colors` - * `:inspect` - * `:width` - * `:history_size` - * `:default_prompt` * `:continuation_prompt` - * `:alive_prompt` - * `:alive_continuation_prompt` - * `:parser` + * `:default_prompt` * `:dot_iex` + * `:history_size` + * `:inspect` + * `:parser` + * `:width` They are discussed individually in the sections below. @@ -488,9 +489,6 @@ defmodule IEx do * `:alive_continuation_prompt` - used when `Node.alive?/0` returns `true` and more input is expected - * `:auto_reload` - when set to `true`, automatically purges in-memory - modules when they get invalidated by a concurrent compilation - The following values in the prompt string will be replaced appropriately: * `%counter` - the index of the history @@ -510,10 +508,16 @@ defmodule IEx do If the parser raises, the buffer is reset to an empty string. - ## dot_iex + ## `.iex` Configure the file loaded into your IEx session when it starts. See more information [in the `.iex.exs` documentation](`m:IEx#module-the-iex-exs-file`). + + ## Auto reloading + + When set to `true`, the `:auto_reload` option automatically purges + in-memory modules when they get invalidated by a concurrent compilation + happening in the Operating System. """ @spec configure(keyword()) :: :ok def configure(options) do From b70986597639915a73332ab17fa5bdc3c0f7cc50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 10 Dec 2024 19:18:32 +0100 Subject: [PATCH 006/128] Improvements to markdown rendering in CHANGELOG --- CHANGELOG.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed9f9ae596..70754374704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -156,14 +156,11 @@ More migrations may be added in future releases. This release includes official support for JSON encoding and decoding. -Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and -[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) -standards. +Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards. ### Encoding -Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. -The default encoding rules are applied as follows: +Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows: | **Elixir** | **JSON** | |------------------------|----------| @@ -177,9 +174,7 @@ The default encoding rules are applied as follows: | `%{atom() => _}` | Object | | `%{integer() => _}` | Object | -You may also implement the `JSON.Encoder` protocol for custom data structures. -If you have a struct, you can derive the implementation of the `JSON.Encoder` -by specifying which fields should be encoded to JSON: +You may also implement the `JSON.Encoder` protocol for custom data structures. If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON: ```elixir @derive {JSON.Encoder, only: [...]} @@ -188,8 +183,7 @@ by specifying which fields should be encoded to JSON: ### Decoding -Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. -The default decoding rules are applied as follows: +Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows: | **JSON** | **Elixir** | |----------|------------------------| From a78f1715dcae2420a6b377cf17fedf5d66580862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 10 Dec 2024 19:29:32 +0100 Subject: [PATCH 007/128] Fix error message around single quote keywords --- lib/elixir/src/elixir_tokenizer.erl | 26 ++++++++++++++----- .../test/elixir/kernel/warning_test.exs | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 0290803970f..cfb6e5e14be 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -266,11 +266,7 @@ tokenize([$" | T], Line, Column, Scope, Tokens) -> %% TODO: Remove me in Elixir v2.0 tokenize([$' | T], Line, Column, Scope, Tokens) -> - Message = "single-quoted strings represent charlists. " - "Use ~c\"\" if you indeed want a charlist or use \"\" instead.\n" - "You may run \"mix format --migrate\" to fix this warning automatically.", - NewScope = prepend_warning(Line, Column, Message, Scope), - handle_strings(T, Line, Column + 1, $', NewScope, Tokens); + handle_strings(T, Line, Column + 1, $', Scope, Tokens); % Operator atoms @@ -786,7 +782,11 @@ handle_strings(T, Line, Column, H, Scope, Tokens) -> "number do not require quotes", [hd(Parts)] ), - prepend_warning(Line, Column, WarnMsg, InterScope); + prepend_warning(Line, Column-1, WarnMsg, InterScope); + + false when H =:= $' -> + WarnMsg = "single quotes around keywords are deprecated. Use double quotes instead", + prepend_warning(Line, Column-1, WarnMsg, InterScope); false -> InterScope @@ -814,7 +814,19 @@ handle_strings(T, Line, Column, H, Scope, Tokens) -> error(Reason, Rest, NewScope, Tokens) end; - {NewLine, NewColumn, Parts, Rest, NewScope} -> + {NewLine, NewColumn, Parts, Rest, InterScope} -> + NewScope = + case H of + $' -> + Message = "single-quoted strings represent charlists. " + "Use ~c\"\" if you indeed want a charlist or use \"\" instead.\n" + "You may run \"mix format --migrate\" to fix this warning automatically.", + prepend_warning(Line, Column-1, Message, InterScope); + + _ -> + InterScope + end, + case unescape_tokens(Parts, Line, Column, NewScope) of {ok, Unescaped} -> Token = {string_type(H), {Line, Column - 1, nil}, Unescaped}, diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 1f0f975adbc..c318fe6526b 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -190,7 +190,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:1:3", "found quoted keyword \"foo\" but the quotes are not required"], + ["nofile:1:2", "found quoted keyword \"foo\" but the quotes are not required"], ~s/["foo": :bar]/ ) From 2273bafb4701cc94b10ba1de69a47dbda4679e56 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Wed, 11 Dec 2024 02:41:11 -0500 Subject: [PATCH 008/128] Improve grammar in CHANGELOG.md (#14050) Co-authored-by: Wojtek Mach --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70754374704..d6e079112ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ defmodule User do end ``` -Elixir's type system will infer the `drive` function expects a `%User{}` struct as input and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`. +Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`. Therefore, the following code should emit a violation, due to an invalid argument: From a4fa71ab23c309d7f471e2a2bb79ad2fbc2938f7 Mon Sep 17 00:00:00 2001 From: Shenghang Tsai Date: Wed, 11 Dec 2024 16:43:42 +0800 Subject: [PATCH 009/128] Update CHANGELOG.md (#14052) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6e079112ef..71446201494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,7 +199,7 @@ Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The def At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project. -This release address by introducing a compiler lock, ensuring that only a single operating system running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other. +This release address by introducing a compiler lock, ensuring that only a single operating system process running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other. These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE. From dfc659104c0d957e173e34d8c0ee73395b67132e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Dec 2024 10:00:31 +0100 Subject: [PATCH 010/128] Touch the compile path when writing .app file for the first time (#14053) Closes #14049. --- lib/mix/lib/mix/tasks/compile.app.ex | 7 +++++++ lib/mix/test/mix/tasks/compile.app_test.exs | 4 ---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 61fcb907783..78c51706fcf 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -189,6 +189,13 @@ defmodule Mix.Tasks.Compile.App do Mix.Project.ensure_structure() File.write!(target, IO.chardata_to_string(contents)) File.touch!(target, new_mtime) + + # If we just created the .app file, it will have touched + # the directory mtime, so we need to reset it. + if current_properties == [] do + File.touch!(compile_path, new_mtime) + end + Mix.shell().info("Generated #{app} app") {:ok, []} else diff --git a/lib/mix/test/mix/tasks/compile.app_test.exs b/lib/mix/test/mix/tasks/compile.app_test.exs index 943a1876dd6..3674bfcfb25 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -66,8 +66,6 @@ defmodule Mix.Tasks.Compile.AppTest do test "generates .app file when changes happen" do in_fixture("no_mixfile", fn -> Mix.Project.push(MixTest.Case.Sample) - # Pre-create the compilation path to avoid mtime races - File.mkdir_p(Mix.Project.compile_path()) Mix.Tasks.Compile.Elixir.run([]) assert Mix.Tasks.Compile.App.run([]) == {:ok, []} @@ -256,8 +254,6 @@ defmodule Mix.Tasks.Compile.AppTest do test ".app contains description and registered (as required by systools)" do in_fixture("no_mixfile", fn -> Mix.Project.push(MixTest.Case.Sample) - # Pre-create the compilation path to avoid mtime races - File.mkdir_p(Mix.Project.compile_path()) Mix.Tasks.Compile.Elixir.run([]) assert Mix.Tasks.Compile.App.run([]) == {:ok, []} From e07a91594b0f579ebbdf0194970a0adab0504145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Dec 2024 12:02:01 +0100 Subject: [PATCH 011/128] Update deprecations --- .../references/compatibility-and-deprecations.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 11a37689306..81a9a57b515 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -82,6 +82,14 @@ The first column is the version the feature was hard deprecated. The second colu Version | Deprecated feature | Replaced by (available since) :-------| :-------------------------------------------------- | :--------------------------------------------------------------- +[v1.18] | `<%#` in EEx | `<%!--` (v1.14) or `<% #` (v1.0) +[v1.18] | `handle_text/2` callback in EEx | `handle_text/3` (v1.14) +[v1.18] | Returning 2-arity fun from `Enumerable.slice/1` | Returning 3-arity (v1.14) +[v1.18] | Ranges with negative steps in `Range.new/2` | Explicit steps in ranges (v1.11) +[v1.18] | `Tuple.append/2` | `Tuple.insert_at/3` (v1.0) +[v1.18] | `mix cmd --app APP` | `mix do --app APP` (v1.14) +[v1.18] | `List.zip/1` | `Enum.zip/1` (v1.0) +[v1.18] | `Module.eval_quoted/3` | `Code.eval_quoted/3` (v1.0) [v1.17] | Single-quoted charlists (`'foo'`) | `~c"foo"` (v1.0) [v1.17] | `left..right` in patterns and guards | `left..right//step` (v1.11) [v1.17] | `ExUnit.Case.register_test/4` | `register_test/6` (v1.10) @@ -214,4 +222,5 @@ Version | Deprecated feature | Replaced by (ava [v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations [v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations [v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations -[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations +[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations +[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations From 3ca0bddd9dfcdef6a0c5d7f7641f6166841407a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Dec 2024 12:16:26 +0100 Subject: [PATCH 012/128] Fix encoding specs --- lib/elixir/lib/json.ex | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex index d81b4ddcc1c..36a2be3d8a8 100644 --- a/lib/elixir/lib/json.ex +++ b/lib/elixir/lib/json.ex @@ -199,6 +199,8 @@ defmodule JSON do @moduledoc since: "1.18.0" + @type encoder :: (term(), encoder() -> iodata()) + @type decode_error_reason :: {:unexpected_end, non_neg_integer()} | {:invalid_byte, non_neg_integer(), byte()} @@ -332,7 +334,7 @@ defmodule JSON do "[123,\"string\",{\"key\":\"value\"}]" """ - @spec encode!(a, (a -> iodata())) :: binary() when a: var + @spec encode!(term(), encoder()) :: binary() def encode!(term, encoder \\ &protocol_encode/2) do IO.iodata_to_binary(encoder.(term, encoder)) end @@ -353,7 +355,7 @@ defmodule JSON do "[123,\"string\",{\"key\":\"value\"}]" """ - @spec encode_to_iodata!(a, (a -> iodata())) :: iodata() when a: var + @spec encode_to_iodata!(term(), encoder()) :: iodata() def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do encoder.(term, encoder) end @@ -365,7 +367,7 @@ defmodule JSON do `encode!/2` and `encode_to_iodata!/2`. The default implementation is an optimized dispatch to the `JSON.Encoder` protocol. """ - @spec protocol_encode(a, (a -> iodata())) :: iodata() when a: var + @spec protocol_encode(term(), encoder()) :: iodata() def protocol_encode(value, encoder) when is_atom(value) do case value do nil -> "null" From cda1a09c58de7a8a6b205a8fa91ee4524443b20a Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Wed, 11 Dec 2024 12:42:04 +0100 Subject: [PATCH 013/128] Improve error message on empty "mix do" (#14055) --- lib/mix/lib/mix/tasks/do.ex | 25 +++++++++++++++++++------ lib/mix/test/mix/tasks/do_test.exs | 8 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/mix/lib/mix/tasks/do.ex b/lib/mix/lib/mix/tasks/do.ex index c68fcb1c236..c46656f4a75 100644 --- a/lib/mix/lib/mix/tasks/do.ex +++ b/lib/mix/lib/mix/tasks/do.ex @@ -58,12 +58,25 @@ defmodule Mix.Tasks.Do do {apps, args} = extract_apps_from_args(args) show_forgotten_apps_warning(apps) - Enum.each(gather_commands(args), fn [task | args] -> - if apps == [] do - Mix.Task.run(task, args) - else - Mix.Task.run_in_apps(task, apps, args) - end + Enum.each(gather_commands(args), fn + [task | args] -> + if apps == [] do + Mix.Task.run(task, args) + else + Mix.Task.run_in_apps(task, apps, args) + end + + [] -> + Mix.raise(""" + One of the commands passed to "mix do" is empty. Each command passed to "mix do" must \ + have at least the task name. These are all invalid: + + mix do + mix do my_task + + mix do + my_task + + Run "mix help do" for more information. + """) end) end diff --git a/lib/mix/test/mix/tasks/do_test.exs b/lib/mix/test/mix/tasks/do_test.exs index 23e540f1d15..dab90c4907c 100644 --- a/lib/mix/test/mix/tasks/do_test.exs +++ b/lib/mix/test/mix/tasks/do_test.exs @@ -13,6 +13,14 @@ defmodule Mix.Tasks.DoTest do end) end + test "raises if a task is empty" do + for args <- [~w(), ~w(+), ~w(help +), ~w(+ help)] do + assert_raise Mix.Error, ~r"^One of the commands passed to \"mix do\" is empty", fn -> + Mix.Tasks.Do.run(args) + end + end + end + test "gather_command returns a list of commands" do assert gather_commands(["help", "+", "compile"]) == [["help"], ["compile"]] From 9c28fb7a39360048106d894054e8dc98a3fb1645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Dec 2024 21:08:12 +0100 Subject: [PATCH 014/128] Improve JSON and Float docs --- lib/elixir/lib/float.ex | 12 +++--------- lib/elixir/lib/json.ex | 6 ++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 5855a975aff..07313e45d31 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -577,15 +577,9 @@ defmodule Float do Returns a charlist which corresponds to the shortest text representation of the given float. - The underlying algorithm changes depending on the Erlang/OTP version: - - * For OTP >= 24, it uses the algorithm presented in "Ryū: fast - float-to-string conversion" in Proceedings of the SIGPLAN '2018 - Conference on Programming Language Design and Implementation. - - * For OTP < 24, it uses the algorithm presented in "Printing Floating-Point - Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996 - Conference on Programming Language Design and Implementation. + It uses the algorithm presented in "Ryū: fast float-to-string conversion" + in Proceedings of the SIGPLAN '2018 Conference on Programming Language + Design and Implementation. For a configurable representation, use `:erlang.float_to_list/2`. diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex index 36a2be3d8a8..679f77a8589 100644 --- a/lib/elixir/lib/json.ex +++ b/lib/elixir/lib/json.ex @@ -328,6 +328,12 @@ defmodule JSON do The second argument is a function that is recursively invoked to encode a term. + > #### IO and performance {: .tip} + > + > If you need to encode data to be sent over the network + > or written to the filesystem, consider using the more + > efficient `encode_to_iodata!/2`. + ## Examples iex> JSON.encode!([123, "string", %{key: "value"}]) From 070a6d51bfdfeb9c833b3854e0e9b2b7cc215448 Mon Sep 17 00:00:00 2001 From: Panagiotis Nezis Date: Thu, 12 Dec 2024 01:36:49 +0200 Subject: [PATCH 015/128] Improve warning in Range.new/2 with negative step (#14062) --- lib/elixir/lib/range.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/range.ex b/lib/elixir/lib/range.ex index 7265b28966b..c1cdcb03f93 100644 --- a/lib/elixir/lib/range.ex +++ b/lib/elixir/lib/range.ex @@ -192,7 +192,7 @@ defmodule Range do IO.warn_once( {__MODULE__, :new}, fn -> - "Range.new/2 has a default step of -1, please call Range.new/3 explicitly passing the step of -1 instead" + "Range.new/2 has a default step of -1 when last < first, please call Range.new/3 explicitly passing the step of -1 instead" end, 3 ) From 55eca5dff5804390eb3f431494160f48afec38af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 12 Dec 2024 00:37:04 +0100 Subject: [PATCH 016/128] Implement JSON.Encoder for Calendar types (#14061) --- lib/elixir/lib/json.ex | 6 ++++++ lib/elixir/test/elixir/json_test.exs | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex index 679f77a8589..0f39243df2d 100644 --- a/lib/elixir/lib/json.ex +++ b/lib/elixir/lib/json.ex @@ -150,6 +150,12 @@ defimpl JSON.Encoder, for: Map do end end +defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do + def encode(value, _encoder) do + [?", @for.to_iso8601(value), ?"] + end +end + defmodule JSON.DecodeError do @moduledoc """ The exception raised by `JSON.decode!/1`. diff --git a/lib/elixir/test/elixir/json_test.exs b/lib/elixir/test/elixir/json_test.exs index 8e301944834..99b8d23cd6d 100644 --- a/lib/elixir/test/elixir/json_test.exs +++ b/lib/elixir/test/elixir/json_test.exs @@ -46,6 +46,13 @@ defmodule JSONTest do assert JSON.encode!(%Token{value: :example}) == "[\"example\"]" assert JSON.encode!(%Token{value: "hello\0world"}) == "[\"hello\\u0000world\"]" end + + test "calendar" do + assert JSON.encode!(~D[2010-04-17]) == "\"2010-04-17\"" + assert JSON.encode!(~T[14:00:00.123]) == "\"14:00:00.123\"" + assert JSON.encode!(~N[2010-04-17 14:00:00.123]) == "\"2010-04-17T14:00:00.123\"" + assert JSON.encode!(~U[2010-04-17 14:00:00.123Z]) == "\"2010-04-17T14:00:00.123Z\"" + end end describe "JSON.Encoder" do From 6c866e8ab23f4ccb51ee653b763cf7eefefb52d5 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Thu, 12 Dec 2024 17:46:24 +0900 Subject: [PATCH 017/128] Fix type warnings in pop_in/1 (#14064) --- lib/elixir/lib/kernel.ex | 4 ++-- lib/elixir/test/elixir/kernel_test.exs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 31a9a4c8c65..0a9d0137652 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -3306,7 +3306,7 @@ defmodule Kernel do end defp nest_pop_in(:map, h, [{:access, key}]) do - quote do + quote generated: true do case unquote(h) do nil -> {nil, nil} h -> Access.pop(h, unquote(key)) @@ -3327,7 +3327,7 @@ defmodule Kernel do end defp nest_pop_in(_, h, [{:access, key}]) do - quote do + quote generated: true do case unquote(h) do nil -> :pop h -> Access.pop(h, unquote(key)) diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index bae0550e3b9..fa4b7364a1a 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -1136,6 +1136,8 @@ defmodule KernelTest do test "pop_in/1" do users = %{"john" => %{age: 27}, "meg" => %{age: 23}} + assert pop_in(users["john"]) == {%{age: 27}, %{"meg" => %{age: 23}}} + assert pop_in(users["john"][:age]) == {27, %{"john" => %{}, "meg" => %{age: 23}}} assert pop_in(users["john"][:name]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}} assert pop_in(users["bob"][:age]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}} From 85e00cddd534528849ab1b89c46a3d592c695644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 12 Dec 2024 09:52:27 +0100 Subject: [PATCH 018/128] Clarify warnings happen on compiled code --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71446201494..07705488685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Elixir v1.18 is an impressive release with improvements across the two main effo ## Type system improvements -The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code: +The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex": ```elixir defmodule User do @@ -26,7 +26,7 @@ end Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`. -Therefore, the following code should emit a violation, due to an invalid argument: +Therefore, the following code in a separate module (either in a separate or the same file), should emit a violation, due to an invalid argument: ```elixir User.drive({:ok, %User{}}, car_choices) From 048ae58d6de303a2f957d1b74816b5b63d3b897c Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 12 Dec 2024 11:51:19 +0100 Subject: [PATCH 019/128] Do not set docs source_ref to -latest tags (#14065) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0a4620bc733..bfda4058745 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ INSTALL_DIR = $(INSTALL) -m755 -d INSTALL_DATA = $(INSTALL) -m644 INSTALL_PROGRAM = $(INSTALL) -m755 GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null )) -GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) ) +GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1)) SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH From 048cc2ddb178a7c0e6557c96a12316b2d99eceae Mon Sep 17 00:00:00 2001 From: Mathias Polligkeit <13847569+woylie@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:51:58 +0900 Subject: [PATCH 020/128] Further improve warning message for range with negative step (#14070) --- lib/elixir/lib/range.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/range.ex b/lib/elixir/lib/range.ex index c1cdcb03f93..b876ffb21d2 100644 --- a/lib/elixir/lib/range.ex +++ b/lib/elixir/lib/range.ex @@ -192,7 +192,7 @@ defmodule Range do IO.warn_once( {__MODULE__, :new}, fn -> - "Range.new/2 has a default step of -1 when last < first, please call Range.new/3 explicitly passing the step of -1 instead" + "Range.new/2 and first..last default to a step of -1 when last < first. Use Range.new(first, last, -1) or first..last//-1, or pass 1 if that was your intention" end, 3 ) From 36c06c3a56058369c3c49b83fe61a984a48c239f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Fri, 13 Dec 2024 09:52:20 +0100 Subject: [PATCH 021/128] Fix JSON.decode/3 spec (#14068) The error clause was missing the :error tuple wrapping --- lib/elixir/lib/json.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex index 0f39243df2d..fc7c6477841 100644 --- a/lib/elixir/lib/json.ex +++ b/lib/elixir/lib/json.ex @@ -265,7 +265,8 @@ defmodule JSON do For streaming decoding, see Erlang's `:json` module. """ - @spec decode(binary(), term(), keyword()) :: {term(), term(), binary()} | decode_error_reason() + @spec decode(binary(), term(), keyword()) :: + {term(), term(), binary()} | {:error, decode_error_reason()} def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do decoders = Keyword.put_new(decoders, :null, nil) From 8e8c9a733407f9691c4c442f035626d45190ecf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 13 Dec 2024 09:55:39 +0100 Subject: [PATCH 022/128] Wrap Range.new in Function.identity to avoid tail call optimization --- lib/elixir/lib/kernel.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 0a9d0137652..3bfc11f0e9d 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -4066,7 +4066,7 @@ defmodule Kernel do end defp stepless_range(nil, first, last, _caller) do - quote(do: Elixir.Range.new(unquote(first), unquote(last))) + quote(do: Function.identity(Elixir.Range.new(unquote(first), unquote(last)))) end defp stepless_range(:guard, first, last, caller) do From 4329a3c2f21b815e8e6ddd01b83ad18de05e17cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 13 Dec 2024 10:01:18 +0100 Subject: [PATCH 023/128] Address bootstrap --- lib/elixir/src/elixir_compiler.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 8ba322b88fd..d9164efcd59 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -203,6 +203,7 @@ bootstrap_files() -> <<"path.ex">>, <<"file.ex">>, <<"map.ex">>, + <<"function.ex">>, <<"range.ex">>, <<"access.ex">>, <<"io.ex">>, From 617dc727594c9ae5b911b9d92484e1ff545b99bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 13 Dec 2024 10:14:47 +0100 Subject: [PATCH 024/128] Fix Range.new deprecation assertion --- lib/elixir/test/elixir/range_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/range_test.exs b/lib/elixir/test/elixir/range_test.exs index 05666d34c4d..a974b3c008e 100644 --- a/lib/elixir/test/elixir/range_test.exs +++ b/lib/elixir/test/elixir/range_test.exs @@ -35,7 +35,7 @@ defmodule RangeTest do assert ExUnit.CaptureIO.capture_io(:stderr, fn -> assert Range.new(3, 1) == 3..1//-1 - end) =~ "has a default step of -1" + end) =~ "default to a step of -1" end test "fields" do From 1c7fc86e9c21ccb5bda6292108a55f4e9969acd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Fri, 13 Dec 2024 16:57:40 +0100 Subject: [PATCH 025/128] Make :source_path in docs chunk a charlist (#14071) --- lib/elixir/src/elixir_erl.erl | 2 +- lib/elixir/test/elixir/kernel/docs_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 58800a2a966..17ed4d9f0ef 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -526,7 +526,7 @@ docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts) - TypeDocs = get_type_docs(Set, Types), ModuleMeta = ModuleDocMeta#{ - source_path => File, + source_path => elixir_utils:characters_to_list(File), source_annos => [Anno], behaviours => [Mod || {behaviour, Mod} <- Attributes] }, diff --git a/lib/elixir/test/elixir/kernel/docs_test.exs b/lib/elixir/test/elixir/kernel/docs_test.exs index c87c9d045c1..ba1cea74f78 100644 --- a/lib/elixir/test/elixir/kernel/docs_test.exs +++ b/lib/elixir/test/elixir/kernel/docs_test.exs @@ -255,7 +255,7 @@ defmodule Kernel.DocsTest do assert module_doc == "Module doc" - file = __ENV__.file + file = String.to_charlist(__ENV__.file) source_annos = [:erl_anno.new({line + 3, 19})] From 305d8e603ca98a5bcd9ec59f69a68568a322bbe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 15 Dec 2024 15:17:57 +0100 Subject: [PATCH 026/128] Do not convert fun() into (... -> any()) and vice-versa in typespecs --- lib/elixir/lib/code/typespec.ex | 9 --------- lib/elixir/lib/kernel/typespec.ex | 9 +-------- lib/elixir/test/elixir/typespec_test.exs | 6 ++---- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/code/typespec.ex b/lib/elixir/lib/code/typespec.ex index 017b2f05dcc..30f0789a479 100644 --- a/lib/elixir/lib/code/typespec.ex +++ b/lib/elixir/lib/code/typespec.ex @@ -27,11 +27,6 @@ defmodule Code.Typespec do end end - def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do - meta = meta(anno) - {:"::", meta, [{name, meta, []}, quote(do: term)]} - end - def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do meta = meta(anno) {:type, _, :fun, [{:type, _, :product, args}, result]} = type @@ -317,10 +312,6 @@ defmodule Code.Typespec do [{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}] end - defp typespec_to_quoted({:type, anno, :fun, []}) do - typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]}) - end - defp typespec_to_quoted({:type, anno, :range, [left, right]}) do {:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]} end diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex index 942cd23455f..fbe1b86b315 100644 --- a/lib/elixir/lib/kernel/typespec.ex +++ b/lib/elixir/lib/kernel/typespec.ex @@ -668,14 +668,7 @@ defmodule Kernel.Typespec do when is_list(args) do {args, state} = fn_args(meta, args, vars, caller, state) {spec, state} = typespec(return, vars, caller, state) - - fun_args = - case [args, spec] do - [{:type, _, :any}, {:type, _, :any, []}] -> [] - pair -> pair - end - - {{:type, location(meta), :fun, fun_args}, state} + {{:type, location(meta), :fun, [args, spec]}, state} end # Handle type operator diff --git a/lib/elixir/test/elixir/typespec_test.exs b/lib/elixir/test/elixir/typespec_test.exs index 21b38f0d8aa..c81bb4682c7 100644 --- a/lib/elixir/test/elixir/typespec_test.exs +++ b/lib/elixir/test/elixir/typespec_test.exs @@ -710,7 +710,8 @@ defmodule TypespecTest do @type my_type :: (... -> any) end - assert [type: {:my_type, {:type, _, :fun, []}, []}] = types(bytecode) + assert [type: {:my_type, {:type, _, :fun, [{:type, _, :any}, {:type, _, :any, []}]}, []}] = + types(bytecode) end test "@type with a fun with multiple arguments and return type" do @@ -1520,9 +1521,6 @@ defmodule TypespecTest do assert ast_string == "@type literal_struct_all_fields_key_type() :: %TypespecTest.SomeStruct{key: integer()}" - {:built_in_fun, _, _} -> - assert ast_string == "@type built_in_fun() :: (... -> any())" - {:built_in_nonempty_list, _, _} -> assert ast_string == "@type built_in_nonempty_list() :: [...]" From 472c4e1a24af9efdbddf3dfd0c3af9b437dbbd37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 16 Dec 2024 12:09:02 +0100 Subject: [PATCH 027/128] Avoid crashes on diagnostics with tabs, closes #14073 --- lib/elixir/src/elixir_errors.erl | 8 ++--- .../test/elixir/kernel/diagnostics_test.exs | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 6dd3197fc63..0f47189050e 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -216,12 +216,12 @@ highlight_at_position(Column, Severity, Length) -> highlight_below_line(Line, Severity) -> % Don't highlight leading whitespaces in line - {_, SpacesMatched} = trim_line(Line, 0), + {Rest, SpacesMatched} = trim_line(Line, 0), - Length = string:length(Line), + Length = string:length(Rest), Highlight = case Severity of - warning -> highlight(lists:duplicate(Length - SpacesMatched, $~), warning); - error -> highlight(lists:duplicate(Length - SpacesMatched, $^), error) + warning -> highlight(lists:duplicate(Length, $~), warning); + error -> highlight(lists:duplicate(Length, $^), error) end, [n_spaces(SpacesMatched), Highlight]. diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index f016ac75cc7..0ce2ed7a88b 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -859,6 +859,35 @@ defmodule Kernel.DiagnosticsTest do purge(Sample) end + @tag :tmp_dir + test "simple warning with tabs (line + file)", %{tmp_dir: tmp_dir} do + path = make_relative_tmp(tmp_dir, "long-warning.ex") + + source = """ + defmodule Sample do + \t@file "#{path}" + \tdefp a do + \t\tUnknown.b() + \tend + end + """ + + File.write!(path, source) + + expected = """ + warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined) + │ + 4 │ \t\tUnknown.b() + │ ~~~~~~~~~~~ + │ + └─ #{path}:4: Sample.a/0 + """ + + assert capture_eval(source, columns: false) =~ expected + after + purge(Sample) + end + test "simple warning (no file)" do source = """ defmodule Sample do From 8ddef06e81d07a778685a799b1f5e4bfbf1bb749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 16 Dec 2024 12:23:40 +0100 Subject: [PATCH 028/128] Also encode Duration to JSON --- lib/elixir/lib/json.ex | 2 +- lib/elixir/test/elixir/json_test.exs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex index fc7c6477841..ec602bf1b7f 100644 --- a/lib/elixir/lib/json.ex +++ b/lib/elixir/lib/json.ex @@ -150,7 +150,7 @@ defimpl JSON.Encoder, for: Map do end end -defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do +defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do def encode(value, _encoder) do [?", @for.to_iso8601(value), ?"] end diff --git a/lib/elixir/test/elixir/json_test.exs b/lib/elixir/test/elixir/json_test.exs index 99b8d23cd6d..5bbc56243f1 100644 --- a/lib/elixir/test/elixir/json_test.exs +++ b/lib/elixir/test/elixir/json_test.exs @@ -52,6 +52,7 @@ defmodule JSONTest do assert JSON.encode!(~T[14:00:00.123]) == "\"14:00:00.123\"" assert JSON.encode!(~N[2010-04-17 14:00:00.123]) == "\"2010-04-17T14:00:00.123\"" assert JSON.encode!(~U[2010-04-17 14:00:00.123Z]) == "\"2010-04-17T14:00:00.123Z\"" + assert JSON.encode!(Duration.new!(month: 2, hour: 3)) == "\"P2MT3H\"" end end From 6a24bafbdaba3f0745338fdeaa26c34e481cf96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 16 Dec 2024 16:12:55 +0100 Subject: [PATCH 029/128] Tag exceptions as dynamic, closes #14074 --- lib/elixir/lib/module/types/expr.ex | 3 ++- .../test/elixir/module/types/expr_test.exs | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 7aab48d6fc3..27b925fe66d 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -444,7 +444,8 @@ defmodule Module.Types.Expr do # to avoid export dependencies. So we do it here. if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do {info, context} = Of.struct_info(exception, meta, stack, context) - {Of.struct_type(exception, info, args), context} + # TODO: For properly defined structs, this should not be dynamic + {dynamic(Of.struct_type(exception, info, args)), context} else # If the exception cannot be found or is invalid, fetch the signature to emit warnings. {_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 7e5499cde56..c5c5a6040e0 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1224,16 +1224,18 @@ defmodule Module.Types.ExprTest do e end ) == - union( - closed_map( - __struct__: atom([ArgumentError]), - __exception__: atom([true]), - message: term() - ), - closed_map( - __struct__: atom([RuntimeError]), - __exception__: atom([true]), - message: term() + dynamic( + union( + closed_map( + __struct__: atom([ArgumentError]), + __exception__: atom([true]), + message: term() + ), + closed_map( + __struct__: atom([RuntimeError]), + __exception__: atom([true]), + message: term() + ) ) ) end From c7f78e8015c5cb73d2302fa7f62ebe3e2131442b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 16 Dec 2024 19:30:42 +0100 Subject: [PATCH 030/128] Emit warnings for duplicate patterns instead of errors --- lib/elixir/src/elixir_clauses.erl | 19 +++++++++++++++---- .../test/elixir/kernel/expansion_test.exs | 11 ----------- .../test/elixir/kernel/warning_test.exs | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index 6efe5577b02..85198f694ad 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -11,7 +11,7 @@ parallel_match(Meta, Expr, S, #{context := match} = E) -> #elixir_ex{vars={_Read, Write}} = S, - Matches = unpack_match(Expr, Meta, []), + Matches = unpack_match(Expr, Meta, [], E), {[{_, EHead} | ETail], EWrites, SM, EM} = lists:foldl(fun({EMeta, Match}, {AccMatches, AccWrites, SI, EI}) -> @@ -31,9 +31,13 @@ parallel_match(Meta, Expr, S, #{context := match} = E) -> VWrite = (Write /= false) andalso elixir_env:merge_vars(Write, PWrites), {EMatch, SM#elixir_ex{vars={VRead, VWrite}, prematch={PRead, PCycles, PInfo}}, EM}. -unpack_match({'=', Meta, [Left, Right]}, _Meta, Acc) -> - unpack_match(Left, Meta, unpack_match(Right, Meta, Acc)); -unpack_match(Node, Meta, Acc) -> +unpack_match({'=', Meta, [{_, VarMeta, _} = Node, Node]}, _Meta, Acc, E) -> + %% TODO: remove this clause on Elixir v1.23 + file_warn(VarMeta, ?key(E, file), ?MODULE, {duplicate_match, Node}), + unpack_match(Node, Meta, Acc, E); +unpack_match({'=', Meta, [Left, Right]}, _Meta, Acc, E) -> + unpack_match(Left, Meta, unpack_match(Right, Meta, Acc, E), E); +unpack_match(Node, Meta, Acc, _E) -> [{Meta, Node} | Acc]. store_cycles([Write | Writes], {Cycles, SkipList}, Acc) -> @@ -532,6 +536,13 @@ origin(Meta, Default) -> false -> Default end. +format_error({duplicate_match, Expr}) -> + String = 'Elixir.Macro':to_string(Expr), + io_lib:format( + "this pattern is matched against itself inside a match: ~ts = ~ts", + [String, String] + ); + format_error({recursive, Vars, TypeExpr}) -> Code = case TypeExpr of diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 86e500e0fb4..81b0a65a70b 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -190,17 +190,6 @@ defmodule Kernel.ExpansionTest do end test "errors on directly recursive definitions" do - assert_compile_error( - ~r""" - recursive variable definition in patterns: - - x = x - - the variable "x" \(context Kernel.ExpansionTest\) is defined in function of itself - """, - fn -> expand(quote(do: (x = x) = :ok)) end - ) - assert_compile_error( ~r""" recursive variable definition in patterns: diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index c318fe6526b..1112b350b20 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -264,6 +264,20 @@ defmodule Kernel.WarningTest do purge(Sample) end + test "duplicate pattern" do + output = + capture_eval(""" + defmodule Sample do + var = quote(do: x) + def hello(unquote(var) = unquote(var)), do: unquote(var) + end + """) + + assert output =~ "this pattern is matched against itself inside a match: x = x" + after + purge(Sample) + end + test "unused compiler variable" do output = capture_eval(""" From 4931ab40a504804105011c65f6ea1b7f5b588ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 17 Dec 2024 10:03:36 +0100 Subject: [PATCH 031/128] Do not warn for CaseTemplate options, closes #14077 --- lib/ex_unit/lib/ex_unit/case.ex | 7 ++++++- lib/ex_unit/lib/ex_unit/case_template.ex | 2 +- lib/ex_unit/test/ex_unit/case_template_test.exs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index 9810df94e9e..81b46908ecd 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -315,6 +315,11 @@ defmodule ExUnit.Case do end end + @keys [:async, :group, :parameterize, :register] + + @doc false + def __keys__(opts), do: Keyword.take(opts, @keys) + @doc false def __register__(module, opts) do if not Keyword.keyword?(opts) do @@ -324,7 +329,7 @@ defmodule ExUnit.Case do end {register?, opts} = Keyword.pop(opts, :register, true) - {next_opts, opts} = Keyword.split(opts, [:async, :group, :parameterize]) + {next_opts, opts} = Keyword.split(opts, @keys) if opts != [] do IO.warn("unknown options given to ExUnit.Case: #{inspect(opts)}") diff --git a/lib/ex_unit/lib/ex_unit/case_template.ex b/lib/ex_unit/lib/ex_unit/case_template.ex index 0e4023a8ff0..2449b7c1998 100644 --- a/lib/ex_unit/lib/ex_unit/case_template.ex +++ b/lib/ex_unit/lib/ex_unit/case_template.ex @@ -83,7 +83,7 @@ defmodule ExUnit.CaseTemplate do # We inject this code in the module that calls "use MyTemplate". def __proxy__(module, opts) do quote do - use ExUnit.Case, unquote(opts) + use ExUnit.Case, ExUnit.Case.__keys__(unquote(opts)) setup_all context do unquote(module).__ex_unit__(:setup_all, context) diff --git a/lib/ex_unit/test/ex_unit/case_template_test.exs b/lib/ex_unit/test/ex_unit/case_template_test.exs index b5265a8ce55..d660a32eb8d 100644 --- a/lib/ex_unit/test/ex_unit/case_template_test.exs +++ b/lib/ex_unit/test/ex_unit/case_template_test.exs @@ -36,7 +36,7 @@ defmodule ExUnit.NestedCase do end defmodule ExUnit.CaseTemplateTest do - use ExUnit.SampleCase, async: true + use ExUnit.SampleCase, async: true, another_option: 123 use ExUnit.NestedCase two = 2 From 7c86c4bdb3836b1262fcf56f381d31d544f29dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 17 Dec 2024 10:11:15 +0100 Subject: [PATCH 032/128] Update CHANGELOG --- CHANGELOG.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07705488685..153cef2eeb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,7 +174,9 @@ Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functio | `%{atom() => _}` | Object | | `%{integer() => _}` | Object | -You may also implement the `JSON.Encoder` protocol for custom data structures. If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON: +You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types. + +If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON: ```elixir @derive {JSON.Encoder, only: [...]} @@ -223,6 +225,32 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z +## v1.18.0-rc.1 + +### 1. Enhancements + +#### Elixir + + * [JSON] Implement `JSON.Encoder` for all Calendar types + +### 2. Bug fixes + +#### Elixir + + * [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation + +### 3. Regressions + +#### Elixir + + * [Kernel] Fix type warnings in `pop_in/1` + * [Kernel] Fix false positive warnings when accessing exceptions from `rescue` + * [Kernel] Emit warnings for duplicate patterns instead of errors + +#### ExUnit + + * [ExUnit] Do not warn on user-supplied CaseTemplate options + ## v1.18.0-rc.0 (2024-12-10) ### 1. Enhancements From 1c0f585f40f1102c0f8132b360f4a41d4835433b Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Wed, 18 Dec 2024 08:34:17 +0100 Subject: [PATCH 033/128] Fuse maps and tuples for printing (#14079) --- lib/elixir/lib/module/types/descr.ex | 95 ++++++++++++- .../test/elixir/module/types/descr_test.exs | 129 ++++++++++++++++++ 2 files changed, 219 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 5220f93e735..91578f6dd99 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1704,6 +1704,49 @@ defmodule Module.Types.Descr do {tag, fields, negs} end) + |> map_fusion() + end + + # Given a dnf, fuse maps when possible + # e.g. %{a: integer(), b: atom()} or %{a: float(), b: atom()} into %{a: number(), b: atom()} + defp map_fusion(dnf) do + # Steps: + # 1. Group maps by tags and keys + # 2. Try fusions for each group until no fusion is found + # 3. Merge the groups back into a dnf + dnf + |> Enum.group_by(fn {tag, fields, _} -> {tag, Map.keys(fields)} end) + |> Enum.flat_map(fn {_, maps} -> fuse_maps(maps) end) + end + + defp fuse_maps(maps) do + Enum.reduce(maps, [], fn map, acc -> + case Enum.split_while(acc, &fusible_maps?(map, &1)) do + {_, []} -> + [map | acc] + + {others, [match | rest]} -> + fused = fuse_map_pair(map, match) + others ++ [fused | rest] + end + end) + end + + # Two maps are fusible if they have no negations and differ in at most one element. + defp fusible_maps?({_, fields1, negs1}, {_, fields2, negs2}) do + negs1 != [] or negs2 != [] or + Map.keys(fields1) + |> Enum.count(fn key -> Map.get(fields1, key) != Map.get(fields2, key) end) > 1 + end + + defp fuse_map_pair({tag, fields1, []}, {_, fields2, []}) do + fused_fields = + Map.new(fields1, fn {key, type1} -> + type2 = Map.get(fields2, key) + {key, if(type1 != type2, do: union(type1, type2), else: type1)} + end) + + {tag, fused_fields, []} end # If all fields are the same except one, we can optimize map difference. @@ -1925,6 +1968,7 @@ defmodule Module.Types.Descr do defp tuple_to_quoted(dnf) do dnf |> tuple_simplify() + |> tuple_fusion() |> Enum.map(&tuple_each_to_quoted/1) |> case do [] -> [] @@ -1932,17 +1976,58 @@ defmodule Module.Types.Descr do end end - defp tuple_each_to_quoted({tag, positive_map, negative_maps}) do - case negative_maps do + # Given a dnf of tuples, fuses the tuple unions when possible, + # e.g. {integer(), atom()} or {float(), atom()} into {number(), atom()} + # The negations of two fused tuples are just concatenated. + defp tuple_fusion(dnf) do + # Steps: + # 1. Consider tuples without negations apart from those with + # 2. Group tuples by size and tag + # 3. Try fusions for each group until no fusion is found + # 4. Merge the groups back into a dnf + dnf + |> Enum.group_by(fn {tag, elems, _} -> {tag, length(elems)} end) + |> Enum.flat_map(fn {_, tuples} -> fuse_tuples(tuples) end) + end + + defp fuse_tuples(tuples) do + Enum.reduce(tuples, [], fn tuple, acc -> + case Enum.split_while(acc, &fusible_tuples?(tuple, &1)) do + {_, []} -> + [tuple | acc] + + {others, [match | rest]} -> + fused = fuse_tuple_pair(tuple, match) + others ++ [fused | rest] + end + end) + end + + # Two tuples are fusible if they have no negations and differ in at most one element. + defp fusible_tuples?({_, elems1, negs1}, {_, elems2, negs2}) do + negs1 != [] or negs2 != [] or + Enum.zip(elems1, elems2) |> Enum.count(fn {a, b} -> a != b end) > 1 + end + + defp fuse_tuple_pair({tag, elems1, []}, {_, elems2, []}) do + fused_elements = + Enum.zip(elems1, elems2) + |> Enum.map(fn {a, b} -> if a != b, do: union(a, b), else: a end) + + {tag, fused_elements, []} + end + + defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}) do + case negative_tuples do [] -> - tuple_literal_to_quoted({tag, positive_map}) + tuple_literal_to_quoted({tag, positive_tuple}) _ -> - negative_maps + negative_tuples |> Enum.map(&tuple_literal_to_quoted/1) |> Enum.reduce(&{:or, [], [&2, &1]}) |> Kernel.then( - &{:and, [], [tuple_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]} + &{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}), {:not, [], [&1]}]} ) end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index b5100b01b6b..c12d2ae8b60 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1268,6 +1268,91 @@ defmodule Module.Types.DescrTest do # assert difference(tuple([number(), term()]), tuple([integer(), atom()])) # |> to_quoted_string() == # "{float(), term()} or {number(), term() and not atom()}" + + assert union(tuple([integer(), atom()]), tuple([integer(), atom()])) |> to_quoted_string() == + "{integer(), atom()}" + + assert union(tuple([integer(), atom()]), tuple([float(), atom()])) |> to_quoted_string() == + "{float() or integer(), atom()}" + + assert union(tuple([integer(), atom()]), tuple([float(), atom()])) + |> union(tuple([pid(), pid(), port()])) + |> union(tuple([pid(), pid(), atom()])) + |> to_quoted_string() == + "{float() or integer(), atom()} or {pid(), pid(), atom() or port()}" + + assert union(open_tuple([integer()]), open_tuple([float()])) |> to_quoted_string() == + "{float() or integer(), ...}" + + # {:ok, {term(), integer()}} or {:ok, {term(), float()}} or {:exit, :kill} or {:exit, :timeout} + assert tuple([atom([:ok]), tuple([term(), empty_list()])]) + |> union(tuple([atom([:ok]), tuple([term(), open_map()])])) + |> union(tuple([atom([:exit]), atom([:kill])])) + |> union(tuple([atom([:exit]), atom([:timeout])])) + |> to_quoted_string() == + "{:exit, :kill or :timeout} or {:ok, {term(), %{...} or empty_list()}}" + + # Detection of duplicates + assert tuple([atom([:ok]), term()]) + |> union(tuple([atom([:ok]), term()])) + |> to_quoted_string() == "{:ok, term()}" + + assert tuple([closed_map(a: integer(), b: atom()), open_map()]) + |> union(tuple([closed_map(a: integer(), b: atom()), open_map()])) + |> to_quoted_string() == + "{%{a: integer(), b: atom()}, %{...}}" + + # Nested fusion + assert tuple([closed_map(a: integer(), b: atom()), open_map()]) + |> union(tuple([closed_map(a: float(), b: atom()), open_map()])) + |> to_quoted_string() == + "{%{a: float() or integer(), b: atom()}, %{...}}" + + # Complex simplification of map/tuple combinations. Initial type is: + # ``` + # dynamic( + # :error or + # ({%Decimal{coef: :inf, exp: integer(), sign: integer()}, binary()} or + # {%Decimal{coef: :NaN, exp: integer(), sign: integer()}, binary()} or + # {%Decimal{coef: integer(), exp: integer(), sign: integer()}, term()} or + # {%Decimal{coef: :inf, exp: integer(), sign: integer()} or + # %Decimal{coef: :NaN, exp: integer(), sign: integer()} or + # %Decimal{coef: integer(), exp: integer(), sign: integer()}, term()}) + # ) + # ``` + decimal_inf = + closed_map( + __struct__: atom([Decimal]), + coef: atom([:inf]), + exp: integer(), + sign: integer() + ) + + decimal_nan = + closed_map( + __struct__: atom([Decimal]), + coef: atom([:NaN]), + exp: integer(), + sign: integer() + ) + + decimal_int = + closed_map(__struct__: atom([Decimal]), coef: integer(), exp: integer(), sign: integer()) + + assert atom([:error]) + |> union( + tuple([decimal_inf, binary()]) + |> union( + tuple([decimal_nan, binary()]) + |> union( + tuple([decimal_int, term()]) + |> union(tuple([union(decimal_inf, union(decimal_nan, decimal_int)), term()])) + ) + ) + ) + |> dynamic() + |> to_quoted_string() == + "dynamic(\n :error or\n ({%Decimal{coef: integer() or (:NaN or :inf), exp: integer(), sign: integer()}, term()} or\n {%Decimal{coef: :NaN or :inf, exp: integer(), sign: integer()}, binary()})\n)" end test "map" do @@ -1311,6 +1396,50 @@ defmodule Module.Types.DescrTest do assert difference(open_map(a: number(), b: atom()), open_map(a: integer())) |> to_quoted_string() == "%{..., a: float(), b: atom()}" + # Basic map fusion + assert union(closed_map(a: integer()), closed_map(a: integer())) |> to_quoted_string() == + "%{a: integer()}" + + assert union(closed_map(a: integer()), closed_map(a: float())) |> to_quoted_string() == + "%{a: float() or integer()}" + + # Nested fusion + assert union(closed_map(a: integer(), b: atom()), closed_map(a: float(), b: atom())) + |> union(closed_map(x: pid(), y: pid(), z: port())) + |> union(closed_map(x: pid(), y: pid(), z: atom())) + |> to_quoted_string() == + "%{a: float() or integer(), b: atom()} or %{x: pid(), y: pid(), z: atom() or port()}" + + # Open map fusion + assert union(open_map(a: integer()), open_map(a: float())) |> to_quoted_string() == + "%{..., a: float() or integer()}" + + # Fusing complex nested maps with unions + assert closed_map(status: atom([:ok]), data: closed_map(value: term(), count: empty_list())) + |> union( + closed_map(status: atom([:ok]), data: closed_map(value: term(), count: open_map())) + ) + |> union(closed_map(status: atom([:error]), reason: atom([:timeout]))) + |> union(closed_map(status: atom([:error]), reason: atom([:crash]))) + |> to_quoted_string() == + "%{data: %{count: %{...} or empty_list(), value: term()}, status: :ok} or\n %{reason: :crash or :timeout, status: :error}" + + # Difference and union tests + assert closed_map(status: atom([:ok]), value: term()) + |> difference(closed_map(status: atom([:ok]), value: float())) + |> union( + closed_map(status: atom([:ok]), value: term()) + |> difference(closed_map(status: atom([:ok]), value: integer())) + ) + |> to_quoted_string() == + "%{status: :ok, value: term()}" + + # Nested map fusion + assert closed_map(data: closed_map(x: integer(), y: atom()), meta: open_map()) + |> union(closed_map(data: closed_map(x: float(), y: atom()), meta: open_map())) + |> to_quoted_string() == + "%{data: %{x: float() or integer(), y: atom()}, meta: %{...}}" + # Test complex combinations assert intersection(open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean())) |> union(difference(open_map(x: atom()), open_map(x: boolean()))) From a426520b669efc61bdf4e9807d950e4654feddbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 18 Dec 2024 09:30:51 +0100 Subject: [PATCH 034/128] Improvements to descr pretty printing --- lib/elixir/lib/module/types/descr.ex | 113 +++++++++--------- .../test/elixir/module/types/descr_test.exs | 13 +- 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 91578f6dd99..20892cf2dd8 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -372,11 +372,18 @@ defmodule Module.Types.Descr do if term_type?(descr) do {:term, [], []} else + # Dynamic always come first for visibility + {dynamic, descr} = + case :maps.take(:dynamic, descr) do + :error -> {[], descr} + {dynamic, descr} -> {to_quoted(:dynamic, dynamic), descr} + end + + # Merge empty list and list together if they both exist {extra, descr} = case descr do - # Merge empty list and list together if they both exist %{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 -> - descr = descr |> Map.delete(:list) |> Map.update!(:bitmap, &(&1 - @bit_empty_list)) + descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list) case list_to_quoted(list, :list) do [] -> {[{:empty_list, [], []}], descr} @@ -387,9 +394,13 @@ defmodule Module.Types.Descr do {[], descr} end - case extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value) end) do + unions = + dynamic ++ + Enum.sort(extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value) end)) + + case unions do [] -> {:none, [], []} - unions -> unions |> Enum.sort() |> Enum.reduce(&{:or, [], [&2, &1]}) + unions -> Enum.reduce(unions, &{:or, [], [&2, &1]}) end end end @@ -785,17 +796,16 @@ defmodule Module.Types.Descr do defp atom_to_quoted({:union, a}) do if :sets.is_subset(@boolset, a) do - :sets.subtract(a, @boolset) - |> :sets.to_list() - |> Enum.sort() - |> Enum.reduce({:boolean, [], []}, &{:or, [], [&2, literal_to_quoted(&1)]}) + entries = + :sets.subtract(a, @boolset) + |> :sets.to_list() + |> Enum.map(&literal_to_quoted/1) + + [{:boolean, [], []} | entries] else :sets.to_list(a) - |> Enum.sort() |> Enum.map(&literal_to_quoted/1) - |> Enum.reduce(&{:or, [], [&2, &1]}) end - |> List.wrap() end defp atom_to_quoted({:negation, a}) do @@ -1064,11 +1074,7 @@ defmodule Module.Types.Descr do |> Enum.reduce(&{:or, [], [&2, &1]}) |> Kernel.then( &[ - {:and, [], - [ - {name, [], arguments}, - {:not, [], [&1]} - ]} + {:and, [], [{name, [], arguments}, {:not, [], [&1]}]} | acc ] ) @@ -1691,7 +1697,7 @@ defmodule Module.Types.Descr do if map_empty_negation?(tag, acc_fields, neg) do {acc_fields, acc_negs} else - case all_but_one?(tag, acc_fields, neg_tag, neg_fields) do + case map_all_but_one?(tag, acc_fields, neg_tag, neg_fields) do {:one, diff_key} -> {Map.update!(acc_fields, diff_key, &difference(&1, neg_fields[diff_key])), acc_negs} @@ -1714,43 +1720,45 @@ defmodule Module.Types.Descr do # 1. Group maps by tags and keys # 2. Try fusions for each group until no fusion is found # 3. Merge the groups back into a dnf - dnf - |> Enum.group_by(fn {tag, fields, _} -> {tag, Map.keys(fields)} end) - |> Enum.flat_map(fn {_, maps} -> fuse_maps(maps) end) + {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _fields, negs} -> negs == [] end) + + without_negs = + without_negs + |> Enum.group_by(fn {tag, fields, _} -> {tag, Map.keys(fields)} end) + |> Enum.flat_map(fn {_, maps} -> map_non_negated_fuse(maps) end) + + without_negs ++ with_negs end - defp fuse_maps(maps) do + defp map_non_negated_fuse(maps) do Enum.reduce(maps, [], fn map, acc -> - case Enum.split_while(acc, &fusible_maps?(map, &1)) do + case Enum.split_while(acc, &non_fusible_maps?(map, &1)) do {_, []} -> [map | acc] {others, [match | rest]} -> - fused = fuse_map_pair(map, match) + fused = map_non_negated_fuse_pair(map, match) others ++ [fused | rest] end end) end - # Two maps are fusible if they have no negations and differ in at most one element. - defp fusible_maps?({_, fields1, negs1}, {_, fields2, negs2}) do - negs1 != [] or negs2 != [] or - Map.keys(fields1) - |> Enum.count(fn key -> Map.get(fields1, key) != Map.get(fields2, key) end) > 1 + # Two maps are fusible if they differ in at most one element. + defp non_fusible_maps?({_, fields1, []}, {_, fields2, []}) do + Enum.count_until(fields1, fn {key, value} -> Map.fetch!(fields2, key) != value end, 2) > 1 end - defp fuse_map_pair({tag, fields1, []}, {_, fields2, []}) do - fused_fields = - Map.new(fields1, fn {key, type1} -> - type2 = Map.get(fields2, key) - {key, if(type1 != type2, do: union(type1, type2), else: type1)} + defp map_non_negated_fuse_pair({tag, fields1, []}, {_, fields2, []}) do + fields = + symmetrical_merge(fields1, fields2, fn _k, v1, v2 -> + if v1 == v2, do: v1, else: union(v1, v2) end) - {tag, fused_fields, []} + {tag, fields, []} end # If all fields are the same except one, we can optimize map difference. - defp all_but_one?(tag1, fields1, tag2, fields2) do + defp map_all_but_one?(tag1, fields1, tag2, fields2) do keys1 = Map.keys(fields1) keys2 = Map.keys(fields2) @@ -1782,10 +1790,6 @@ defmodule Module.Types.Descr do dnf |> map_normalize() |> Enum.map(&map_each_to_quoted/1) - |> case do - [] -> [] - dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap() - end end defp map_each_to_quoted({tag, positive_map, negative_maps}) do @@ -1970,10 +1974,6 @@ defmodule Module.Types.Descr do |> tuple_simplify() |> tuple_fusion() |> Enum.map(&tuple_each_to_quoted/1) - |> case do - [] -> [] - dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap() - end end # Given a dnf of tuples, fuses the tuple unions when possible, @@ -1985,34 +1985,37 @@ defmodule Module.Types.Descr do # 2. Group tuples by size and tag # 3. Try fusions for each group until no fusion is found # 4. Merge the groups back into a dnf - dnf - |> Enum.group_by(fn {tag, elems, _} -> {tag, length(elems)} end) - |> Enum.flat_map(fn {_, tuples} -> fuse_tuples(tuples) end) + {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _elems, negs} -> negs == [] end) + + without_negs = + without_negs + |> Enum.group_by(fn {tag, elems, _} -> {tag, length(elems)} end) + |> Enum.flat_map(fn {_, tuples} -> tuple_non_negated_fuse(tuples) end) + + without_negs ++ with_negs end - defp fuse_tuples(tuples) do + defp tuple_non_negated_fuse(tuples) do Enum.reduce(tuples, [], fn tuple, acc -> - case Enum.split_while(acc, &fusible_tuples?(tuple, &1)) do + case Enum.split_while(acc, &non_fusible_tuples?(tuple, &1)) do {_, []} -> [tuple | acc] {others, [match | rest]} -> - fused = fuse_tuple_pair(tuple, match) + fused = tuple_non_negated_fuse_pair(tuple, match) others ++ [fused | rest] end end) end # Two tuples are fusible if they have no negations and differ in at most one element. - defp fusible_tuples?({_, elems1, negs1}, {_, elems2, negs2}) do - negs1 != [] or negs2 != [] or - Enum.zip(elems1, elems2) |> Enum.count(fn {a, b} -> a != b end) > 1 + defp non_fusible_tuples?({_, elems1, []}, {_, elems2, []}) do + Enum.zip(elems1, elems2) |> Enum.count_until(fn {a, b} -> a != b end, 2) > 1 end - defp fuse_tuple_pair({tag, elems1, []}, {_, elems2, []}) do + defp tuple_non_negated_fuse_pair({tag, elems1, []}, {_, elems2, []}) do fused_elements = - Enum.zip(elems1, elems2) - |> Enum.map(fn {a, b} -> if a != b, do: union(a, b), else: a end) + Enum.zip_with(elems1, elems2, fn a, b -> if a == b, do: a, else: union(a, b) end) {tag, fused_elements, []} end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index c12d2ae8b60..13517e87bd0 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1184,7 +1184,7 @@ defmodule Module.Types.DescrTest do test "boolean" do assert boolean() |> to_quoted_string() == "boolean()" - assert atom([true, false, :a]) |> to_quoted_string() == "boolean() or :a" + assert atom([true, false, :a]) |> to_quoted_string() == ":a or boolean()" assert atom([true, :a]) |> to_quoted_string() == ":a or true" assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()" end @@ -1199,7 +1199,7 @@ defmodule Module.Types.DescrTest do assert intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic(atom())" assert union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == - "dynamic() or (:bar or :foo)" + "dynamic() or :bar or :foo" assert intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() == "dynamic(%{a: integer()})" @@ -1256,7 +1256,7 @@ defmodule Module.Types.DescrTest do assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}" assert union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == - "{integer(), atom()} or {atom(), ...}" + "{atom(), ...} or {integer(), atom()}" assert difference(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == "{integer(), atom()}" @@ -1352,7 +1352,12 @@ defmodule Module.Types.DescrTest do ) |> dynamic() |> to_quoted_string() == - "dynamic(\n :error or\n ({%Decimal{coef: integer() or (:NaN or :inf), exp: integer(), sign: integer()}, term()} or\n {%Decimal{coef: :NaN or :inf, exp: integer(), sign: integer()}, binary()})\n)" + """ + dynamic( + :error or {%Decimal{coef: :NaN or :inf, exp: integer(), sign: integer()}, binary()} or + {%Decimal{coef: :NaN or :inf or integer(), exp: integer(), sign: integer()}, term()} + )\ + """ end test "map" do From ed83c407a53e9483b38224538b8736b710cdee04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Dec 2024 12:12:49 +0100 Subject: [PATCH 035/128] Simplify and optimize NaiveDateTime.utc_now --- lib/elixir/lib/calendar/naive_datetime.ex | 39 +++++++++++------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 4fb676c107d..43e71c9eaa3 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -110,30 +110,12 @@ defmodule NaiveDateTime do @spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t def utc_now(calendar_or_time_unit \\ Calendar.ISO) - def utc_now(Calendar.ISO) do - {:ok, {year, month, day}, {hour, minute, second}, microsecond} = - Calendar.ISO.from_unix(:os.system_time(), :native) - - %NaiveDateTime{ - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond, - calendar: Calendar.ISO - } - end - def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do utc_now(time_unit, Calendar.ISO) end def utc_now(calendar) do - calendar - |> DateTime.utc_now() - |> DateTime.to_naive() + utc_now(:native, calendar) end @doc """ @@ -158,7 +140,20 @@ defmodule NaiveDateTime do @spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t def utc_now(time_unit, calendar) when time_unit in [:native, :microsecond, :millisecond, :second] do - DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive() + {:ok, {year, month, day}, {hour, minute, second}, microsecond} = + Calendar.ISO.from_unix(System.os_time(time_unit), time_unit) + + %NaiveDateTime{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond, + calendar: Calendar.ISO + } + |> convert!(calendar) end @doc """ @@ -1265,6 +1260,10 @@ defmodule NaiveDateTime do {:ok, t} | {:error, :incompatible_calendars} # Keep it multiline for proper function clause errors. + def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do + {:ok, ndt} + end + def convert( %{ calendar: calendar, From ca6edfd389bd9963f0c5906d22bf0f87d6d9c37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Dec 2024 12:13:17 +0100 Subject: [PATCH 036/128] Include types in comparison warning --- lib/elixir/lib/module/types/apply.ex | 27 +++++++++++++++---- .../test/elixir/module/types/expr_test.exs | 12 +++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index b8a19022e39..9de891fb5fc 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -363,14 +363,14 @@ defmodule Module.Types.Apply do match?({false, _}, map_fetch(left, :__struct__)) or match?({false, _}, map_fetch(right, :__struct__)) -> - warning = {:struct_comparison, expr, context} + warning = {:struct_comparison, expr, name, left, right, context} warn(__MODULE__, warning, elem(expr, 1), stack, context) number_type?(left) and number_type?(right) -> context disjoint?(left, right) -> - warning = {:mismatched_comparison, expr, context} + warning = {:mismatched_comparison, expr, name, left, right, context} warn(__MODULE__, warning, elem(expr, 1), stack, context) true -> @@ -395,7 +395,7 @@ defmodule Module.Types.Apply do context disjoint?(left, right) -> - warning = {:mismatched_comparison, expr, context} + warning = {:mismatched_comparison, expr, name, left, right, context} warn(__MODULE__, warning, elem(expr, 1), stack, context) true -> @@ -810,7 +810,7 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:mismatched_comparison, expr, context}) do + def format_diagnostic({:mismatched_comparison, expr, name, left, right, context}) do traces = collect_traces(expr, context) %{ @@ -821,6 +821,10 @@ defmodule Module.Types.Apply do comparison between distinct types found: #{expr_to_string(expr) |> indent(4)} + + given types: + + #{type_comparison_to_string(name, left, right) |> indent(4)} """, format_traces(traces), """ @@ -833,7 +837,7 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:struct_comparison, expr, context}) do + def format_diagnostic({:struct_comparison, expr, name, left, right, context}) do traces = collect_traces(expr, context) %{ @@ -844,6 +848,10 @@ defmodule Module.Types.Apply do comparison with structs found: #{expr_to_string(expr) |> indent(4)} + + given types: + + #{type_comparison_to_string(name, left, right) |> indent(4)} """, format_traces(traces), """ @@ -933,6 +941,15 @@ defmodule Module.Types.Apply do alias Inspect.Algebra, as: IA + defp type_comparison_to_string(fun, left, right) do + {Kernel, fun, [left, right], _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right]) + + {fun, [], [to_quoted(left), to_quoted(right)]} + |> Code.Formatter.to_algebra() + |> Inspect.Algebra.format(98) + |> IO.iodata_to_binary() + end + defp clauses_args_to_quoted_string([{args, _return}], converter) do "\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4)) end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index c5c5a6040e0..6c4f4b62365 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -845,6 +845,10 @@ defmodule Module.Types.ExprTest do min(x, y) + given types: + + min(dynamic(:foo), integer()) + where "x" was given the type: # type: dynamic(:foo) @@ -868,6 +872,10 @@ defmodule Module.Types.ExprTest do x === y + given types: + + integer() === float() + where "x" was given the type: # type: integer() @@ -893,6 +901,10 @@ defmodule Module.Types.ExprTest do mod.<=(x, y) + given types: + + dynamic(:foo) <= dynamic(%Point{x: term(), y: term(), z: term()}) + where "mod" was given the type: # type: dynamic(Kernel) From c6597bcb20e9926d6c8a437058003c7d4faa13ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Dec 2024 15:12:31 +0100 Subject: [PATCH 037/128] Collapse structs for better pretty printing in different scenarios --- lib/elixir/lib/module/types/apply.ex | 2 +- lib/elixir/lib/module/types/descr.ex | 119 +++++++++++------- lib/elixir/lib/module/types/helpers.ex | 2 +- lib/elixir/lib/module/types/of.ex | 4 +- .../test/elixir/module/types/expr_test.exs | 12 +- .../test/elixir/module/types/pattern_test.exs | 6 +- 6 files changed, 92 insertions(+), 53 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 9de891fb5fc..49f98295262 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -944,7 +944,7 @@ defmodule Module.Types.Apply do defp type_comparison_to_string(fun, left, right) do {Kernel, fun, [left, right], _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right]) - {fun, [], [to_quoted(left), to_quoted(right)]} + {fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]} |> Code.Formatter.to_algebra() |> Inspect.Algebra.format(98) |> IO.iodata_to_binary() diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 20892cf2dd8..c9af3919dde 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -367,8 +367,13 @@ defmodule Module.Types.Descr do @doc """ Converts a descr to its quoted representation. + + ## Options + + * `:collapse_structs` - do not show struct fields that match + their default type """ - def to_quoted(descr) do + def to_quoted(descr, opts \\ []) do if term_type?(descr) do {:term, [], []} else @@ -376,7 +381,7 @@ defmodule Module.Types.Descr do {dynamic, descr} = case :maps.take(:dynamic, descr) do :error -> {[], descr} - {dynamic, descr} -> {to_quoted(:dynamic, dynamic), descr} + {dynamic, descr} -> {to_quoted(:dynamic, dynamic, opts), descr} end # Merge empty list and list together if they both exist @@ -385,7 +390,7 @@ defmodule Module.Types.Descr do %{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 -> descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list) - case list_to_quoted(list, :list) do + case list_to_quoted(list, :list, opts) do [] -> {[{:empty_list, [], []}], descr} unions -> {unions, descr} end @@ -396,7 +401,9 @@ defmodule Module.Types.Descr do unions = dynamic ++ - Enum.sort(extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value) end)) + Enum.sort( + extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value, opts) end) + ) case unions do [] -> {:none, [], []} @@ -405,19 +412,19 @@ defmodule Module.Types.Descr do end end - defp to_quoted(:atom, val), do: atom_to_quoted(val) - defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) - defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr) - defp to_quoted(:map, dnf), do: map_to_quoted(dnf) - defp to_quoted(:list, dnf), do: list_to_quoted(dnf, :non_empty_list) - defp to_quoted(:tuple, dnf), do: tuple_to_quoted(dnf) + defp to_quoted(:atom, val, _opts), do: atom_to_quoted(val) + defp to_quoted(:bitmap, val, _opts), do: bitmap_to_quoted(val) + defp to_quoted(:dynamic, descr, opts), do: dynamic_to_quoted(descr, opts) + defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts) + defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, :non_empty_list, opts) + defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts) @doc """ Converts a descr to its quoted string representation. """ - def to_quoted_string(descr) do + def to_quoted_string(descr, opts \\ []) do descr - |> to_quoted() + |> to_quoted(opts) |> Code.Formatter.to_algebra() |> Inspect.Algebra.format(98) |> IO.iodata_to_binary() @@ -1045,16 +1052,16 @@ defmodule Module.Types.Descr do end end - defp list_to_quoted(dnf, name) do + defp list_to_quoted(dnf, name, opts) do dnf = list_normalize(dnf) for {list_type, last_type, negs} <- dnf, reduce: [] do acc -> arguments = if subtype?(last_type, @empty_list) do - [to_quoted(list_type)] + [to_quoted(list_type, opts)] else - [to_quoted(list_type), to_quoted(last_type)] + [to_quoted(list_type, opts), to_quoted(last_type, opts)] end if negs == [] do @@ -1064,9 +1071,9 @@ defmodule Module.Types.Descr do |> Enum.map(fn {ty, lst} -> args = if subtype?(lst, @empty_list) do - [to_quoted(ty)] + [to_quoted(ty, opts)] else - [to_quoted(ty), to_quoted(lst)] + [to_quoted(ty, opts), to_quoted(lst, opts)] end {name, [], args} @@ -1176,7 +1183,7 @@ defmodule Module.Types.Descr do end end - defp dynamic_to_quoted(descr) do + defp dynamic_to_quoted(descr, opts) do cond do term_type?(descr) -> [{:dynamic, [], []}] @@ -1185,7 +1192,7 @@ defmodule Module.Types.Descr do [single] true -> - case to_quoted(descr) do + case to_quoted(descr, opts) do {:none, _meta, []} = none -> [none] descr -> [{:dynamic, [], [descr]}] end @@ -1786,51 +1793,77 @@ defmodule Module.Types.Descr do end)) end - defp map_to_quoted(dnf) do + defp map_to_quoted(dnf, opts) do dnf |> map_normalize() - |> Enum.map(&map_each_to_quoted/1) + |> Enum.map(&map_each_to_quoted(&1, opts)) end - defp map_each_to_quoted({tag, positive_map, negative_maps}) do + defp map_each_to_quoted({tag, positive_map, negative_maps}, opts) do case negative_maps do [] -> - map_literal_to_quoted({tag, positive_map}) + map_literal_to_quoted({tag, positive_map}, opts) _ -> negative_maps - |> Enum.map(&map_literal_to_quoted/1) + |> Enum.map(&map_literal_to_quoted(&1, opts)) |> Enum.reduce(&{:or, [], [&2, &1]}) |> Kernel.then( - &{:and, [], [map_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]} + &{:and, [], [map_literal_to_quoted({tag, positive_map}, opts), {:not, [], [&1]}]} ) end end - def map_literal_to_quoted({:closed, fields}) when map_size(fields) == 0 do + def map_literal_to_quoted({:closed, fields}, _opts) when map_size(fields) == 0 do {:empty_map, [], []} end - def map_literal_to_quoted({tag, fields}) do + def map_literal_to_quoted({tag, fields}, opts) do case tag do :closed -> with %{__struct__: struct_descr} <- fields, {_, [struct]} <- atom_fetch(struct_descr) do + fields = Map.delete(fields, :__struct__) + + fields = + with true <- Keyword.get(opts, :collapse_structs, false), + [_ | _] = info <- maybe_struct(struct), + true <- Enum.all?(info, &is_map_key(fields, &1.field)) do + Enum.reduce(info, fields, fn %{field: field}, acc -> + # TODO: This should consider the struct default value + if Map.fetch!(acc, field) == term() do + Map.delete(acc, field) + else + acc + end + end) + else + _ -> fields + end + {:%, [], [ literal_to_quoted(struct), - {:%{}, [], map_fields_to_quoted(tag, Map.delete(fields, :__struct__))} + {:%{}, [], map_fields_to_quoted(tag, fields, opts)} ]} else - _ -> {:%{}, [], map_fields_to_quoted(tag, fields)} + _ -> {:%{}, [], map_fields_to_quoted(tag, fields, opts)} end :open -> - {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields)]} + {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields, opts)]} + end + end + + defp maybe_struct(struct) do + try do + struct.__info__(:struct) + rescue + _ -> nil end end - defp map_fields_to_quoted(tag, map) do + defp map_fields_to_quoted(tag, map, opts) do sorted = Enum.sort(Map.to_list(map)) keyword? = Inspect.List.keyword?(sorted) @@ -1846,9 +1879,9 @@ defmodule Module.Types.Descr do {optional?, type} = pop_optional_static(type) cond do - not optional? -> {key, to_quoted(type)} + not optional? -> {key, to_quoted(type, opts)} empty?(type) -> {key, {:not_set, [], []}} - true -> {key, {:if_set, [], [to_quoted(type)]}} + true -> {key, {:if_set, [], [to_quoted(type, opts)]}} end end end @@ -1969,11 +2002,11 @@ defmodule Module.Types.Descr do # This is a cheap optimization that relies on structural equality. defp tuple_union(left, right), do: left ++ (right -- left) - defp tuple_to_quoted(dnf) do + defp tuple_to_quoted(dnf, opts) do dnf |> tuple_simplify() |> tuple_fusion() - |> Enum.map(&tuple_each_to_quoted/1) + |> Enum.map(&tuple_each_to_quoted(&1, opts)) end # Given a dnf of tuples, fuses the tuple unions when possible, @@ -2020,27 +2053,27 @@ defmodule Module.Types.Descr do {tag, fused_elements, []} end - defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}) do + defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}, opts) do case negative_tuples do [] -> - tuple_literal_to_quoted({tag, positive_tuple}) + tuple_literal_to_quoted({tag, positive_tuple}, opts) _ -> negative_tuples - |> Enum.map(&tuple_literal_to_quoted/1) + |> Enum.map(&tuple_literal_to_quoted(&1, opts)) |> Enum.reduce(&{:or, [], [&2, &1]}) |> Kernel.then( - &{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}), {:not, [], [&1]}]} + &{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}, opts), {:not, [], [&1]}]} ) end end - defp tuple_literal_to_quoted({:closed, []}), do: {:{}, [], []} + defp tuple_literal_to_quoted({:closed, []}, _opts), do: {:{}, [], []} - defp tuple_literal_to_quoted({tag, elements}) do + defp tuple_literal_to_quoted({tag, elements}, opts) do case tag do - :closed -> {:{}, [], Enum.map(elements, &to_quoted/1)} - :open -> {:{}, [], Enum.map(elements, &to_quoted/1) ++ [{:..., [], nil}]} + :closed -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts))} + :open -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts)) ++ [{:..., [], nil}]} end end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index fa1774adea4..d43b663b7d3 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -141,7 +141,7 @@ defmodule Module.Types.Helpers do column: meta[:column], hints: formatter_hints ++ expr_hints(expr), formatted_expr: formatted_expr, - formatted_type: Module.Types.Descr.to_quoted_string(type) + formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true) } end) |> Enum.sort_by(&{&1.line, &1.column}) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index d04b53151fb..a5bdff26995 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -495,13 +495,11 @@ defmodule Module.Types.Of do unknown key .#{key} in expression: #{expr_to_string(expr) |> indent(4)} - """, - empty_if(dot_var?(expr), """ the given type does not have the given key: #{to_quoted_string(type) |> indent(4)} - """), + """, format_traces(traces) ]) } diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 6c4f4b62365..500393a4d16 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -798,9 +798,9 @@ defmodule Module.Types.ExprTest do x.foo_bar - where "x" was given the type: + the given type does not have the given key: - # type: dynamic(%URI{ + dynamic(%URI{ authority: term(), fragment: term(), host: term(), @@ -810,6 +810,10 @@ defmodule Module.Types.ExprTest do scheme: term(), userinfo: term() }) + + where "x" was given the type: + + # type: dynamic(%URI{}) # from: types_test.ex:LINE-4:43 x = %URI{} """ @@ -903,7 +907,7 @@ defmodule Module.Types.ExprTest do given types: - dynamic(:foo) <= dynamic(%Point{x: term(), y: term(), z: term()}) + dynamic(:foo) <= dynamic(%Point{}) where "mod" was given the type: @@ -919,7 +923,7 @@ defmodule Module.Types.ExprTest do where "y" was given the type: - # type: dynamic(%Point{x: term(), y: term(), z: term()}) + # type: dynamic(%Point{}) # from: types_test.ex:LINE-2 y = %Point{} diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 490c725a4b8..b6e789e5422 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -166,9 +166,13 @@ defmodule Module.Types.PatternTest do x.foo_bar + the given type does not have the given key: + + dynamic(%Point{x: term(), y: term(), z: term()}) + where "x" was given the type: - # type: dynamic(%Point{x: term(), y: term(), z: term()}) + # type: dynamic(%Point{}) # from: types_test.ex:LINE-1 x = %Point{} """ From 9df42f5a0a37926a2de98ed79bbcfa6cd81c6fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Dec 2024 17:26:59 +0100 Subject: [PATCH 038/128] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 153cef2eeb9..b469bc2753a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -293,6 +293,7 @@ You may also prefer to write using guards: #### IEx + * [IEx] Add `IEx.configure(auto_reload: true)` to automatically pick up modules recompiled from other operating system processes * [IEx] Add `:dot_iex` support to `IEx.configure/1` * [IEx] Add report for normal/shutdown exits in IEx @@ -366,6 +367,8 @@ You may also prefer to write using guards: #### Mix * [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP` + * [mix compile] `:warnings_as_errors` configuration in `:elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix compile`. Alternatively, you might alias the task: `aliases: [compile: "compile --warnings-as-errors"]` + * [mix test] `:warnings_as_errors` configuration in `:test_elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix test`. Alternatively, you might alias the task: `aliases: [test: "test --warnings-as-errors"]` * [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0` ## v1.17 From 3564c68cdced43fa615acfef9057da447ee15476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Dec 2024 18:30:20 +0100 Subject: [PATCH 039/128] Release v1.18.0 --- CHANGELOG.md | 31 ++++--------------------------- VERSION | 2 +- bin/elixir | 2 +- bin/elixir.bat | 2 +- bin/elixir.ps1 | 2 +- 5 files changed, 8 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b469bc2753a..e8ccdce5a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -225,33 +225,7 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z -## v1.18.0-rc.1 - -### 1. Enhancements - -#### Elixir - - * [JSON] Implement `JSON.Encoder` for all Calendar types - -### 2. Bug fixes - -#### Elixir - - * [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation - -### 3. Regressions - -#### Elixir - - * [Kernel] Fix type warnings in `pop_in/1` - * [Kernel] Fix false positive warnings when accessing exceptions from `rescue` - * [Kernel] Emit warnings for duplicate patterns instead of errors - -#### ExUnit - - * [ExUnit] Do not warn on user-supplied CaseTemplate options - -## v1.18.0-rc.0 (2024-12-10) +## v1.18.0 (2024-12-19) ### 1. Enhancements @@ -266,6 +240,8 @@ You may also prefer to write using guards: * [Config] Add `Config.read_config/1` * [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2` * [Exception] Add `MissingApplicationsError` exception to denote missing applications + * [JSON] Add a new `JSON` module with encoding and decoding functionality + * [JSON] Implement `JSON.Encoder` for all Calendar types * [Kernel] Update source code parsing to match [UTS #55](https://www.unicode.org/reports/tr55/) latest recommendations. In particular, mixed script is allowed in identifiers as long as they are separate by underscores (`_`), such as `http_сервер`. Previously allowed highly restrictive identifiers, which mixed Latin and other scripts, such as the japanese word for t-shirt, `Tシャツ`, now require the underscore as well * [Kernel] Warn on bidirectional confusability in identifiers * [Kernel] Verify the type of the binary generators @@ -317,6 +293,7 @@ You may also prefer to write using guards: * [Code.Fragment] Properly handle keyword keys as their own entry * [Inspect.Algebra] Ensure `next_break_fits` respects `line_length` * [Kernel] Validate AST on `unquote` and `unquote_splicing` to provide better error reports instead of failing too late inside the compiler + * [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation * [Module] Include module attribute line and name when tracing its aliases * [Stream] Do not halt streams twice in `Stream.transform/5` * [URI] Fix a bug when a schemaless URI is given to `URI.merge/2` diff --git a/VERSION b/VERSION index ca117a054fb..84cc529467b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.0-rc.0 +1.18.0 diff --git a/bin/elixir b/bin/elixir index 23c07afe136..c937bea182c 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.18.0-rc.0 +ELIXIR_VERSION=1.18.0 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index d4c66c8ab6e..09f3050ff4a 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @echo off -set ELIXIR_VERSION=1.18.0-rc.0 +set ELIXIR_VERSION=1.18.0 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 index b9b7e7f5a0d..4ef9cd0e1b7 100755 --- a/bin/elixir.ps1 +++ b/bin/elixir.ps1 @@ -1,6 +1,6 @@ #!/usr/bin/env pwsh -$ELIXIR_VERSION = "1.18.0-rc.0" +$ELIXIR_VERSION = "1.18.0" $scriptPath = Split-Path -Parent $PSCommandPath $erlExec = "erl" From 5d65d60721ea42628c2c0e9fb6fa377dab40e389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Dec 2024 20:34:17 +0100 Subject: [PATCH 040/128] Comment out canonical and update release notes --- Makefile | 2 +- RELEASE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bfda4058745..8d0270a5757 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PREFIX ?= /usr/local TEST_FILES ?= "*_test.exs" SHARE_PREFIX ?= $(PREFIX)/share MAN_PREFIX ?= $(SHARE_PREFIX)/man -CANONICAL := v1.18/ +# CANONICAL := main/ ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ERLC := erlc -I lib/elixir/include ERL_MAKE := erl -make diff --git a/RELEASE.md b/RELEASE.md index dc769aec9f7..83aa5ffd958 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -30,7 +30,7 @@ ### Back in main -1. Bump /VERSION file, bin/elixir and bin/elixir.bat +1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1 2. Start new /CHANGELOG.md From e5033c94ce93649fbdbf0002d5084e317a360772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 20 Dec 2024 10:43:14 +0100 Subject: [PATCH 041/128] Keep traces backwards compatible --- lib/elixir/lib/module/types/helpers.ex | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index d43b663b7d3..a76260e97c9 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -85,6 +85,9 @@ defmodule Module.Types.Helpers do @doc """ Collect traces from variables in expression. + + This information is exposed to language servers and + therefore must remain backwards compatible. """ def collect_traces(expr, %{vars: vars}) do {_, versions} = @@ -135,16 +138,17 @@ defmodule Module.Types.Helpers do formatter -> formatter.(expr) end + # This information is exposed to language servers and + # therefore must remain backwards compatible. %{ file: file, - line: meta[:line], - column: meta[:column], - hints: formatter_hints ++ expr_hints(expr), + meta: meta, formatted_expr: formatted_expr, + formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)), formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true) } end) - |> Enum.sort_by(&{&1.line, &1.column}) + |> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]}) |> Enum.dedup() end @@ -161,7 +165,7 @@ defmodule Module.Types.Helpers do location = trace.file |> Path.relative_to_cwd() - |> Exception.format_file_line(trace.line, trace.column) + |> Exception.format_file_line(trace.meta[:line], trace.meta[:column]) |> String.replace_suffix(":", "") [ @@ -173,7 +177,7 @@ defmodule Module.Types.Helpers do """, indent(trace.formatted_expr, 4), ?\n, - format_hints(trace.hints) + trace.formatted_hints ] end From e4c86d4b5f91fe26f9dc255cb8cc1afcb86493f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Fri, 20 Dec 2024 11:29:29 +0100 Subject: [PATCH 042/128] Hash release files after signing (#14085) --- .github/workflows/release.yml | 47 ++++++++++++------- .../workflows/release_pre_built/action.yml | 6 --- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23557c18b44..3d769bf1699 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,6 @@ jobs: git push origin $ref_name --force build: - needs: create_draft_release strategy: fail-fast: true matrix: @@ -80,6 +79,22 @@ jobs: otp: ${{ matrix.otp }} build_docs: ${{ matrix.build_docs }} + - name: "Sign files with Trusted Signing" + if: github.repository == 'elixir-lang/elixir' + uses: azure/trusted-signing-action@v0.5.0 + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: https://eus.codesigning.azure.net/ + trusted-signing-account-name: trusted-signing-elixir + certificate-profile-name: Elixir + files-folder: ${{ github.workspace }} + files-folder-filter: exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + - name: "Attest release .exe provenance" uses: actions/attest-build-provenance@v2 id: attest-exe-provenance @@ -112,6 +127,18 @@ jobs: env: ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}" + - name: Create Release Hashes + run: | + shasum -a 1 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha1sum + shasum -a 256 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha256sum + shasum -a 1 elixir-otp-${{ matrix.otp }}.exe > elixir-otp-${{ matrix.otp }}.exe.sha1sum + shasum -a 256 elixir-otp-${{ matrix.otp }}.exe > elixir-otp-${{ matrix.otp }}.exe.sha256sum + - name: Create Docs Hashes + if: ${{ matrix.build_docs }} + run: | + shasum -a 1 Docs.zip > Docs.zip.sha1sum + shasum -a 256 Docs.zip > Docs.zip.sha256sum + - name: "Upload release artifacts" uses: actions/upload-artifact@v4 with: @@ -126,7 +153,7 @@ jobs: path: Docs.zip* upload-release: - needs: build + needs: [build, create_draft_release] runs-on: windows-2022 steps: @@ -137,22 +164,6 @@ jobs: mv Docs/* . shell: bash - - name: "Sign files with Trusted Signing" - if: github.repository == 'elixir-lang/elixir' - uses: azure/trusted-signing-action@v0.5.0 - with: - azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} - azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} - azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} - endpoint: https://eus.codesigning.azure.net/ - trusted-signing-account-name: trusted-signing-elixir - certificate-profile-name: Elixir - files-folder: ${{ github.workspace }} - files-folder-filter: exe - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - - name: Upload Pre-built shell: bash env: diff --git a/.github/workflows/release_pre_built/action.yml b/.github/workflows/release_pre_built/action.yml index c7a12e0d1bf..3d6efd332af 100644 --- a/.github/workflows/release_pre_built/action.yml +++ b/.github/workflows/release_pre_built/action.yml @@ -19,8 +19,6 @@ runs: run: | make Precompiled.zip mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip - shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum - shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum echo "$PWD/bin" >> $GITHUB_PATH - name: Install NSIS shell: bash @@ -34,8 +32,6 @@ runs: export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip (cd lib/elixir/scripts/windows_installer && ./build.sh) mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe . - shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum - shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum - name: Get ExDoc ref if: ${{ inputs.build_docs }} shell: bash @@ -66,5 +62,3 @@ runs: run: | git fetch --tags make Docs.zip - shasum -a 1 Docs.zip > Docs.zip.sha1sum - shasum -a 256 Docs.zip > Docs.zip.sha256sum From 5aa049ccb38e943a63a16c1103d60947325637e3 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 20 Dec 2024 22:27:11 +0900 Subject: [PATCH 043/128] Remove no_parens when using capture with arity (#14090) --- lib/elixir/src/elixir_fn.erl | 4 ++++ lib/elixir/test/elixir/kernel/expansion_test.exs | 5 +++++ lib/elixir/test/elixir/kernel/fn_test.exs | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/lib/elixir/src/elixir_fn.erl b/lib/elixir/src/elixir_fn.erl index 668b201909d..fafdbd8207a 100644 --- a/lib/elixir/src/elixir_fn.erl +++ b/lib/elixir/src/elixir_fn.erl @@ -112,6 +112,10 @@ capture_expr(Meta, Expr, S, E, Escaped, Sequential) -> case escape(Expr, E, Escaped) of {_, []} when not Sequential -> invalid_capture(Meta, Expr, E); + {{{'.', _, [_, _]} = Dot, _, Args}, []} -> + Meta2 = lists:keydelete(no_parens, 1, Meta), + Fn = {fn, Meta2, [{'->', Meta2, [[], {Dot, Meta2, Args}]}]}, + {expand, Fn, S, E}; {EExpr, EDict} -> EVars = validate(Meta, EDict, 1, E), Fn = {fn, Meta, [{'->', Meta, [EVars, EExpr]}]}, diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 81b0a65a70b..4e08384a5b0 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -1203,6 +1203,11 @@ defmodule Kernel.ExpansionTest do [{:->, [{:line, 1}], [[{:capture, [line: 1], nil}], {:capture, [line: 1], nil}]}]} end + test "removes no_parens when expanding 0-arity capture to fn" do + assert expand(quote(do: &foo().bar/0)) == + quote(do: fn -> foo().bar() end) + end + test "expands remotes" do assert expand(quote(do: &List.flatten/2)) == quote(do: &:"Elixir.List".flatten/2) diff --git a/lib/elixir/test/elixir/kernel/fn_test.exs b/lib/elixir/test/elixir/kernel/fn_test.exs index 1304d753577..0d0e8471994 100644 --- a/lib/elixir/test/elixir/kernel/fn_test.exs +++ b/lib/elixir/test/elixir/kernel/fn_test.exs @@ -95,6 +95,12 @@ defmodule Kernel.FnTest do assert (&mod.flatten/1) == (&List.flatten/1) end + test "capture with module from local call" do + assert (&math_mod().pi/0).() == :math.pi() + end + + defp math_mod, do: :math + test "local partial application" do assert (&atb(&1, :utf8)).(:a) == "a" assert (&atb(List.to_atom(&1), :utf8)).(~c"a") == "a" From 0fe6f68c3413e7932570fa0e226ae1f0d023c7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Sat, 21 Dec 2024 12:22:38 +0100 Subject: [PATCH 044/128] Fix mix escript.build when protocol consolidation is disabled (#14098) --- lib/mix/lib/mix/tasks/escript.build.ex | 2 +- lib/mix/test/mix/tasks/escript_test.exs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/escript.build.ex b/lib/mix/lib/mix/tasks/escript.build.ex index 36fce7e6759..7a3cebfac0a 100644 --- a/lib/mix/lib/mix/tasks/escript.build.ex +++ b/lib/mix/lib/mix/tasks/escript.build.ex @@ -342,7 +342,7 @@ defmodule Mix.Tasks.Escript.Build do {zip_path, consolidated[Path.basename(path)] || path} end else - [] + files end end diff --git a/lib/mix/test/mix/tasks/escript_test.exs b/lib/mix/test/mix/tasks/escript_test.exs index 5b284e9f08f..4e25a9b0b49 100644 --- a/lib/mix/test/mix/tasks/escript_test.exs +++ b/lib/mix/test/mix/tasks/escript_test.exs @@ -50,6 +50,20 @@ defmodule Mix.Tasks.EscriptTest do end) end + test "generate escript without protocol consolidation" do + in_fixture("escript_test", fn -> + push_project_with_config(Escript, consolidate_protocols: false) + + Mix.Tasks.Escript.Build.run([]) + assert_received {:mix_shell, :info, ["Generated escript escript_test with MIX_ENV=dev"]} + assert System.cmd("escript", ["escript_test"]) == {"TEST\n", 0} + assert count_abstract_code("escript_test") == 0 + + # Does not consolidate protocols + assert System.cmd("escript", ["escript_test", "--protocol", "Enumerable"]) == {"false\n", 0} + end) + end + test "generate escript with --no-compile option" do in_fixture("escript_test", fn -> push_project_with_config(Escript) From 4a5fb1eb6645dc46a95ecda4b0151e75957c78c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 21 Dec 2024 18:24:32 +0100 Subject: [PATCH 045/128] Use binwrite only on Windows, closes #14101 --- lib/mix/lib/mix/shell/io.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/shell/io.ex b/lib/mix/lib/mix/shell/io.ex index 44d1e11e312..ca558030a66 100644 --- a/lib/mix/lib/mix/shell/io.ex +++ b/lib/mix/lib/mix/shell/io.ex @@ -100,12 +100,12 @@ defmodule Mix.Shell.IO do """ def cmd(command, opts \\ []) do print_app? = Keyword.get(opts, :print_app, true) + windows? = match?({:win32, _}, :os.type()) Mix.Shell.cmd(command, opts, fn data -> if print_app?, do: print_app() - # Due to encoding of shell command on Windows, - # let's write the data as is - IO.binwrite(data) + # Due to encoding of shell command on Windows, write the data as is. + if windows?, do: IO.binwrite(data), else: IO.write(data) end) end end From 4e3203f19a2072ad9d3dd48b5abc7d32307695f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Sat, 21 Dec 2024 16:17:38 +0100 Subject: [PATCH 046/128] Fix Release Signing (#14099) (#14100) --- .github/workflows/release.yml | 138 +++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d769bf1699..e0f7f1947f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,42 +79,6 @@ jobs: otp: ${{ matrix.otp }} build_docs: ${{ matrix.build_docs }} - - name: "Sign files with Trusted Signing" - if: github.repository == 'elixir-lang/elixir' - uses: azure/trusted-signing-action@v0.5.0 - with: - azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} - azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} - azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} - endpoint: https://eus.codesigning.azure.net/ - trusted-signing-account-name: trusted-signing-elixir - certificate-profile-name: Elixir - files-folder: ${{ github.workspace }} - files-folder-filter: exe - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - - - name: "Attest release .exe provenance" - uses: actions/attest-build-provenance@v2 - id: attest-exe-provenance - with: - subject-path: "elixir-otp-${{ matrix.otp }}.exe" - - name: "Copy release .exe provenance" - run: cp "$ATTESTATION" elixir-otp-${{ matrix.otp }}.exe.sigstore - env: - ATTESTATION: "${{ steps.attest-exe-provenance.outputs.bundle-path }}" - - - name: "Attest release .zip provenance" - uses: actions/attest-build-provenance@v2 - id: attest-zip-provenance - with: - subject-path: "elixir-otp-${{ matrix.otp }}.zip" - - name: "Copy release .zip provenance" - run: cp "$ATTESTATION" elixir-otp-${{ matrix.otp }}.zip.sigstore - env: - ATTESTATION: "${{ steps.attest-zip-provenance.outputs.bundle-path }}" - - name: "Attest docs provenance" uses: actions/attest-build-provenance@v2 id: attest-docs-provenance @@ -127,23 +91,23 @@ jobs: env: ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}" - - name: Create Release Hashes - run: | - shasum -a 1 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha1sum - shasum -a 256 elixir-otp-${{ matrix.otp }}.zip > elixir-otp-${{ matrix.otp }}.zip.sha256sum - shasum -a 1 elixir-otp-${{ matrix.otp }}.exe > elixir-otp-${{ matrix.otp }}.exe.sha1sum - shasum -a 256 elixir-otp-${{ matrix.otp }}.exe > elixir-otp-${{ matrix.otp }}.exe.sha256sum - name: Create Docs Hashes if: ${{ matrix.build_docs }} run: | shasum -a 1 Docs.zip > Docs.zip.sha1sum shasum -a 256 Docs.zip > Docs.zip.sha256sum - - name: "Upload release artifacts" + - name: "Upload linux release artifacts" + uses: actions/upload-artifact@v4 + with: + name: build-linux-elixir-otp-${{ matrix.otp }} + path: elixir-otp-${{ matrix.otp }}.zip + + - name: "Upload windows release artifacts" uses: actions/upload-artifact@v4 with: - name: elixir-otp-${{ matrix.otp }} - path: elixir-otp-${{ matrix.otp }}* + name: build-windows-elixir-otp-${{ matrix.otp }} + path: elixir-otp-${{ matrix.otp }}.exe - name: "Upload doc artifacts" uses: actions/upload-artifact@v4 @@ -151,18 +115,84 @@ jobs: with: name: Docs path: Docs.zip* + + sign: + needs: [build] + strategy: + fail-fast: true + matrix: + otp: [26, 27] + flavor: [windows, linux] - upload-release: - needs: [build, create_draft_release] - runs-on: windows-2022 + env: + RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }} + + runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }} steps: - uses: actions/download-artifact@v4 + with: + name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }} - - run: | - mv elixir-otp-*/* . - mv Docs/* . + - name: "Sign files with Trusted Signing" + if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows' + uses: azure/trusted-signing-action@v0.5.0 + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: https://eus.codesigning.azure.net/ + trusted-signing-account-name: trusted-signing-elixir + certificate-profile-name: Elixir + files-folder: ${{ github.workspace }} + files-folder-filter: exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: "Attest release provenance" + uses: actions/attest-build-provenance@v2 + id: attest-provenance + with: + subject-path: ${{ env.RELEASE_FILE }} + - name: "Copy release .zip provenance" shell: bash + run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore" + env: + ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}" + + - name: Create Release Hashes + if: matrix.flavor == 'windows' + shell: pwsh + run: | + $sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1 + $sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum" + + $sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256 + $sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum" + + - name: Create Release Hashes + if: matrix.flavor == 'linux' + shell: bash + run: | + shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum" + shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum" + + - name: "Upload linux release artifacts" + uses: actions/upload-artifact@v4 + with: + name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }} + path: ${{ env.RELEASE_FILE }}* + + upload-release: + needs: [create_draft_release, build, sign] + runs-on: ubuntu-22.04 + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: '{sign-*-elixir-otp-*,Docs}' + merge-multiple: true - name: Upload Pre-built shell: bash @@ -190,7 +220,7 @@ jobs: Docs.zip.sigstore upload-builds-hex-pm: - needs: build + needs: [build, sign] runs-on: ubuntu-22.04 concurrency: builds-hex-pm env: @@ -204,6 +234,9 @@ jobs: OTP_GENERIC_VERSION: "25" steps: - uses: actions/download-artifact@v4 + with: + pattern: '{sign-*-elixir-otp-*,Docs}' + merge-multiple: true - name: Init purge keys file run: | @@ -213,7 +246,6 @@ jobs: run: | ref_name=${{ github.ref_name }} - mv elixir-otp-*/* . for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do dest=${zip/elixir/${ref_name}} surrogate_key=${dest/.zip$/} From d4e6a558cbc5101bef317cd6bf38a8937b723259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Sat, 21 Dec 2024 17:59:53 +0100 Subject: [PATCH 047/128] Fix Release Signing Again (#14099) (#14102) This time, fix the Docs publishing. --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0f7f1947f4..9a28d67400d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -264,7 +264,6 @@ jobs: done - name: Upload Docs to S3 - working-directory: Docs run: | version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g') From a1be4fbc8623e56a75b599543dfb0e79e410fb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 21 Dec 2024 21:29:04 +0100 Subject: [PATCH 048/128] Verify no warnings are emitted on generated clauses, see #14094 --- .../elixir/module/types/integration_test.exs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 3208cd2cbd0..c97dc32df6b 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -195,6 +195,31 @@ defmodule Module.Types.IntegrationTest do assert_warnings(files, warnings) end + + test "unused generated overridable private clauses" do + files = %{ + "a.ex" => """ + defmodule A do + use B + def public(x), do: private(x) + defp private(x), do: super(List.to_tuple(x)) + end + """, + "b.ex" => """ + defmodule B do + defmacro __using__(_) do + quote generated: true do + defp private({:ok, ok}), do: ok + defp private(:error), do: :error + defoverridable private: 1 + end + end + end + """ + } + + assert_no_warnings(files) + end end describe "undefined warnings" do From 4adaac702e65dd638f7585c30579298270778495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 22 Dec 2024 09:54:57 +0100 Subject: [PATCH 049/128] Do not validate clauses of overridable functions, closes #14094 --- lib/elixir/lib/module/types.ex | 7 +++-- lib/elixir/src/elixir_overridable.erl | 3 ++- .../elixir/module/types/integration_test.exs | 27 +++++++++++++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index efead6b04ba..aa704982096 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -182,9 +182,12 @@ defmodule Module.Types do end defp warn_unused_clauses(defs, stack, context) do - for {fun_arity, pending} <- context.local_used, pending != [], reduce: context do + for {fun_arity, pending} <- context.local_used, + pending != [], + {_fun_arity, kind, meta, clauses} = List.keyfind(defs, fun_arity, 0), + not Keyword.get(meta, :from_super, false), + reduce: context do context -> - {_fun_arity, kind, _meta, clauses} = List.keyfind(defs, fun_arity, 0) {_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity) clauses_indexes = diff --git a/lib/elixir/src/elixir_overridable.erl b/lib/elixir/src/elixir_overridable.erl index c08b7ec37a2..6defa05f8b6 100644 --- a/lib/elixir/src/elixir_overridable.erl +++ b/lib/elixir/src/elixir_overridable.erl @@ -75,8 +75,9 @@ store_not_overridden(Module) -> %% Private store(Set, Module, Tuple, {_, Count, Def, Overridden}, Hidden) -> - {{{def, {Name, Arity}}, Kind, Meta, File, _Check, + {{{def, {Name, Arity}}, Kind, BaseMeta, File, _Check, {Defaults, _HasBody, _LastDefaults}}, Clauses} = Def, + Meta = [{from_super, Hidden} | BaseMeta], {FinalKind, FinalName, FinalArity, FinalClauses} = case Hidden of diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index c97dc32df6b..31affb838d4 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -196,7 +196,30 @@ defmodule Module.Types.IntegrationTest do assert_warnings(files, warnings) end - test "unused generated overridable private clauses" do + test "unused generated private clauses" do + files = %{ + "a.ex" => """ + defmodule A do + use B + def public(x), do: private(List.to_tuple(x)) + end + """, + "b.ex" => """ + defmodule B do + defmacro __using__(_) do + quote generated: true do + defp private({:ok, ok}), do: ok + defp private(:error), do: :error + end + end + end + """ + } + + assert_no_warnings(files) + end + + test "unused overridable private clauses" do files = %{ "a.ex" => """ defmodule A do @@ -208,7 +231,7 @@ defmodule Module.Types.IntegrationTest do "b.ex" => """ defmodule B do defmacro __using__(_) do - quote generated: true do + quote do defp private({:ok, ok}), do: ok defp private(:error), do: :error defoverridable private: 1 From 1a2be16109f856120fe2a4c7c8120d6a01a7447b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 22 Dec 2024 11:02:14 +0100 Subject: [PATCH 050/128] Track source for warnings, closes #14093 --- lib/elixir/lib/module/parallel_checker.ex | 12 ++-- lib/elixir/src/elixir_errors.erl | 5 +- .../elixir/module/types/integration_test.exs | 64 ++++++++++++++++--- .../test/mix/tasks/compile.elixir_test.exs | 7 +- 4 files changed, 70 insertions(+), 18 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 42a0d61fadc..5df93d783bf 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -239,7 +239,7 @@ defmodule Module.ParallelChecker do |> Module.Types.warnings(file, definitions, no_warn_undefined, cache) |> Kernel.++(behaviour_warnings) |> group_warnings() - |> emit_warnings(log?) + |> emit_warnings(file, log?) Enum.each(after_verify, fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) @@ -320,9 +320,9 @@ defmodule Module.ParallelChecker do Enum.sort(ungrouped ++ grouped) end - defp emit_warnings(warnings, log?) do + defp emit_warnings(warnings, file, log?) do Enum.flat_map(warnings, fn {locations, diagnostic} -> - diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1)) + diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1)) log? and print_diagnostics(diagnostics) diagnostics end) @@ -336,10 +336,12 @@ defmodule Module.ParallelChecker do :elixir_errors.print_diagnostics(diagnostics) end - defp to_diagnostic(diagnostic, {file, position, mfa}) when is_list(position) do + defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do + file = Path.absname(file) + %{ severity: :warning, - source: file, + source: source, file: file, position: position_to_tuple(position), stacktrace: [to_stacktrace(file, position, mfa)], diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 0f47189050e..c8943cbe6af 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -92,7 +92,10 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) -> Diagnostic = #{ severity => Severity, - source => File, + source => case get(elixir_compiler_file) of + undefined -> File; + CompilerFile -> CompilerFile + end, file => File, position => Position, message => unicode:characters_to_binary(Message), diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 31affb838d4..943c18f7d8c 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -36,7 +36,7 @@ defmodule Module.Types.IntegrationTest do """ } - modules = compile(files) + modules = compile_modules(files) assert [ {{:c, 0}, %{}}, @@ -243,6 +243,37 @@ defmodule Module.Types.IntegrationTest do assert_no_warnings(files) end + + test "returns diagnostics with source and file" do + files = %{ + "a.ex" => """ + defmodule A do + @file "generated.ex" + def fun(arg) do + :ok = List.to_tuple(arg) + end + end + """ + } + + {_modules, warnings} = with_compile_warnings(files) + + assert [ + %{ + message: "the following pattern will never match" <> _, + file: file, + source: source + } + ] = warnings.runtime_warnings + + assert String.ends_with?(source, "a.ex") + assert Path.type(source) == :absolute + assert String.ends_with?(file, "generated.ex") + assert Path.type(file) == :absolute + after + :code.delete(A) + :code.purge(A) + end end describe "undefined warnings" do @@ -980,26 +1011,39 @@ defmodule Module.Types.IntegrationTest do defp capture_compile_warnings(files) do in_tmp(fn -> paths = generate_files(files) - capture_io(:stderr, fn -> compile_files(paths) end) + capture_io(:stderr, fn -> compile_to_path(paths) end) + end) + end + + defp with_compile_warnings(files) do + in_tmp(fn -> + paths = generate_files(files) + with_io(:stderr, fn -> compile_to_path(paths) end) |> elem(0) end) end - defp compile(files) do + defp compile_modules(files) do in_tmp(fn -> paths = generate_files(files) - compile_files(paths) + {modules, _warnings} = compile_to_path(paths) + + Map.new(modules, fn module -> + {^module, binary, _filename} = :code.get_object_code(module) + {module, binary} + end) end) end - defp compile_files(paths) do - {:ok, modules, _warnings} = Kernel.ParallelCompiler.compile_to_path(paths, ".") + defp compile_to_path(paths) do + {:ok, modules, warnings} = + Kernel.ParallelCompiler.compile_to_path(paths, ".", return_diagnostics: true) - Map.new(modules, fn module -> - {^module, binary, _filename} = :code.get_object_code(module) + for module <- modules do :code.delete(module) :code.purge(module) - {module, binary} - end) + end + + {modules, warnings} end defp generate_files(files) do diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 5d31f6e224b..8b130b74f02 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1045,12 +1045,15 @@ defmodule Mix.Tasks.Compile.ElixirTest do # Compiles with missing external resources file = Path.absname("lib/a.eex") + source = Path.absname("lib/a.ex") assert capture_io(:stderr, fn -> - assert {:ok, [%Mix.Task.Compiler.Diagnostic{file: ^file, position: 13}]} = + assert {:ok, + [%Mix.Task.Compiler.Diagnostic{source: ^source, file: ^file, position: 13}]} = Mix.Tasks.Compile.Elixir.run([]) - assert {:noop, [%Mix.Task.Compiler.Diagnostic{file: ^file, position: 13}]} = + assert {:noop, + [%Mix.Task.Compiler.Diagnostic{source: ^source, file: ^file, position: 13}]} = Mix.Tasks.Compile.Elixir.run(["--all-warnings"]) end) =~ "oops" From 8c9f303e370607950fdee8039acddb024ffdd448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 22 Dec 2024 17:56:03 +0100 Subject: [PATCH 051/128] Avoid crash when typing violation is detected on dynamic dispatch Closes #14105. --- lib/elixir/lib/module/types/apply.ex | 106 ++++++++++++------ .../test/elixir/module/types/expr_test.exs | 39 +++++++ 2 files changed, 112 insertions(+), 33 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 49f98295262..2c315b88cb9 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -269,7 +269,7 @@ defmodule Module.Types.Apply do case signature(name, arity) do :none -> {dynamic(), context} - info -> apply_remote(info, args_types, expr, stack, context) + info -> apply_remote(nil, name, info, args_types, expr, stack, context) end end @@ -287,10 +287,14 @@ defmodule Module.Types.Apply do {value_type, context} :badtuple -> - {error_type(), badremote_error(expr, [integer(), tuple], stack, context)} + {error_type(), + badremote_error(:erlang, :element, expr, [integer(), tuple], stack, context)} - reason -> - {error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)} + :badindex -> + mfac = mfac(expr, :erlang, :element, 2) + + {error_type(), + error({:badindex, mfac, expr, tuple, index - 1, context}, meta, stack, context)} end end @@ -308,10 +312,16 @@ defmodule Module.Types.Apply do {value_type, context} :badtuple -> - {error_type(), badremote_error(expr, [integer(), tuple, value], stack, context)} + args_types = [integer(), tuple, value] + + {error_type(), + badremote_error(:erlang, :insert_element, expr, args_types, stack, context)} - reason -> - {error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)} + :badindex -> + mfac = mfac(expr, :erlang, :insert_element, 3) + + {error_type(), + error({:badindex, mfac, expr, tuple, index - 2, context}, meta, stack, context)} end end @@ -322,10 +332,16 @@ defmodule Module.Types.Apply do {value_type, context} :badtuple -> - {error_type(), badremote_error(expr, [integer(), tuple], stack, context)} + args_types = [integer(), tuple] + + {error_type(), + badremote_error(:erlang, :delete_element, expr, args_types, stack, context)} - reason -> - {error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)} + :badindex -> + mfac = mfac(expr, :erlang, :delete_element, 2) + + {error_type(), + error({:badindex, mfac, expr, tuple, index - 1, context}, meta, stack, context)} end end @@ -340,7 +356,7 @@ defmodule Module.Types.Apply do {value_type, context} :badnonemptylist -> - {error_type(), badremote_error(expr, [list], stack, context)} + {error_type(), badremote_error(:erlang, :hd, expr, [list], stack, context)} end end @@ -350,7 +366,7 @@ defmodule Module.Types.Apply do {value_type, context} :badnonemptylist -> - {error_type(), badremote_error(expr, [list], stack, context)} + {error_type(), badremote_error(:erlang, :tl, expr, [list], stack, context)} end end @@ -405,30 +421,36 @@ defmodule Module.Types.Apply do {return(boolean(), args_types, stack), context} end - def remote(mod, name, args_types, expr, stack, context) do + def remote(mod, fun, args_types, expr, stack, context) do arity = length(args_types) - case :elixir_rewrite.inline(mod, name, arity) do - {mod, name} -> - remote(mod, name, args_types, expr, stack, context) + case :elixir_rewrite.inline(mod, fun, arity) do + {new_mod, new_fun} -> + expr = inline_meta(expr, mod, fun) + remote(new_mod, new_fun, args_types, expr, stack, context) false -> - {info, context} = signature(mod, name, arity, elem(expr, 1), stack, context) - apply_remote(info, args_types, expr, stack, context) + {info, context} = signature(mod, fun, arity, elem(expr, 1), stack, context) + apply_remote(mod, fun, info, args_types, expr, stack, context) end end - defp apply_remote(info, args_types, expr, stack, context) do + defp apply_remote(mod, fun, info, args_types, expr, stack, context) do case apply_signature(info, args_types, stack) do {:ok, _indexes, type} -> {type, context} {:error, domain, clauses} -> - error = {:badremote, expr, args_types, domain, clauses, context} + mfac = mfac(expr, mod, fun, length(args_types)) + error = {:badremote, mfac, expr, args_types, domain, clauses, context} {error_type(), error(error, elem(expr, 1), stack, context)} end end + defp inline_meta({node, meta, args}, mod, fun) do + {node, [inline: {mod, fun}] ++ meta, args} + end + @doc """ Returns the type of a remote capture. """ @@ -718,22 +740,28 @@ defmodule Module.Types.Apply do error(__MODULE__, warning, meta, stack, context) end - defp badremote_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do - {_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, length(args_types)) - error({:badremote, expr, args_types, domain || args, clauses, context}, meta, stack, context) + defp badremote_error(mod, fun, {_, meta, _} = expr, args_types, stack, context) do + arity = length(args_types) + mfac = mfac(expr, mod, fun, arity) + {_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, arity) + domain = domain || args + tuple = {:badremote, mfac, expr, args_types, domain, clauses, context} + error(tuple, meta, stack, context) end - ## Diagnosstics + ## Diagnostics - def format_diagnostic({:badindex, expr, type, index, context}) do + def format_diagnostic({:badindex, mfac, expr, type, index, context}) do traces = collect_traces(expr, context) + {mod, fun, arity, _converter} = mfac + mfa = Exception.format_mfa(mod, fun, arity) %{ details: %{typing_traces: traces}, message: IO.iodata_to_binary([ """ - expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{format_mfa(expr)}: + expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{mfa}: #{expr_to_string(expr) |> indent(4)} @@ -778,10 +806,9 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:badremote, expr, args_types, domain, clauses, context}) do + def format_diagnostic({:badremote, mfac, expr, args_types, domain, clauses, context}) do traces = collect_traces(expr, context) - {{:., _, [mod, fun]}, _, args} = expr - {mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args) + {mod, fun, arity, converter} = mfac explanation = empty_arg_reason(converter.(args_types)) || @@ -790,12 +817,15 @@ defmodule Module.Types.Apply do #{clauses_args_to_quoted_string(clauses, converter)} """ + mfa_or_fa = + if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}" + %{ details: %{typing_traces: traces}, message: IO.iodata_to_binary([ """ - incompatible types given to #{Exception.format_mfa(mod, fun, length(args))}: + incompatible types given to #{mfa_or_fa}: #{expr_to_string(expr) |> indent(4)} @@ -932,9 +962,19 @@ defmodule Module.Types.Apply do defp pluralize(1, singular, _), do: "1 #{singular}" defp pluralize(i, _, plural), do: "#{i} #{plural}" - defp format_mfa({{:., _, [mod, fun]}, _, args}) do - {mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args) - Exception.format_mfa(mod, fun, length(args)) + defp mfac({_, [inline: {mod, fun}] ++ _, _}, _mod, _fun, arity) do + {mod, fun, arity, & &1} + end + + defp mfac({{:., _, [mod, fun]}, _, args}, _mod, _fun, _arity) + when is_atom(mod) and is_atom(fun) do + {mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args) + {mod, fun, length(args), converter} + end + + defp mfac(_, mod, fun, arity) + when is_atom(mod) and is_atom(fun) and is_integer(arity) do + {mod, fun, arity, & &1} end ## Algebra helpers diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 500393a4d16..d2a18116ad0 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -243,6 +243,35 @@ defmodule Module.Types.ExprTest do """ end + test "calling a function with invalid arguments on variables" do + assert typeerror!( + ( + x = List + x.to_tuple(123) + ) + ) + |> strip_ansi() == + ~l""" + incompatible types given to List.to_tuple/1: + + x.to_tuple(123) + + given types: + + integer() + + but expected one of: + + list(term()) + + where "x" was given the type: + + # type: List + # from: types_test.ex:LINE-5 + x = List + """ + end + test "capture a function with non atoms" do assert typeerror!([<>], &x.foo_bar/2) == ~l""" @@ -1400,6 +1429,16 @@ defmodule Module.Types.ExprTest do assert typewarn!(:string.__info__(:functions)) == {dynamic(), ":string.__info__/1 is undefined or private"} + + assert typeerror!([x], x.__info__(:whatever)) |> strip_ansi() =~ """ + incompatible types given to __info__/1: + + x.__info__(:whatever) + + given types: + + :whatever + """ end test "behaviour_info/1" do From 712f24af0a13dd81b07d58ebd0408ce249c33ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 10:31:43 +0100 Subject: [PATCH 052/128] Do not run async groups on load and support --repeat-until-failure (#14107) --- lib/ex_unit/lib/ex_unit/runner.ex | 4 +- lib/ex_unit/lib/ex_unit/server.ex | 109 ++++++++++++++---------------- lib/ex_unit/test/ex_unit_test.exs | 18 +++++ 3 files changed, 72 insertions(+), 59 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/runner.ex b/lib/ex_unit/lib/ex_unit/runner.ex index 89996230cca..d94ee86f70e 100644 --- a/lib/ex_unit/lib/ex_unit/runner.ex +++ b/lib/ex_unit/lib/ex_unit/runner.ex @@ -125,7 +125,7 @@ defmodule ExUnit.Runner do # Run all sync modules directly for pair <- sync_modules do - running = spawn_modules(config, [[pair]], false, %{}) + running = spawn_modules(config, [{nil, [pair]}], false, %{}) running != %{} and wait_until_available(config, running) end @@ -161,7 +161,7 @@ defmodule ExUnit.Runner do running end - defp spawn_modules(config, [[_ | _] = modules | groups], async?, running) do + defp spawn_modules(config, [{_group, [_ | _] = modules} | groups], async?, running) do if max_failures_reached?(config) do running else diff --git a/lib/ex_unit/lib/ex_unit/server.ex b/lib/ex_unit/lib/ex_unit/server.ex index 820e6c1ca0e..1088dee923b 100644 --- a/lib/ex_unit/lib/ex_unit/server.ex +++ b/lib/ex_unit/lib/ex_unit/server.ex @@ -57,9 +57,10 @@ defmodule ExUnit.Server do state = %{ loaded: System.monotonic_time(), waiting: nil, - async_groups: %{}, + groups: %{}, + async_groups: [], async_modules: :queue.new(), - sync_modules: :queue.new() + sync_modules: [] } {:ok, state} @@ -72,31 +73,31 @@ defmodule ExUnit.Server do # Called once after all async modules have been sent and reverts the state. def handle_call(:take_sync_modules, _from, state) do - %{waiting: nil, loaded: :done, async_modules: async_modules} = state - 0 = :queue.len(async_modules) + %{waiting: nil, loaded: :done, async_groups: []} = state + true = :queue.is_empty(state.async_modules) - {:reply, :queue.to_list(state.sync_modules), - %{state | sync_modules: :queue.new(), loaded: System.monotonic_time()}} + {:reply, state.sync_modules, %{state | sync_modules: [], loaded: System.monotonic_time()}} end # Called by the runner when --repeat-until-failure is used. def handle_call({:restore_modules, async_modules, sync_modules}, _from, state) do - {async_modules, async_groups} = - Enum.map_reduce(async_modules, %{}, fn - {nil, [module]}, {modules, groups} -> - {[{:module, module} | modules], groups} + {async_modules, async_groups, groups} = + Enum.reduce(async_modules, {[], [], []}, fn + {nil, [module]}, {async_modules, async_groups, groups} -> + {[module | async_modules], async_groups, groups} - {group, group_modules}, {modules, groups} -> - {[{:group, group} | modules], Map.put(groups, group, group_modules)} + {group, group_modules}, {async_modules, async_groups, groups} -> + {async_modules, [group | async_groups], [{group, group_modules} | groups]} end) {:reply, :ok, %{ state | loaded: :done, + groups: Map.new(groups), async_groups: async_groups, async_modules: :queue.from_list(async_modules), - sync_modules: :queue.from_list(sync_modules) + sync_modules: sync_modules }} end @@ -108,22 +109,24 @@ defmodule ExUnit.Server do when is_integer(loaded) do state = if uniq? do - async_groups = - Map.new(state.async_groups, fn {group, modules} -> - {group, Enum.uniq(modules)} - end) - + groups = Map.new(state.groups, fn {group, modules} -> {group, Enum.uniq(modules)} end) + async_groups = state.async_groups |> Enum.uniq() |> Enum.reverse() async_modules = :queue.to_list(state.async_modules) |> Enum.uniq() |> :queue.from_list() - sync_modules = :queue.to_list(state.sync_modules) |> Enum.uniq() |> :queue.from_list() + sync_modules = state.sync_modules |> Enum.uniq() |> Enum.reverse() %{ state - | async_groups: async_groups, + | groups: groups, + async_groups: async_groups, async_modules: async_modules, sync_modules: sync_modules } else - state + %{ + state + | async_groups: Enum.reverse(state.async_groups), + sync_modules: Enum.reverse(state.sync_modules) + } end diff = System.convert_time_unit(System.monotonic_time() - loaded, :native, :microsecond) @@ -132,9 +135,7 @@ defmodule ExUnit.Server do def handle_call({:add, false = _async, _group, names}, _from, %{loaded: loaded} = state) when is_integer(loaded) do - state = - update_in(state.sync_modules, &Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end)) - + state = update_in(state.sync_modules, &Enum.reverse(names, &1)) {:reply, :ok, state} end @@ -143,7 +144,7 @@ defmodule ExUnit.Server do state = update_in( state.async_modules, - &Enum.reduce(names, &1, fn name, q -> :queue.in({:module, name}, q) end) + &Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end) ) {:reply, :ok, take_modules(state)} @@ -151,17 +152,16 @@ defmodule ExUnit.Server do def handle_call({:add, true = _async, group, names}, _from, %{loaded: loaded} = state) when is_integer(loaded) do - {async_groups, async_modules} = - case state.async_groups do - %{^group => entries} = async_groups -> - {%{async_groups | group => names ++ entries}, state.async_modules} + {groups, async_groups} = + case state.groups do + %{^group => entries} = groups -> + {%{groups | group => Enum.reverse(names, entries)}, state.async_groups} - %{} = async_groups -> - {Map.put(async_groups, group, names), :queue.in({:group, group}, state.async_modules)} + %{} = groups -> + {Map.put(groups, group, names), [group | state.async_groups]} end - {:reply, :ok, - take_modules(%{state | async_groups: async_groups, async_modules: async_modules})} + {:reply, :ok, take_modules(%{state | groups: groups, async_groups: async_groups})} end def handle_call({:add, _async?, _group, _names}, _from, state) do @@ -174,49 +174,44 @@ defmodule ExUnit.Server do defp take_modules(%{waiting: {from, count}} = state) do has_async_modules? = not :queue.is_empty(state.async_modules) + has_async_groups? = state.async_groups != [] cond do - not has_async_modules? and state.loaded == :done -> + not has_async_modules? and not has_async_groups? and state.loaded == :done -> GenServer.reply(from, nil) %{state | waiting: nil} - not has_async_modules? -> - state - - true -> - {async_modules, remaining_modules} = take_until(count, state.async_modules) + has_async_modules? -> + {reply, remaining_modules} = take_until(count, state.async_modules) + GenServer.reply(from, reply) + %{state | async_modules: remaining_modules, waiting: nil} - {async_modules, remaining_groups} = - Enum.map_reduce(async_modules, state.async_groups, fn - {:module, module}, async_groups -> - {[module], async_groups} + has_async_groups? -> + {groups, remaining_groups} = Enum.split(state.async_groups, count) - {:group, group}, async_groups -> - {group_modules, async_groups} = Map.pop!(async_groups, group) - {Enum.reverse(group_modules), async_groups} + {reply, groups} = + Enum.map_reduce(groups, state.groups, fn group, acc -> + {entries, acc} = Map.pop!(acc, group) + {{group, Enum.reverse(entries)}, acc} end) - GenServer.reply(from, async_modules) + GenServer.reply(from, reply) + %{state | groups: groups, async_groups: remaining_groups, waiting: nil} - %{ - state - | async_groups: remaining_groups, - async_modules: remaining_modules, - waiting: nil - } + true -> + state end end - # :queue.split fails if the provided count is larger than the queue size; - # as we also want to return the values as a list later, we directly - # return {list, queue} instead of {queue, queue} + # :queue.split fails if the provided count is larger than the queue size. + # We also want to return the values as tuples of shape {group, [modules]}. defp take_until(n, queue), do: take_until(n, queue, []) defp take_until(0, queue, acc), do: {Enum.reverse(acc), queue} defp take_until(n, queue, acc) do case :queue.out(queue) do - {{:value, item}, queue} -> take_until(n - 1, queue, [item | acc]) + {{:value, item}, queue} -> take_until(n - 1, queue, [{nil, [item]} | acc]) {:empty, queue} -> {Enum.reverse(acc), queue} end end diff --git a/lib/ex_unit/test/ex_unit_test.exs b/lib/ex_unit/test/ex_unit_test.exs index 3db534d401e..152a9122f7b 100644 --- a/lib/ex_unit/test/ex_unit_test.exs +++ b/lib/ex_unit/test/ex_unit_test.exs @@ -997,6 +997,24 @@ defmodule ExUnitTest do assert length(runs) == 6 end + test "repeats tests up to the configured number of times with groups" do + defmodule TestGroupedRepeatUntilFailureReached do + use ExUnit.Case, async: true, group: :example + test __ENV__.line, do: assert(true) + end + + configure_and_reload_on_exit(repeat_until_failure: 5) + + output = + capture_io(fn -> + assert ExUnit.run() == %{total: 1, failures: 0, skipped: 0, excluded: 0} + end) + + runs = String.split(output, "Running ExUnit", trim: true) + # 6 runs in total, 5 repeats + assert length(runs) == 6 + end + test "stops on failure" do {:ok, pid} = Agent.start_link(fn -> 0 end) Process.register(pid, :ex_unit_repeat_until_failure_count) From fae36c5e4928d87bed939dee4cc73e5ec1cb6084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 10:49:54 +0100 Subject: [PATCH 053/128] Queue async groups until state is loaded --- lib/ex_unit/lib/ex_unit/server.ex | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/server.ex b/lib/ex_unit/lib/ex_unit/server.ex index 1088dee923b..b991816c873 100644 --- a/lib/ex_unit/lib/ex_unit/server.ex +++ b/lib/ex_unit/lib/ex_unit/server.ex @@ -173,20 +173,13 @@ defmodule ExUnit.Server do end defp take_modules(%{waiting: {from, count}} = state) do - has_async_modules? = not :queue.is_empty(state.async_modules) - has_async_groups? = state.async_groups != [] - cond do - not has_async_modules? and not has_async_groups? and state.loaded == :done -> - GenServer.reply(from, nil) - %{state | waiting: nil} - - has_async_modules? -> + not :queue.is_empty(state.async_modules) -> {reply, remaining_modules} = take_until(count, state.async_modules) GenServer.reply(from, reply) %{state | async_modules: remaining_modules, waiting: nil} - has_async_groups? -> + state.async_groups != [] and state.loaded == :done -> {groups, remaining_groups} = Enum.split(state.async_groups, count) {reply, groups} = @@ -198,6 +191,10 @@ defmodule ExUnit.Server do GenServer.reply(from, reply) %{state | groups: groups, async_groups: remaining_groups, waiting: nil} + state.loaded == :done -> + GenServer.reply(from, nil) + %{state | waiting: nil} + true -> state end From 04378bd9a6d85d2699ec7ece051d56a0af66e4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 23 Dec 2024 10:55:52 +0100 Subject: [PATCH 054/128] Add build lock to deps.loadpaths (#14108) --- lib/mix/lib/mix/tasks/deps.loadpaths.ex | 51 +++++++++++++++++-------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/mix/lib/mix/tasks/deps.loadpaths.ex b/lib/mix/lib/mix/tasks/deps.loadpaths.ex index 1f6566216a9..d7daa070738 100644 --- a/lib/mix/lib/mix/tasks/deps.loadpaths.ex +++ b/lib/mix/lib/mix/tasks/deps.loadpaths.ex @@ -48,6 +48,12 @@ defmodule Mix.Tasks.Deps.Loadpaths do Mix.Task.run("archive.check", args) end + config = Mix.Project.config() + + if "--no-elixir-version-check" not in args do + check_elixir_version(config) + end + all = Mix.Dep.load_and_cache() all = @@ -57,14 +63,8 @@ defmodule Mix.Tasks.Deps.Loadpaths do all end - config = Mix.Project.config() - - if "--no-elixir-version-check" not in args do - check_elixir_version(config) - end - if "--no-deps-check" not in args do - deps_check(all, "--no-compile" in args) + deps_check(config, all, "--no-compile" in args) end Code.prepend_paths(Enum.flat_map(all, &Mix.Dep.load_paths/1), cache: true) @@ -93,25 +93,38 @@ defmodule Mix.Tasks.Deps.Loadpaths do end end + defp deps_check(config, all, no_compile?) do + with {:compile, _to_compile} <- deps_check(all, no_compile?) do + # We need to compile, we first grab the lock, then, we check + # again and compile if still applicable + Mix.Project.with_build_lock(config, fn -> + all = reload_deps(all) + + with {:compile, to_compile} <- deps_check(all, no_compile?) do + Mix.Tasks.Deps.Compile.compile(to_compile) + + to_compile + |> reload_deps() + |> Enum.filter(&(not Mix.Dep.ok?(&1))) + |> show_not_ok!() + end + end) + end + end + defp deps_check(all, no_compile?) do all = Enum.map(all, &check_lock/1) - {not_ok, compile} = partition(all, [], []) + {not_ok, to_compile} = partition(all, [], []) cond do not_ok != [] -> show_not_ok!(not_ok) - compile == [] or no_compile? -> + to_compile == [] or no_compile? -> :ok true -> - Mix.Tasks.Deps.Compile.compile(compile) - - compile - |> Enum.map(& &1.app) - |> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache()) - |> Enum.filter(&(not Mix.Dep.ok?(&1))) - |> show_not_ok!() + {:compile, to_compile} end end @@ -136,6 +149,12 @@ defmodule Mix.Tasks.Deps.Loadpaths do {Enum.reverse(not_ok), Enum.reverse(compile)} end + defp reload_deps(deps) do + deps + |> Enum.map(& &1.app) + |> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache()) + end + # Those are compiled by umbrella. defp from_umbrella?(dep) do dep.opts[:from_umbrella] From b7a5fd7b56764774b4a2c7d8a0e96221f3bc6b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 12:23:48 +0100 Subject: [PATCH 055/128] Improve container_cursor_to_quoted with trailing fragment, closes #14087 --- lib/elixir/lib/code/fragment.ex | 3 --- lib/elixir/test/elixir/code_fragment_test.exs | 25 ++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 75a9cf325a3..74ab83f594d 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -1226,10 +1226,7 @@ defmodule Code.Fragment do defp maybe_missing_stab?([{:else, _} | _]), do: true defp maybe_missing_stab?([{:catch, _} | _]), do: true defp maybe_missing_stab?([{:rescue, _} | _]), do: true - defp maybe_missing_stab?([{:stab_op, _, :->} | _]), do: false - defp maybe_missing_stab?([{:eol, _}, next | _]) when elem(next, 0) != :",", do: false - defp maybe_missing_stab?([_ | tail]), do: maybe_missing_stab?(tail) defp maybe_missing_stab?([]), do: false end diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs index a36f498e05f..2736937e810 100644 --- a/lib/elixir/test/elixir/code_fragment_test.exs +++ b/lib/elixir/test/elixir/code_fragment_test.exs @@ -1315,9 +1315,15 @@ defmodule CodeFragmentTest do assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") == s2q!("(fn x -> __cursor__() end)") - assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") == + assert cc2q!("(fn x ->", trailing_fragment: "\n:ok end)") == s2q!("(fn x -> __cursor__() end)") + assert cc2q!("(fn x when ", trailing_fragment: "-> :ok end)") == + s2q!("(fn x when __cursor__() -> :ok end)") + + assert cc2q!("(fn x when ", trailing_fragment: "->\n:ok end)") == + s2q!("(fn x when __cursor__() -> :ok end)") + assert cc2q!("(fn") == s2q!("(__cursor__())") assert cc2q!("(fn x") == s2q!("(__cursor__())") assert cc2q!("(fn x,") == s2q!("(__cursor__())") @@ -1327,6 +1333,23 @@ defmodule CodeFragmentTest do assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())") end + test "do -> end" do + assert cc2q!("if do\nx ->\n", trailing_fragment: "y\nz ->\nw\nend") == + s2q!("if do\nx ->\n__cursor__()\nend") + + assert cc2q!("if do\nx ->\ny", trailing_fragment: "\nz ->\nw\nend") == + s2q!("if do\nx ->\n__cursor__()\nend") + + assert cc2q!("if do\nx ->\ny\n", trailing_fragment: "\nz ->\nw\nend") == + s2q!("if do\nx ->\ny\n__cursor__()\nend") + + assert cc2q!("for x <- [], reduce: %{} do\ny, ", trailing_fragment: "-> :ok\nend") == + s2q!("for x <- [], reduce: %{} do\ny, __cursor__() -> :ok\nend") + + assert cc2q!("for x <- [], reduce: %{} do\ny, z when ", trailing_fragment: "-> :ok\nend") == + s2q!("for x <- [], reduce: %{} do\ny, z when __cursor__() -> :ok\nend") + end + test "removes tokens until opening" do assert cc2q!("(123") == s2q!("(__cursor__())") assert cc2q!("[foo") == s2q!("[__cursor__()]") From a3a632efd16ca0596e16c39619112f5f3ec18c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Dec 2024 17:25:14 +0100 Subject: [PATCH 056/128] Improve warning as errors deprecation notice --- lib/elixir/lib/code.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 1a4a400f06b..47ee408cd47 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1709,7 +1709,9 @@ defmodule Code do def put_compiler_option(:warnings_as_errors, _value) do IO.warn( ":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <> - "pass it as option to Kernel.ParallelCompiler instead" + "pass it as option to Kernel.ParallelCompiler instead or as a --warnings-as-errors flag. " <> + "If you need to set it as a default in a mix task, you can also set it under aliases: " <> + "[compile: \"compile --warnings-as-errors\"]" ) :ok From 463f1aa5937437d5ed7130bcfbb1b38449ef76e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 18:25:42 +0100 Subject: [PATCH 057/128] Do not recompile if compilation fails due to --warnings-as-errors --- lib/elixir/lib/code.ex | 2 +- lib/elixir/lib/kernel/parallel_compiler.ex | 21 +------ .../elixir/kernel/parallel_compiler_test.exs | 62 ------------------- lib/mix/lib/mix/compilers/elixir.ex | 21 ++++--- 4 files changed, 15 insertions(+), 91 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 47ee408cd47..be6e4beaaf2 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1709,7 +1709,7 @@ defmodule Code do def put_compiler_option(:warnings_as_errors, _value) do IO.warn( ":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <> - "pass it as option to Kernel.ParallelCompiler instead or as a --warnings-as-errors flag. " <> + "instead you must pass it as a --warnings-as-errors flag. " <> "If you need to set it as a default in a mix task, you can also set it under aliases: " <> "[compile: \"compile --warnings-as-errors\"]" ) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index db5a969a7cb..23d79179f3c 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -257,28 +257,13 @@ defmodule Kernel.ParallelCompiler do {status, modules_or_errors, info} = try do - outcome = spawn_workers(schedulers, cache, files, output, options) - {outcome, Keyword.get(options, :warnings_as_errors, false)} + spawn_workers(schedulers, cache, files, output, options) else - {{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true} - when r_warnings != [] or c_warnings != [] -> - message = - "Compilation failed due to warnings while using the --warnings-as-errors option" - - IO.puts(:stderr, message) - errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error)) - {:error, errors, %{info | runtime_warnings: [], compile_warnings: []}} - - {{:ok, outcome, info}, _} -> + {:ok, outcome, info} -> beam_timestamp = Keyword.get(options, :beam_timestamp) {:ok, write_module_binaries(outcome, output, beam_timestamp), info} - {{:error, errors, info}, true} -> - %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info - info = %{info | runtime_warnings: [], compile_warnings: []} - {:error, c_warnings ++ r_warnings ++ errors, info} - - {{:error, errors, info}, _} -> + {:error, errors, info} -> {:error, errors, info} after Module.ParallelChecker.stop(cache) diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index 95b5ae32b9e..6a0dc97a051 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -421,40 +421,6 @@ defmodule Kernel.ParallelCompilerTest do end) end - test "supports warnings as errors" do - [fixture] = - write_tmp( - "warnings_as_errors", - warnings_as_errors: """ - defmodule WarningsSample do - def hello(a), do: a - def hello(b), do: b - end - """ - ) - - output = tmp_path("not_to_be_used") - - try do - msg = - capture_io(:stderr, fn -> - assert {:error, [error], []} = - Kernel.ParallelCompiler.compile_to_path([fixture], output, - warnings_as_errors: true - ) - - assert {^fixture, {3, 7}, "this clause " <> _} = error - end) - - assert msg =~ - "Compilation failed due to warnings while using the --warnings-as-errors option\n" - after - purge([WarningsSample]) - end - - refute File.exists?(output) - end - test "does not use incorrect line number when error originates in another file" do File.mkdir_p!(tmp_path()) @@ -587,33 +553,5 @@ defmodule Kernel.ParallelCompilerTest do "cannot compile module WithBehaviourAndStruct (errors have been logged)" end) =~ expected_msg end - - test "supports warnings as errors" do - [fixture] = - write_tmp( - "warnings_as_errors", - warnings_as_errors: """ - defmodule WarningsSample do - def hello(a), do: a - def hello(b), do: b - end - """ - ) - - try do - msg = - capture_io(:stderr, fn -> - assert {:error, [error], []} = - Kernel.ParallelCompiler.require([fixture], warnings_as_errors: true) - - assert {^fixture, {3, 7}, "this clause " <> _} = error - end) - - assert msg =~ - "Compilation failed due to warnings while using the --warnings-as-errors option\n" - after - purge([WarningsSample]) - end - end end end diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index dcf04e4ed62..629bc73cd04 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -208,14 +208,18 @@ defmodule Mix.Compilers.Elixir do end Mix.Task.Compiler.notify_modules_compiled(lazy_modules_diff) - - unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings}) + unless_warnings_as_errors(opts, {:ok, all_warnings}) {:error, errors, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}, state} -> + {errors, warnings} = + if opts[:warnings_as_errors], + do: {errors ++ r_warnings ++ c_warnings, []}, + else: {errors, r_warnings ++ c_warnings} + # In case of errors, we show all previous warnings and all new ones. {_, _, sources, _, _, _} = state errors = Enum.map(errors, &diagnostic/1) - warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic/1) + warnings = Enum.map(warnings, &diagnostic/1) all_warnings = Keyword.get(opts, :all_warnings, errors == []) {:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors} after @@ -254,8 +258,7 @@ defmodule Mix.Compilers.Elixir do all_warnings = Keyword.get(opts, :all_warnings, true) previous_warnings = previous_warnings(sources, all_warnings) - - unless_previous_warnings_as_errors(previous_warnings, opts, {status, previous_warnings}) + unless_warnings_as_errors(opts, {status, previous_warnings}) end end @@ -1012,8 +1015,8 @@ defmodule Mix.Compilers.Elixir do File.rm(manifest <> ".checkpoint") end - defp unless_previous_warnings_as_errors(previous_warnings, opts, {status, all_warnings}) do - if previous_warnings != [] and opts[:warnings_as_errors] do + defp unless_warnings_as_errors(opts, {status, all_warnings}) do + if all_warnings != [] and opts[:warnings_as_errors] do message = "Compilation failed due to warnings while using the --warnings-as-errors option" IO.puts(:stderr, message) {:error, all_warnings} @@ -1049,7 +1052,6 @@ defmodule Mix.Compilers.Elixir do threshold = opts[:long_compilation_threshold] || 10 profile = opts[:profile] verbose = opts[:verbose] || false - warnings_as_errors = opts[:warnings_as_errors] || false pid = spawn_link(fn -> @@ -1071,8 +1073,7 @@ defmodule Mix.Compilers.Elixir do long_compilation_threshold: threshold, profile: profile, beam_timestamp: timestamp, - return_diagnostics: true, - warnings_as_errors: warnings_as_errors + return_diagnostics: true ] response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts) From 546a0db392a07b9363d440ebab33faf9e7352be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 24 Dec 2024 09:41:34 +0100 Subject: [PATCH 058/128] Use division by zero to show exception Closes #14111. --- lib/ex_unit/lib/ex_unit/assertions.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index ba119f1057b..22b1a790e57 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -786,7 +786,7 @@ defmodule ExUnit.Assertions do ## Examples assert_raise ArithmeticError, "bad argument in arithmetic expression", fn -> - 1 + "test" + 1 / 0 end assert_raise RuntimeError, ~r/^today's lucky number is 0\.\d+!$/, fn -> From 4911916f626021953b2614bb9699998cffb25200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 24 Dec 2024 09:47:23 +0100 Subject: [PATCH 059/128] Do not warn when comparing literals --- lib/elixir/lib/kernel.ex | 8 +++--- lib/elixir/lib/module/types/apply.ex | 12 +++++++-- lib/elixir/lib/module/types/expr.ex | 5 ++-- .../test/elixir/module/types/expr_test.exs | 27 +++++++++++-------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 3bfc11f0e9d..3ceeab3835f 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -1972,7 +1972,7 @@ defmodule Kernel do defp build_boolean_check(operator, check, true_clause, false_clause) do annotate_case( - [optimize_boolean: true, type_check: :expr], + [optimize_boolean: true], quote do case unquote(check) do false -> unquote(false_clause) @@ -2006,7 +2006,7 @@ defmodule Kernel do assert_no_match_or_guard_scope(__CALLER__.context, "!") annotate_case( - [optimize_boolean: true, type_check: :expr], + [optimize_boolean: true], quote do case unquote(value) do x when :"Elixir.Kernel".in(x, [false, nil]) -> false @@ -2020,7 +2020,7 @@ defmodule Kernel do assert_no_match_or_guard_scope(__CALLER__.context, "!") annotate_case( - [optimize_boolean: true, type_check: :expr], + [optimize_boolean: true], quote do case unquote(value) do x when :"Elixir.Kernel".in(x, [false, nil]) -> true @@ -3910,7 +3910,7 @@ defmodule Kernel do defp build_if(condition, do: do_clause, else: else_clause) do annotate_case( - [optimize_boolean: true, type_check: :expr], + [optimize_boolean: true], quote do case unquote(condition) do x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 2c315b88cb9..ed510e5e634 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -400,11 +400,19 @@ defmodule Module.Types.Apply do end end - def remote(:erlang, name, [left, right] = args_types, expr, stack, context) + def remote( + :erlang, + name, + [left, right] = args_types, + {_, _, args} = expr, + stack, + context + ) when name in [:==, :"/=", :"=:=", :"=/="] do context = cond do - stack.mode == :infer -> + # We ignore quoted literals as they most likely come from generated code. + stack.mode == :infer or Macro.quoted_literal?(args) -> context name in [:==, :"/="] and number_type?(left) and number_type?(right) -> diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 27b925fe66d..22bddbbf5c1 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -263,8 +263,9 @@ defmodule Module.Types.Expr do {case_type, context} = of_expr(case_expr, stack, context) # If we are only type checking the expression and the expression is a literal, - # let's mark it as generated, as it is most likely a macro code. - if is_atom(case_expr) and {:type_check, :expr} in meta do + # let's mark it as generated, as it is most likely a macro code. However, if + # no clause is matched, we should still check for that. + if Macro.quoted_literal?(case_expr) do for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args} else clauses diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index d2a18116ad0..27d9e4ed26f 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -860,7 +860,11 @@ defmodule Module.Types.ExprTest do test "in dynamic mode" do assert typedyn!([x = 123, y = 456.0], x < y) == dynamic(boolean()) assert typedyn!([x = 123, y = 456.0], x == y) == dynamic(boolean()) - assert typedyn!(123 == 456) == boolean() + assert typedyn!([x = 123, y = 456], x == y) == dynamic(boolean()) + end + + test "using literals" do + assert typecheck!(:foo == :bar) == boolean() end test "min/max" do @@ -1060,6 +1064,15 @@ defmodule Module.Types.ExprTest do end describe "case" do + test "does not type check literals" do + assert typecheck!( + case :dev do + :dev -> :ok + :prod -> :error + end + ) == atom([:ok, :error]) + end + test "returns unions of all clauses" do assert typecheck!( [x], @@ -1115,7 +1128,7 @@ defmodule Module.Types.ExprTest do end describe "conditionals" do - test "if does not report on literal atoms" do + test "if does not report on literals" do assert typecheck!( if true do :ok @@ -1123,17 +1136,9 @@ defmodule Module.Types.ExprTest do ) == atom([:ok, nil]) end - test "and does not report on literal atoms" do + test "and does not report on literals" do assert typecheck!(false and true) == boolean() end - - test "and reports on non-atom literals" do - assert typeerror!(1 and true) == ~l""" - the following conditional expression will always evaluate to integer(): - - 1 - """ - end end describe "receive" do From 6a3301f237ad6dff6f66c9ded91426a23abfe757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 24 Dec 2024 10:17:42 +0100 Subject: [PATCH 060/128] Release v1.18.1 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ VERSION | 2 +- bin/elixir | 2 +- bin/elixir.bat | 2 +- bin/elixir.ps1 | 2 +- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ccdce5a07..844bc7b8c12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -225,6 +225,36 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z +## v1.18.1 (2024-12-24) + +### 1. Enhancements + + * [Kernel] Do not emit type violation warnings when comparing or matching against literals + * [Kernel] Do not validate clauses of private overridable functions + +### 2. Bug fixes + +#### Elixir + + * [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions + * [Kernel] Do not crash when typing violation is detected on dynamic dispatch + * [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation + * [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation + * [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases + * [Kernel] Ensure diagnostic traces are kept backwards compatible + +#### ExUnit + + * [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading + * [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups + +#### Mix + + * [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors` + * [mix deps.loadpaths] Add build lock + * [mix escript.build] Ensure build succeeds when protocol consolidation is disabled + * [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems + ## v1.18.0 (2024-12-19) ### 1. Enhancements diff --git a/VERSION b/VERSION index 84cc529467b..ec6d649be65 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.0 +1.18.1 diff --git a/bin/elixir b/bin/elixir index c937bea182c..69fa4b4695a 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.18.0 +ELIXIR_VERSION=1.18.1 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index 09f3050ff4a..781cd9ad066 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @echo off -set ELIXIR_VERSION=1.18.0 +set ELIXIR_VERSION=1.18.1 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 index 4ef9cd0e1b7..c53c82273f3 100755 --- a/bin/elixir.ps1 +++ b/bin/elixir.ps1 @@ -1,6 +1,6 @@ #!/usr/bin/env pwsh -$ELIXIR_VERSION = "1.18.0" +$ELIXIR_VERSION = "1.18.1" $scriptPath = Split-Path -Parent $PSCommandPath $erlExec = "erl" From b9bd0e3dd9180401b6cd8ec3b005fd153f6b19c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Dec 2024 19:37:22 +0100 Subject: [PATCH 061/128] Bring conditional violation reports back --- lib/elixir/lib/kernel.ex | 8 ++++---- lib/elixir/test/elixir/module/types/expr_test.exs | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 3ceeab3835f..3bfc11f0e9d 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -1972,7 +1972,7 @@ defmodule Kernel do defp build_boolean_check(operator, check, true_clause, false_clause) do annotate_case( - [optimize_boolean: true], + [optimize_boolean: true, type_check: :expr], quote do case unquote(check) do false -> unquote(false_clause) @@ -2006,7 +2006,7 @@ defmodule Kernel do assert_no_match_or_guard_scope(__CALLER__.context, "!") annotate_case( - [optimize_boolean: true], + [optimize_boolean: true, type_check: :expr], quote do case unquote(value) do x when :"Elixir.Kernel".in(x, [false, nil]) -> false @@ -2020,7 +2020,7 @@ defmodule Kernel do assert_no_match_or_guard_scope(__CALLER__.context, "!") annotate_case( - [optimize_boolean: true], + [optimize_boolean: true, type_check: :expr], quote do case unquote(value) do x when :"Elixir.Kernel".in(x, [false, nil]) -> true @@ -3910,7 +3910,7 @@ defmodule Kernel do defp build_if(condition, do: do_clause, else: else_clause) do annotate_case( - [optimize_boolean: true], + [optimize_boolean: true, type_check: :expr], quote do case unquote(condition) do x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 27d9e4ed26f..49c4c8bf932 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1139,6 +1139,14 @@ defmodule Module.Types.ExprTest do test "and does not report on literals" do assert typecheck!(false and true) == boolean() end + + test "and reports violations" do + assert typeerror!([x = 123], x and true) =~ """ + the following conditional expression will always evaluate to integer(): + + x + """ + end end describe "receive" do From 265b9aaeb3d50353aa55e729ee5f388c96319b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 27 Dec 2024 11:37:25 +0100 Subject: [PATCH 062/128] Update AST metadata documentation (#14120) --- lib/elixir/lib/macro.ex | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 117cc4bee9a..734c6dad60b 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -146,8 +146,6 @@ defmodule Macro do * `:delimiter` - contains the opening delimiter for sigils, strings, and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like) - * `:format` - set to `:keyword` when an atom is defined as a keyword - * `:do` - contains metadata about the `do` location in a function call with `do`-`end` blocks (when `:token_metadata` is true) @@ -159,10 +157,18 @@ defmodule Macro do expressions inside "blocks of code", which are either direct children of a `__block__` or the right side of `->`. The last expression of the block does not have metadata if it is not followed by an end of line - character (either a newline or `;`) + character (either a newline or `;`). This entry may appear multiple times + in the same metadata if the expression is surround by parens + + * `:format` - set to `:keyword` when an atom is defined as a keyword. + It may also be set to `:atom` to distinguish `nil`, `false`, and `true` * `:indentation` - indentation of a sigil heredoc + * `:parens` - denotes a node was surrounded by parens for grouping. + This entry may appear multiple times in the same metadata if + multiple pairs are used for grouping + The following metadata keys are private: * `:alias` - Used for alias hygiene. From 0cb79b0ba127062e441ca04cfc8c85680f118899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 28 Dec 2024 09:18:59 +0100 Subject: [PATCH 063/128] Add OTP 25 to sign entry --- .github/workflows/release.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a28d67400d..2efac4bbcd2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,13 +115,13 @@ jobs: with: name: Docs path: Docs.zip* - + sign: needs: [build] strategy: fail-fast: true matrix: - otp: [26, 27] + otp: [25, 26, 27] flavor: [windows, linux] env: @@ -160,14 +160,14 @@ jobs: run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore" env: ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}" - + - name: Create Release Hashes if: matrix.flavor == 'windows' shell: pwsh run: | $sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1 $sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum" - + $sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256 $sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum" @@ -191,7 +191,7 @@ jobs: steps: - uses: actions/download-artifact@v4 with: - pattern: '{sign-*-elixir-otp-*,Docs}' + pattern: "{sign-*-elixir-otp-*,Docs}" merge-multiple: true - name: Upload Pre-built @@ -235,7 +235,7 @@ jobs: steps: - uses: actions/download-artifact@v4 with: - pattern: '{sign-*-elixir-otp-*,Docs}' + pattern: "{sign-*-elixir-otp-*,Docs}" merge-multiple: true - name: Init purge keys file From 6dbb932bf31418baf5fe986c8e7cbee0119d2bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 28 Dec 2024 09:32:43 +0100 Subject: [PATCH 064/128] Properly raise for invalid async value --- lib/ex_unit/lib/ex_unit/case.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index 81b46908ecd..519ed08a1ba 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -566,6 +566,10 @@ defmodule ExUnit.Case do group = Keyword.get(opts, :group, nil) parameterize = Keyword.get(opts, :parameterize, nil) + if not is_boolean(async?) do + raise ArgumentError, ":async must be a boolean, got: #{inspect(async?)}" + end + if not (parameterize == nil or (is_list(parameterize) and Enum.all?(parameterize, &is_map/1))) do raise ArgumentError, ":parameterize must be a list of maps, got: #{inspect(parameterize)}" end From 7e149619f54c0d57c402a5aa8f60415091971499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Dec 2024 11:07:22 +0100 Subject: [PATCH 065/128] Fix expression-tag pairing inside with type checking --- lib/elixir/lib/module/types/expr.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 22bddbbf5c1..ff7f5646509 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -518,7 +518,7 @@ defmodule Module.Types.Expr do defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do {pattern, guards} = extract_head([left]) - {_type, context} = Pattern.of_match(pattern, guards, dynamic(), :with, expr, stack, context) + {_type, context} = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context) {_, context} = of_expr(right, stack, context) context end From f3dfb72c69ae4ba9742d8bd4bb97bcdb557b374e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Dec 2024 11:39:36 +0100 Subject: [PATCH 066/128] Properly handle optional keys in map intersection --- lib/elixir/lib/module/types/descr.ex | 9 ++++----- lib/elixir/test/elixir/module/types/descr_test.exs | 3 +++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index c9af3919dde..175e3e315d2 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1329,11 +1329,10 @@ defmodule Module.Types.Descr do :maps.next(iterator) |> map_literal_intersection_loop(acc) _ -> - # If the key is marked as not_set in the open map, we can ignore it. - if type1 == @not_set do - :maps.next(iterator) |> map_literal_intersection_loop(acc) - else - throw(:empty) + # If the key is optional in the open map, we can ignore it + case type1 do + %{optional: 1} -> :maps.next(iterator) |> map_literal_intersection_loop(acc) + _ -> throw(:empty) end end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 13517e87bd0..3011da51345 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -163,6 +163,9 @@ defmodule Module.Types.DescrTest do assert intersection(closed_map(a: integer()), open_map(b: not_set())) == closed_map(a: integer()) + assert intersection(closed_map(a: integer()), open_map(b: if_set(integer()))) == + closed_map(a: integer()) + assert equal?( intersection(closed_map(a: integer()), closed_map(a: if_set(integer()))), closed_map(a: integer()) From c7a841baae4a47cc93a42743bfa5509e0e8abfff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 30 Dec 2024 17:00:16 +0100 Subject: [PATCH 067/128] Add an environment variable to optionally disable compilation locking (#14129) --- lib/mix/lib/mix.ex | 5 +++++ lib/mix/lib/mix/sync/lock.ex | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 97f018e6fce..dc4181a33ab 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -352,6 +352,11 @@ defmodule Mix do * `MIX_INSTALL_DIR` *(since v1.12.0)* - specifies directory where `Mix.install/2` keeps install cache + * `MIX_OS_CONCURRENCY_LOCK` - when set to `0` or `false`, disables mix compilation locking. + While not recommended, this may be necessary in cases where hard links or TCP sockets are + not available. When opting for this behaviour, make sure to not start concurrent compilations + of the same project. + * `MIX_PATH` - appends extra code paths * `MIX_PROFILE` - a list of comma-separated Mix tasks to profile the time spent on diff --git a/lib/mix/lib/mix/sync/lock.ex b/lib/mix/lib/mix/sync/lock.ex index 5253d610e09..dc65e57713c 100644 --- a/lib/mix/lib/mix/sync/lock.ex +++ b/lib/mix/lib/mix/sync/lock.ex @@ -79,6 +79,10 @@ defmodule Mix.Sync.Lock do This function can also be called if this process already has the lock. In such case the function is executed immediately. + When the `MIX_OS_CONCURRENCY_LOCK` environment variable is set to + a falsy value, the lock is ignored and the function is executed + immediately. + ## Options * `:on_taken` - a one-arity function called if the lock is held @@ -96,9 +100,9 @@ defmodule Mix.Sync.Lock do path = Path.join([System.tmp_dir!(), "mix_lock", hash]) pdict_key = {__MODULE__, path} - has_lock? = Process.get(pdict_key) + has_lock? = Process.get(pdict_key, false) - if has_lock? do + if has_lock? or lock_disabled?() do fun.() else lock = lock(path, opts[:on_taken]) @@ -115,6 +119,8 @@ defmodule Mix.Sync.Lock do end end + defp lock_disabled?(), do: System.get_env("MIX_OS_CONCURRENCY_LOCK") in ~w(0 false) + defp lock(path, on_taken) do File.mkdir_p!(path) @@ -198,11 +204,12 @@ defmodule Mix.Sync.Lock do :invalidated {:error, reason} -> - raise File.LinkError, - reason: reason, - action: "create hard link", - existing: port_path, - new: lock_path + Mix.raise(""" + could not create hard link from #{port_path} to "#{lock_path}: #{:file.format_error(reason)}. + + Hard link support is required for Mix compilation locking. If your system \ + does not support hard links, set MIX_OS_CONCURRENCY_LOCK=0\ + """) end end From 2da3300bfce855d2565644fb90e493e4f2769017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Dec 2024 08:45:30 +0100 Subject: [PATCH 068/128] Do not wrap literals in variable when expanding in/2 --- lib/elixir/lib/kernel.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 3bfc11f0e9d..04777ddb320 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -4584,6 +4584,9 @@ defmodule Kernel do defp in_var(true, {atom, _, context} = var, fun) when is_atom(atom) and is_atom(context), do: fun.(var) + defp in_var(true, var, fun) when is_atom(var) or is_binary(var) or is_number(var), + do: fun.(var) + defp in_var(true, ast, fun) do quote do var = unquote(ast) From dddb8f7b1931e0c1de1da2d479bb3bfe889c263c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Dec 2024 08:53:47 +0100 Subject: [PATCH 069/128] Bring kv-observer image back as it is used in guides --- lib/elixir/pages/images/kv-observer.png | Bin 0 -> 34195 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/elixir/pages/images/kv-observer.png diff --git a/lib/elixir/pages/images/kv-observer.png b/lib/elixir/pages/images/kv-observer.png new file mode 100644 index 0000000000000000000000000000000000000000..7527d7e5c807127471122a4972adec488a210d01 GIT binary patch literal 34195 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 literal 0 HcmV?d00001 From f088fc9cdb093d3d8db4088d79a84fe79826c92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 2 Jan 2025 09:40:50 +0100 Subject: [PATCH 070/128] Print compilation lock waiting message to stderr (#14138) --- lib/mix/lib/mix/project.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex index 6f946b7e27d..157badc96b2 100644 --- a/lib/mix/lib/mix/project.ex +++ b/lib/mix/lib/mix/project.ex @@ -917,7 +917,10 @@ defmodule Mix.Project do build_path = build_path(config) on_taken = fn os_pid -> - Mix.shell().info("Waiting for lock on the build directory (held by process #{os_pid})") + Mix.shell().error([ + IO.ANSI.reset(), + "Waiting for lock on the build directory (held by process #{os_pid})" + ]) end Mix.Sync.Lock.with_lock(build_path, fun, on_taken: on_taken) @@ -931,7 +934,10 @@ defmodule Mix.Project do deps_path = deps_path(config) on_taken = fn os_pid -> - Mix.shell().info("Waiting for lock on the deps directory (held by process #{os_pid})") + Mix.shell().error([ + IO.ANSI.reset(), + "Waiting for lock on the deps directory (held by process #{os_pid})" + ]) end Mix.Sync.Lock.with_lock(deps_path, fun, on_taken: on_taken) From 24714c68a795156b603b39a423f3c91dcdde92ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Jan 2025 18:35:46 +0100 Subject: [PATCH 071/128] Improve error message for invalid default arguments --- lib/elixir/lib/module/types/apply.ex | 23 ++++++++++--- lib/elixir/src/elixir_def.erl | 2 +- .../elixir/module/types/integration_test.exs | 33 +++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index ed510e5e634..c01a9d12da1 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -785,7 +785,7 @@ defmodule Module.Types.Apply do def format_diagnostic({:badlocal, expr, args_types, domain, clauses, context}) do traces = collect_traces(expr, context) converter = &Function.identity/1 - {fun, _, _} = expr + {fun, meta, _} = expr explanation = empty_arg_reason(args_types) || @@ -794,16 +794,29 @@ defmodule Module.Types.Apply do #{clauses_args_to_quoted_string(clauses, converter)} """ - %{ - details: %{typing_traces: traces}, - message: - IO.iodata_to_binary([ + banner = + case fun == :super && meta[:default] && meta[:super] do + {_kind, fun} -> + """ + incompatible types given as default arguments to #{fun}/#{length(args_types)}: + """ + + _ -> """ incompatible types given to #{fun}/#{length(args_types)}: #{expr_to_string(expr) |> indent(4)} given types: + """ + end + + %{ + details: %{typing_traces: traces}, + message: + IO.iodata_to_binary([ + banner, + """ #{args_to_quoted_string(args_types, domain, converter) |> indent(4)} diff --git a/lib/elixir/src/elixir_def.erl b/lib/elixir/src/elixir_def.erl index af9874b2985..da348e7eeb3 100644 --- a/lib/elixir/src/elixir_def.erl +++ b/lib/elixir/src/elixir_def.erl @@ -314,7 +314,7 @@ unpack_defaults(Kind, Meta, Name, Args, S, E) -> unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, VersionOffset, Acc, Clauses) -> Base = match_defaults(Acc, length(Acc) + VersionOffset, []), {Args, Invoke} = extract_defaults(List, length(Base) + VersionOffset, [], []), - Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}} | DefaultMeta], Base ++ Invoke}}, + Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}}, {default, true} | DefaultMeta], Base ++ Invoke}}, unpack_expanded(Kind, Meta, Name, T, VersionOffset, [Expr | Acc], [Clause | Clauses]); unpack_expanded(Kind, Meta, Name, [H | T], VersionOffset, Acc, Clauses) -> unpack_expanded(Kind, Meta, Name, T, VersionOffset, [H | Acc], Clauses); diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 943c18f7d8c..5ca6f17bb34 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -244,6 +244,39 @@ defmodule Module.Types.IntegrationTest do assert_no_warnings(files) end + test "incompatible default argument" do + files = %{ + "a.ex" => """ + defmodule A do + def ok(x = :ok \\\\ nil) do + x + end + end + """ + } + + warnings = [ + ~S""" + warning: incompatible types given as default arguments to ok/1: + + -nil- + + but expected one of: + + dynamic(:ok) + + typing violation found at: + │ + 2 │ def ok(x = :ok \\ nil) do + │ ~ + │ + └─ a.ex:2:18: A.ok/0 + """ + ] + + assert_warnings(files, warnings) + end + test "returns diagnostics with source and file" do files = %{ "a.ex" => """ From 432d6fa1c28a70452589edb99a279241f6bfbf5f Mon Sep 17 00:00:00 2001 From: Guillaume Milan Date: Tue, 7 Jan 2025 11:39:41 +0100 Subject: [PATCH 072/128] Fix crashing on autocompleting structs with runtime values (#14150) --- lib/iex/lib/iex/autocomplete.ex | 2 +- lib/iex/test/iex/autocomplete_test.exs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex index ae245e2a6fb..531b129e661 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -363,7 +363,7 @@ defmodule IEx.Autocomplete do end) entries = - for {key, _value} <- pairs, + for key when key != :__struct__ <- Map.keys(pairs), name = Atom.to_string(key), if(hint == "", do: not String.starts_with?(name, "_"), diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index 0827daf82e9..c9d7747091e 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -430,6 +430,19 @@ defmodule IEx.AutocompleteTest do assert {:yes, ~c"ry: ", []} = expand(~c"%URI{var | path: \"foo\", que") assert {:no, [], []} = expand(~c"%URI{var | path: \"foo\", unkno") assert {:no, [], []} = expand(~c"%Unknown{var | path: \"foo\", unkno") + + eval("var = %URI{}") + + assert {:yes, ~c"", entries} = expand(~c"%{var | ") + assert ~c"path:" in entries + assert ~c"query:" in entries + + assert {:yes, ~c"", entries} = expand(~c"%{var | path: \"foo\",") + assert ~c"path:" not in entries + assert ~c"query:" in entries + + assert {:yes, ~c"ry: ", []} = expand(~c"%{var | path: \"foo\", que") + assert {:no, [], []} = expand(~c"%URI{var | path: \"foo\", unkno") end test "completion for map keys in update syntax" do From defef37856b2d1befa07119db8bc54f35bada6f2 Mon Sep 17 00:00:00 2001 From: Daniel Drexler Date: Thu, 9 Jan 2025 02:22:02 -0600 Subject: [PATCH 073/128] Improve warning for charlists (#14160) The current warning for single-quoted character lists is confusing because it appears to be a stylistic note instead of a deprecation warning. This commit updates the language to make the reasoning clearer and to more clearly describe what `mix format --migrate` will change about the users' code. --- lib/elixir/src/elixir_tokenizer.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index cfb6e5e14be..5823299a39b 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -818,9 +818,10 @@ handle_strings(T, Line, Column, H, Scope, Tokens) -> NewScope = case H of $' -> - Message = "single-quoted strings represent charlists. " + Message = "using single-quoted strings to represent charlists is deprecated.\n" "Use ~c\"\" if you indeed want a charlist or use \"\" instead.\n" - "You may run \"mix format --migrate\" to fix this warning automatically.", + "You may run \"mix format --migrate\" to change all single-quoted\n" + "strings to use the ~c sigil and fix this warning.", prepend_warning(Line, Column-1, Message, InterScope); _ -> From 5c340e488ea91d07953151189accb88d8997e546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 9 Jan 2025 10:03:13 +0100 Subject: [PATCH 074/128] Migrate the git repo against the origin branch, closes #14163 --- lib/mix/lib/mix/scm/git.ex | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/scm/git.ex b/lib/mix/lib/mix/scm/git.ex index 4ff914dc7ee..95fcff44caa 100644 --- a/lib/mix/lib/mix/scm/git.ex +++ b/lib/mix/lib/mix/scm/git.ex @@ -126,17 +126,17 @@ defmodule Mix.SCM.Git do update_origin(opts[:git]) # Fetch external data - rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts) + lock_rev = get_lock_rev(opts[:lock], opts) ["--git-dir=.git", "fetch", "--force", "--quiet"] |> Kernel.++(progress_switch(git_version())) |> Kernel.++(tags_switch(opts[:tag])) |> Kernel.++(depth_switch(opts[:depth])) - |> Kernel.++(refspec_switch(opts, rev)) + |> Kernel.++(refspec_switch(opts, lock_rev || get_opts_rev(opts))) |> git!() # Migrate the Git repo - rev = rev || default_branch() + rev = lock_rev || get_origin_opts_rev(opts) || default_branch() git!(["--git-dir=.git", "checkout", "--quiet", rev]) if opts[:submodules] do @@ -314,6 +314,14 @@ defmodule Mix.SCM.Git do opts[:branch] || opts[:ref] || opts[:tag] end + defp get_origin_opts_rev(opts) do + if branch = opts[:branch] do + "origin/#{branch}" + else + opts[:ref] || opts[:tag] + end + end + defp redact_uri(git) do case URI.parse(git) do %{userinfo: nil} -> git From ac910c1cc232adf1dfb3f5f7fca9a5bbb21a2ec2 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Thu, 9 Jan 2025 18:25:51 +0900 Subject: [PATCH 075/128] Prevent infinite loop in compiler for some invalid type specs (#14155) --- lib/elixir/lib/kernel/typespec.ex | 11 ++++++---- lib/elixir/test/elixir/typespec_test.exs | 27 +++++++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex index fbe1b86b315..023c86f9fac 100644 --- a/lib/elixir/lib/kernel/typespec.ex +++ b/lib/elixir/lib/kernel/typespec.ex @@ -938,7 +938,7 @@ defmodule Kernel.Typespec do ## Helpers - # This is a backport of Macro.expand/2 because we want to expand + # This is a modified backport of Macro.expand/2 because we want to expand # aliases but we don't them to become compile-time references. defp expand_remote({:__aliases__, meta, list} = alias, env) do case :elixir_aliases.expand_or_concat(meta, list, env, true) do @@ -946,9 +946,12 @@ defmodule Kernel.Typespec do receiver [head | tail] -> - case Macro.expand_once(head, env) do - head when is_atom(head) -> :elixir_aliases.concat([head | tail]) - _ -> alias + case Macro.expand(head, env) do + head when is_atom(head) -> + :elixir_aliases.concat([head | tail]) + + _ -> + compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}") end end end diff --git a/lib/elixir/test/elixir/typespec_test.exs b/lib/elixir/test/elixir/typespec_test.exs index c81bb4682c7..7cf5e714dc3 100644 --- a/lib/elixir/test/elixir/typespec_test.exs +++ b/lib/elixir/test/elixir/typespec_test.exs @@ -75,6 +75,14 @@ defmodule TypespecTest do @type my_type :: %URI.t(){} end end + + assert_raise Kernel.TypespecError, + ~r"unexpected expression in typespec: t\.Foo", + fn -> + test_module do + @type my_type :: t.Foo + end + end end test "invalid function specification" do @@ -120,7 +128,7 @@ defmodule TypespecTest do test "redefined type" do assert_raise Kernel.TypespecError, - ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:126", + ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:134", fn -> test_module do @type foo :: atom @@ -129,7 +137,7 @@ defmodule TypespecTest do end assert_raise Kernel.TypespecError, - ~r"type foo/2 is already defined in .*test/elixir/typespec_test.exs:136", + ~r"type foo/2 is already defined in .*test/elixir/typespec_test.exs:144", fn -> test_module do @type foo :: atom @@ -139,7 +147,7 @@ defmodule TypespecTest do end assert_raise Kernel.TypespecError, - ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:145", + ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:153", fn -> test_module do @type foo :: atom @@ -841,6 +849,19 @@ defmodule TypespecTest do assert [{:atom, _, Keyword}, {:atom, _, :t}, [{:var, _, :value}]] = kw_with_value_args end + test "@type with macro in alias" do + bytecode = + test_module do + defmacro module() do + quote do: __MODULE__ + end + + @type my_type :: module().Foo + end + + assert [type: {:my_type, {:atom, _, TypespecTest.TypespecSample.Foo}, []}] = types(bytecode) + end + test "@type with a reserved signature" do assert_raise Kernel.TypespecError, ~r"type required\/1 is a reserved type and it cannot be defined", From 40da7e8857a3cefaff91a13d6ebc956c9a8d5299 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Thu, 9 Jan 2025 18:26:50 +0900 Subject: [PATCH 076/128] Fix ExUnit crash when diffing bitstring specifiers (#14161) --- lib/ex_unit/lib/ex_unit/diff.ex | 3 ++- lib/ex_unit/test/ex_unit/diff_test.exs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/ex_unit/lib/ex_unit/diff.ex b/lib/ex_unit/lib/ex_unit/diff.ex index 03c75c96e45..170e988c35a 100644 --- a/lib/ex_unit/lib/ex_unit/diff.ex +++ b/lib/ex_unit/lib/ex_unit/diff.ex @@ -878,7 +878,8 @@ defmodule ExUnit.Diff do end defp rebuild_split_strings(%{contents: contents, delimiter: delimiter}, right) do - %{contents: contents ++ [{false, right}], delimiter: delimiter} + {new_right, diff} = extract_diff_meta(right) + %{contents: contents ++ [{diff, new_right}], delimiter: delimiter} end defp rebuild_concat_string(literal, nil, []) do diff --git a/lib/ex_unit/test/ex_unit/diff_test.exs b/lib/ex_unit/test/ex_unit/diff_test.exs index 019f6c754c7..a2317cf73e2 100644 --- a/lib/ex_unit/test/ex_unit/diff_test.exs +++ b/lib/ex_unit/test/ex_unit/diff_test.exs @@ -1133,6 +1133,12 @@ defmodule ExUnit.DiffTest do "-<> <> \"baz\"-", "+\"foobar\"+" ) + + refute_diff( + "hello " <> <<_::binary-size(6)>> = "hello world", + "\"hello \" <> -<<_::binary-size(6)>>-", + "\"hello +world+\"" + ) end test "underscore" do From b0dae83353faa7a81043c2bf0be9d602d0b5325a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 9 Jan 2025 16:36:59 +0100 Subject: [PATCH 077/128] Clarify the need for better tools in the TCP guide --- lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md index abb883f3a87..30826a5e0e6 100644 --- a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md +++ b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md @@ -157,7 +157,7 @@ say me say me ``` -Yes, it works! However, does it *scale*? +Yes, it works! However, can it handle more than one client? Try to connect two telnet clients at the same time. When you do so, you will notice that the second client doesn't echo: @@ -303,4 +303,8 @@ Luckily, this can be done by using `Supervisor.child_spec/2`, which allows us to Now we have an always running acceptor that starts temporary task processes under an always running task supervisor. +## Wrapping up + +In this chapter we have implemented a straigh-forward TCP acceptor, which allowed is to explore tools for concurrency and fault-tolerance. While our acceptor can manage concurrent connections, it is still not ready for production. In practice, TCP servers run a pool of acceptors, instead of a single one, each of them with their own supervisor. While Elixir has tools to make it easier to partition and scale the accept, such as the `PartitionSupervisor`, they are out of scope for this guide. In any case, a better path forward may be to use existing packages tailored for this use case, such as [Ranch](https://github.com/ninenines/ranch) (in Erlang) or [Thousand Island](https://github.com/mtrudel/thousand_island) (in Elixir). + In the next chapter, we will start parsing the client requests and sending responses, finishing our server. From f5e42ba11aab10ff4553e125362ce7f60e41e8b5 Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Thu, 9 Jan 2025 12:10:23 -0500 Subject: [PATCH 078/128] Update task-and-gen-tcp.md (#14166) --- lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md index 30826a5e0e6..79d3a2972ec 100644 --- a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md +++ b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md @@ -305,6 +305,6 @@ Now we have an always running acceptor that starts temporary task processes unde ## Wrapping up -In this chapter we have implemented a straigh-forward TCP acceptor, which allowed is to explore tools for concurrency and fault-tolerance. While our acceptor can manage concurrent connections, it is still not ready for production. In practice, TCP servers run a pool of acceptors, instead of a single one, each of them with their own supervisor. While Elixir has tools to make it easier to partition and scale the accept, such as the `PartitionSupervisor`, they are out of scope for this guide. In any case, a better path forward may be to use existing packages tailored for this use case, such as [Ranch](https://github.com/ninenines/ranch) (in Erlang) or [Thousand Island](https://github.com/mtrudel/thousand_island) (in Elixir). +In this chapter, we implemented a basic TCP acceptor while exploring concurrency and fault-tolerance. Our acceptor can manage concurrent connections, but it is still not ready for production. Production-ready TCP servers run a pool of acceptors, each with their own supervisor. Elixir's `PartitionSupervisor` might be used to partition and scale the acceptor, but it is out of scope for this guide. In practice, you will use existing packages tailored for this use-case, such as [Ranch](https://github.com/ninenines/ranch) (in Erlang) or [Thousand Island](https://github.com/mtrudel/thousand_island) (in Elixir). In the next chapter, we will start parsing the client requests and sending responses, finishing our server. From ff29628602c656dc6ebb0b3b72f1be9c79aaefc1 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Fri, 10 Jan 2025 20:02:36 +0100 Subject: [PATCH 079/128] Track mix_pubsub and mix_lock folders per user (#14171) --- lib/mix/lib/mix/sync/lock.ex | 9 ++++++++- lib/mix/lib/mix/sync/pubsub.ex | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/sync/lock.ex b/lib/mix/lib/mix/sync/lock.ex index dc65e57713c..62369a6e0fe 100644 --- a/lib/mix/lib/mix/sync/lock.ex +++ b/lib/mix/lib/mix/sync/lock.ex @@ -97,7 +97,7 @@ defmodule Mix.Sync.Lock do opts = Keyword.validate!(opts, [:on_taken]) hash = key |> :erlang.md5() |> Base.url_encode64(padding: false) - path = Path.join([System.tmp_dir!(), "mix_lock", hash]) + path = Path.join(base_path(), hash) pdict_key = {__MODULE__, path} has_lock? = Process.get(pdict_key, false) @@ -119,6 +119,13 @@ defmodule Mix.Sync.Lock do end end + defp base_path do + user = System.get_env("USER", "default") + path = Path.join([System.tmp_dir!(), "mix_lock_#{Base.url_encode64(user, padding: false)}"]) + File.mkdir_p!(path) + path + end + defp lock_disabled?(), do: System.get_env("MIX_OS_CONCURRENCY_LOCK") in ~w(0 false) defp lock(path, on_taken) do diff --git a/lib/mix/lib/mix/sync/pubsub.ex b/lib/mix/lib/mix/sync/pubsub.ex index 8c72406e944..b048f69f2a5 100644 --- a/lib/mix/lib/mix/sync/pubsub.ex +++ b/lib/mix/lib/mix/sync/pubsub.ex @@ -272,9 +272,16 @@ defmodule Mix.Sync.PubSub do defp hash(key), do: :erlang.md5(key) + defp base_path do + user = System.get_env("USER", "default") + path = Path.join([System.tmp_dir!(), "mix_pubsub_#{Base.url_encode64(user, padding: false)}"]) + File.mkdir_p!(path) + path + end + defp path(hash) do hash = Base.url_encode64(hash, padding: false) - Path.join([System.tmp_dir!(), "mix_pubsub", hash]) + Path.join(base_path(), hash) end defp recv(socket, size, timeout \\ :infinity) do From c78f426bf547cc905a647763ae0a58fe8a1528bc Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Thu, 9 Jan 2025 21:49:21 +0000 Subject: [PATCH 080/128] Improve clarity of Path.safe_relative/2 argument names (#14167) Switches from `cwd` to `relative_to`, which is more generic and more consistent with possible use cases of the function. --- lib/elixir/lib/path.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 7dca45f018c..535cac5ea6c 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -884,7 +884,7 @@ defmodule Path do * A `..` component would make it so that the path would traverse up above the root of `relative_to`. - * A symbolic link in the path points to something above the root of `cwd`. + * A symbolic link in the path points to something above the root of `relative_to`. ## Examples @@ -906,10 +906,10 @@ defmodule Path do """ @doc since: "1.14.0" @spec safe_relative(t, t) :: {:ok, binary} | :error - def safe_relative(path, cwd \\ File.cwd!()) do + def safe_relative(path, relative_to \\ File.cwd!()) do path = IO.chardata_to_string(path) - case :filelib.safe_relative_path(path, cwd) do + case :filelib.safe_relative_path(path, relative_to) do :unsafe -> :error relative_path -> {:ok, IO.chardata_to_string(relative_path)} end From 9593cef2f5146486afcacb205ceae9674851e5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Sat, 11 Jan 2025 10:53:25 +0100 Subject: [PATCH 081/128] Remove duplicate call to mkdir in concurrency lock (#14175) --- lib/mix/lib/mix/sync/lock.ex | 5 ++--- lib/mix/lib/mix/sync/pubsub.ex | 13 ++++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/mix/lib/mix/sync/lock.ex b/lib/mix/lib/mix/sync/lock.ex index 62369a6e0fe..89bc5826646 100644 --- a/lib/mix/lib/mix/sync/lock.ex +++ b/lib/mix/lib/mix/sync/lock.ex @@ -120,10 +120,9 @@ defmodule Mix.Sync.Lock do end defp base_path do + # We include user in the dir to avoid permission conflicts across users user = System.get_env("USER", "default") - path = Path.join([System.tmp_dir!(), "mix_lock_#{Base.url_encode64(user, padding: false)}"]) - File.mkdir_p!(path) - path + Path.join(System.tmp_dir!(), "mix_lock_#{Base.url_encode64(user, padding: false)}") end defp lock_disabled?(), do: System.get_env("MIX_OS_CONCURRENCY_LOCK") in ~w(0 false) diff --git a/lib/mix/lib/mix/sync/pubsub.ex b/lib/mix/lib/mix/sync/pubsub.ex index b048f69f2a5..bf58a098af2 100644 --- a/lib/mix/lib/mix/sync/pubsub.ex +++ b/lib/mix/lib/mix/sync/pubsub.ex @@ -272,18 +272,17 @@ defmodule Mix.Sync.PubSub do defp hash(key), do: :erlang.md5(key) - defp base_path do - user = System.get_env("USER", "default") - path = Path.join([System.tmp_dir!(), "mix_pubsub_#{Base.url_encode64(user, padding: false)}"]) - File.mkdir_p!(path) - path - end - defp path(hash) do hash = Base.url_encode64(hash, padding: false) Path.join(base_path(), hash) end + defp base_path do + # We include user in the dir to avoid permission conflicts across users + user = System.get_env("USER", "default") + Path.join(System.tmp_dir!(), "mix_pubsub_#{Base.url_encode64(user, padding: false)}") + end + defp recv(socket, size, timeout \\ :infinity) do # eintr is "Interrupted system call". with {:error, :eintr} <- :gen_tcp.recv(socket, size, timeout) do From 6f7eaf1122951d1f7e504dbf0be3d4e8ab8e6a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 12 Jan 2025 20:08:30 +0100 Subject: [PATCH 082/128] Keep line information in defstruct --- lib/elixir/lib/kernel.ex | 34 +++++++++----------------- lib/elixir/lib/kernel/utils.ex | 6 ++--- lib/elixir/test/elixir/kernel_test.exs | 4 +++ 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 04777ddb320..5a454e3281c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5463,33 +5463,21 @@ defmodule Kernel do use `@type`. """ defmacro defstruct(fields) do - header = - quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do - {struct, derive, escaped_struct, kv, body} = - Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__) - - case derive do - [] -> :ok - _ -> Protocol.__derive__(derive, __MODULE__, __ENV__) - end - end + quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do + {struct, derive, escaped_struct, kv, body} = + Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__) - # We attach the line: 0 to struct functions because we don't want - # the generated callbacks to count towards code coverage and metrics, - # especially since they are often expanded at compile-time. - functions = - quote line: 0, unquote: false do - def __struct__(), do: unquote(escaped_struct) - def __struct__(unquote(kv)), do: unquote(body) + case derive do + [] -> :ok + _ -> Protocol.__derive__(derive, __MODULE__, __ENV__) end - footer = - quote do - Kernel.Utils.announce_struct(__MODULE__) - struct - end + def __struct__(), do: unquote(escaped_struct) + def __struct__(unquote(kv)), do: unquote(body) - {:__block__, [], [header, functions, footer]} + Kernel.Utils.announce_struct(__MODULE__) + struct + end end @doc ~S""" diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 251be8c19de..b2c2ffc6aed 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -174,14 +174,14 @@ defmodule Kernel.Utils do true -> case enforce_keys do [] -> - quote do + quote line: 0, generated: true do Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map -> %{map | key => val} end) end _ -> - quote do + quote line: 0, generated: true do {map, keys} = Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn {key, val}, {map, keys} -> @@ -201,7 +201,7 @@ defmodule Kernel.Utils do end false -> - quote do + quote line: 0, generated: true do :lists.foldl( fn {key, val}, acc -> %{acc | key => val} end, unquote(escaped_struct), diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index fa4b7364a1a..4a458e9fd17 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -205,6 +205,10 @@ defmodule KernelTest do defmodule User do assert is_map(defstruct name: "john") + # Ensure we keep the line information around. + # It is important for debugging tools, ExDoc, etc. + {:v1, :def, anno, _clauses} = Module.get_definition(__MODULE__, {:__struct__, 1}) + anno[:line] == __ENV__.line - 4 end test "struct/1 and struct/2" do From 98aeee34b651827887f7054411f0720e0d7ba58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Jan 2025 07:33:06 +0100 Subject: [PATCH 083/128] Simplify bat and ps1 files (#14180) --- bin/elixir.bat | 4 ++-- bin/elixir.ps1 | 50 ++++++++------------------------------------------ 2 files changed, 10 insertions(+), 44 deletions(-) diff --git a/bin/elixir.bat b/bin/elixir.bat index 781cd9ad066..251a6aaec24 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -133,9 +133,9 @@ if not defined useIEx ( set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra% if defined ELIXIR_CLI_DRY_RUN ( - echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* + echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* ) else ( - "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* + "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* ) exit /B %ERRORLEVEL% :end diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 index c53c82273f3..b7b4d8e263d 100755 --- a/bin/elixir.ps1 +++ b/bin/elixir.ps1 @@ -76,14 +76,6 @@ if (($allArgs.Count -eq 0) -or (($allArgs.Count -eq 1) -and ($allArgs[0] -in @(" exit 1 } -function NormalizeArg { - param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string[]] $Items - ) - $Items -join "," -} - function QuoteString { param( [Parameter(ValueFromPipeline = $true)] @@ -103,7 +95,6 @@ function QuoteString { } } -$elixirParams = @() $erlangParams = @() $beforeExtras = @() $allOtherParams = @() @@ -111,32 +102,16 @@ $allOtherParams = @() $runErlPipe = $null $runErlLog = $null -for ($i = 0; $i -lt $allArgs.Count; $i++) { +:loop for ($i = 0; $i -lt $allArgs.Count; $i++) { $private:arg = $allArgs[$i] switch -exact ($arg) { { $_ -in @("-e", "-r", "-pr", "-pa", "-pz", "--eval", "--remsh", "--dot-iex", "--dbg") } { - $private:nextArg = NormalizeArg($allArgs[++$i]) - - $elixirParams += $arg - $elixirParams += $nextArg - + ++$i break } - { $_ -in @("-v", "--version") } { - # Standalone options goes only once in the Elixir params, when they are empty. - if (($elixirParams.Count -eq 0) -and ($allOtherParams.Count -eq 0)) { - $elixirParams += $arg - } - else { - $allOtherParams += $arg - } - break - } - - "--no-halt" { - $elixirParams += $arg + { $_ -in @("-v", "--version", "--no-halt") } { break } @@ -204,14 +179,11 @@ for ($i = 0; $i -lt $allArgs.Count; $i++) { } "+iex" { - $elixirParams += "+iex" $useIex = $true - break } "+elixirc" { - $elixirParams += "+elixirc" break } @@ -229,9 +201,6 @@ for ($i = 0; $i -lt $allArgs.Count; $i++) { exit 1 } - $elixirParams += "--rpc-eval" - $elixirParams += $key - $elixirParams += $value break } @@ -249,16 +218,14 @@ for ($i = 0; $i -lt $allArgs.Count; $i++) { exit 1 } - $elixirParams += "-boot_var" - $elixirParams += $key - $elixirParams += $value + $erlangParams += "-boot_var" + $erlangParams += $key + $erlangParams += $value break } Default { - $private:normalized = NormalizeArg $arg - $allOtherParams += $normalized - break + break :loop } } } @@ -280,8 +247,7 @@ if ($null -ne $env:ELIXIR_ERL_OPTIONS) { $allParams += $erlangParams $allParams += $beforeExtras $allParams += "-extra" -$allParams += $elixirParams -$allParams += $allOtherParams +$allParams += $allArgs $binSuffix = "" From 786f3ce79721779efdd4985989c72264b98b2e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Jan 2025 08:54:07 +0100 Subject: [PATCH 084/128] Add --color for easy enabling and disabling of coloring (#14183) --- bin/elixir | 3 ++- bin/elixir.bat | 2 ++ bin/elixir.ps1 | 3 ++- lib/elixir/lib/kernel/cli.ex | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bin/elixir b/bin/elixir index 69fa4b4695a..676a0d21e8f 100755 --- a/bin/elixir +++ b/bin/elixir @@ -18,6 +18,7 @@ Usage: $(basename "$0") [options] [.exs file] [data] -pz "PATH" Appends the given path to Erlang code path (*) -v, --version Prints Erlang/OTP and Elixir versions (standalone) + --color BOOL Enables or disables ANSI coloring --erl "SWITCHES" Switches to be passed down to Erlang (*) --eval "COMMAND" Evaluates the given command, same as -e (*) --logger-otp-reports BOOL Enables or disables OTP reporting @@ -114,7 +115,7 @@ while [ $I -le $LENGTH ]; do -v|--no-halt) C=1 ;; - -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg) + -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg|--color) C=2 ;; --rpc-eval) diff --git a/bin/elixir.bat b/bin/elixir.bat index 251a6aaec24..ddf06ee1f41 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -24,6 +24,7 @@ echo -pa "PATH" Prepends the given path to Erlang code path echo -pz "PATH" Appends the given path to Erlang code path (*) echo -v, --version Prints Erlang/OTP and Elixir versions (standalone) echo. +echo --color BOOL Enables or disables ANSI coloring echo --erl "SWITCHES" Switches to be passed down to Erlang (*) echo --eval "COMMAND" Evaluates the given command, same as -e (*) echo --logger-otp-reports BOOL Enables or disables OTP reporting @@ -110,6 +111,7 @@ if ""==!par:--no-halt=! (goto startloop) if ""==!par:--remsh=! (shift && goto startloop) if ""==!par:--dot-iex=! (shift && goto startloop) if ""==!par:--dbg=! (shift && goto startloop) +if ""==!par:--color=! (shift && goto startloop) rem ******* ERLANG PARAMETERS ********************** if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop) if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop) diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 index b7b4d8e263d..007f3aa05ca 100755 --- a/bin/elixir.ps1 +++ b/bin/elixir.ps1 @@ -26,6 +26,7 @@ Usage: $scriptName [options] [.exs file] [data] -pz "PATH" Appends the given path to Erlang code path (*) -v, --version Prints Erlang/OTP and Elixir versions (standalone) + --color Enables or disables ANSI coloring --erl "SWITCHES" Switches to be passed down to Erlang (*) --eval "COMMAND" Evaluates the given command, same as -e (*) --logger-otp-reports BOOL Enables or disables OTP reporting @@ -106,7 +107,7 @@ $runErlLog = $null $private:arg = $allArgs[$i] switch -exact ($arg) { - { $_ -in @("-e", "-r", "-pr", "-pa", "-pz", "--eval", "--remsh", "--dot-iex", "--dbg") } { + { $_ -in @("-e", "-r", "-pr", "-pa", "-pz", "--eval", "--remsh", "--dot-iex", "--dbg", "--color") } { ++$i break } diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index c4b302b236b..517a47ce277 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -295,6 +295,24 @@ defmodule Kernel.CLI do parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]}) end + defp parse_argv([~c"--color", value | t], config) do + config = + case value do + ~c"true" -> + Application.put_env(:elixir, :ansi_enabled, true) + config + + ~c"false" -> + Application.put_env(:elixir, :ansi_enabled, false) + config + + _ -> + %{config | errors: ["--color : must be a boolean" | config.errors]} + end + + parse_argv(t, config) + end + ## Compiler defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do From 88c75bcbf5c52b7d1c5f976298afc18f33b1f8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Jan 2025 11:29:21 +0100 Subject: [PATCH 085/128] Remove powershell scripts It current leaves the shell broken after quitting Erlang. Issues have been reported upstream, so we may be able to bring it back in future Erlang/OTP versions. --- bin/elixir.ps1 | 271 --------------------- bin/elixirc.ps1 | 35 --- bin/iex.ps1 | 30 --- bin/mix.ps1 | 32 ++- lib/elixir/test/elixir/kernel/cli_test.exs | 4 +- 5 files changed, 22 insertions(+), 350 deletions(-) delete mode 100755 bin/elixir.ps1 delete mode 100755 bin/elixirc.ps1 delete mode 100755 bin/iex.ps1 diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 deleted file mode 100755 index 007f3aa05ca..00000000000 --- a/bin/elixir.ps1 +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env pwsh - -$ELIXIR_VERSION = "1.18.1" - -$scriptPath = Split-Path -Parent $PSCommandPath -$erlExec = "erl" - -# The iex.ps1, elixirc.ps1 and mix.ps1 scripts may populate this var. -if ($null -eq $allArgs) { - $allArgs = $args -} - -function PrintElixirHelp { - $scriptName = Split-Path -Leaf $PSCommandPath - $help = @" -Usage: $scriptName [options] [.exs file] [data] - -## General options - - -e "COMMAND" Evaluates the given command (*) - -h, --help Prints this message (standalone) - -r "FILE" Requires the given files/patterns (*) - -S SCRIPT Finds and executes the given script in `$PATH - -pr "FILE" Requires the given files/patterns in parallel (*) - -pa "PATH" Prepends the given path to Erlang code path (*) - -pz "PATH" Appends the given path to Erlang code path (*) - -v, --version Prints Erlang/OTP and Elixir versions (standalone) - - --color Enables or disables ANSI coloring - --erl "SWITCHES" Switches to be passed down to Erlang (*) - --eval "COMMAND" Evaluates the given command, same as -e (*) - --logger-otp-reports BOOL Enables or disables OTP reporting - --logger-sasl-reports BOOL Enables or disables SASL reporting - --no-halt Does not halt the Erlang VM after execution - --short-version Prints Elixir version (standalone) - -Options given after the .exs file or -- are passed down to the executed code. -Options can be passed to the Erlang runtime using `$ELIXIR_ERL_OPTIONS or --erl. - -## Distribution options - -The following options are related to node distribution. - - --cookie COOKIE Sets a cookie for this distributed node - --hidden Makes a hidden node - --name NAME Makes and assigns a name to the distributed node - --rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*) - --sname NAME Makes and assigns a short name to the distributed node - ---name and --sname may be set to undefined so one is automatically generated. - -## Release options - -The following options are generally used under releases. - - --boot "FILE" Uses the given FILE.boot to start the system - --boot-var VAR "VALUE" Makes `$VAR available as VALUE to FILE.boot (*) - --erl-config "FILE" Loads configuration in FILE.config written in Erlang (*) - --vm-args "FILE" Passes the contents in file as arguments to the VM - ---pipe-to is not supported via PowerShell. - -** Options marked with (*) can be given more than once. -** Standalone options can't be combined with other options. -"@ - - Write-Host $help -} - -if (($allArgs.Count -eq 1) -and ($allArgs[0] -eq "--short-version")) { - Write-Host "$ELIXIR_VERSION" - exit -} - -if (($allArgs.Count -eq 0) -or (($allArgs.Count -eq 1) -and ($allArgs[0] -in @("-h", "--help")))) { - PrintElixirHelp - exit 1 -} - -function QuoteString { - param( - [Parameter(ValueFromPipeline = $true)] - [string] $Item - ) - - # We surround the string with double quotes, in order to preserve its contents as - # only one command arg. - # This is needed because PowerShell consider spaces as separator of arguments. - # The double quotes around will be removed when PowerShell process the argument. - # See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#passing-quoted-strings-to-external-commands - if ($Item.Contains(" ")) { - '"' + $Item + '"' - } - else { - $Item - } -} - -$erlangParams = @() -$beforeExtras = @() -$allOtherParams = @() - -$runErlPipe = $null -$runErlLog = $null - -:loop for ($i = 0; $i -lt $allArgs.Count; $i++) { - $private:arg = $allArgs[$i] - - switch -exact ($arg) { - { $_ -in @("-e", "-r", "-pr", "-pa", "-pz", "--eval", "--remsh", "--dot-iex", "--dbg", "--color") } { - ++$i - break - } - - { $_ -in @("-v", "--version", "--no-halt") } { - break - } - - "--cookie" { - $erlangParams += "-setcookie" - $erlangParams += $allArgs[++$i] - break - } - - "--hidden" { - $erlangParams += "-hidden" - break - } - - "--name" { - $erlangParams += "-name" - $erlangParams += $allArgs[++$i] - break - } - - "--sname" { - $erlangParams += "-sname" - $erlangParams += $allArgs[++$i] - break - } - - "--boot" { - $erlangParams += "-boot" - $erlangParams += $allArgs[++$i] - break - } - - "--erl-config" { - $erlangParams += "-config" - $erlangParams += $allArgs[++$i] - break - } - - "--vm-args" { - $erlangParams += "-args_file" - $erlangParams += $allArgs[++$i] - break - } - - "--logger-otp-reports" { - $private:tempVal = $allArgs[$i + 1] - if ($tempVal -in @("true", "false")) { - $erlangParams += @("-logger", "handle_otp_reports", $allArgs[++$i]) - } - break - } - - "--logger-sasl-reports" { - $private:tempVal = $allArgs[$i + 1] - if ($tempVal -in @("true", "false")) { - $erlangParams += @("-logger", "handle_sasl_reports", $allArgs[++$i]) - } - break - } - - "--erl" { - $private:erlFlags = $allArgs[++$i] -split " " - $beforeExtras += $erlFlags - break - } - - "+iex" { - $useIex = $true - break - } - - "+elixirc" { - break - } - - "--rpc-eval" { - $private:key = $allArgs[++$i] - $private:value = $allArgs[++$i] - - if ($null -eq $key) { - Write-Error "--rpc-eval: NODE must be present" - exit 1 - } - - if ($null -eq $value) { - Write-Error "--rpc-eval: COMMAND for the '$key' node must be present" - exit 1 - } - - break - } - - "--boot-var" { - $private:key = $allArgs[++$i] - $private:value = $allArgs[++$i] - - if ($null -eq $key) { - Write-Error "--boot-var: VAR must be present" - exit 1 - } - - if ($null -eq $value) { - Write-Error "--boot-var: Value for the '$key' var must be present" - exit 1 - } - - $erlangParams += "-boot_var" - $erlangParams += $key - $erlangParams += $value - break - } - - Default { - break :loop - } - } -} - -if ($null -eq $useIEx) { - $beforeExtras = @("-s", "elixir", "start_cli") + $beforeExtras -} - -$beforeExtras = @("-pa", "$(Join-Path $scriptPath -ChildPath "../lib/elixir/ebin")") + $beforeExtras -$beforeExtras = @("-noshell", "-elixir_root", "$(Join-Path $scriptPath -ChildPath "../lib")") + $beforeExtras - -$allParams = @() - -if ($null -ne $env:ELIXIR_ERL_OPTIONS) { - $private:erlFlags = $env:ELIXIR_ERL_OPTIONS -split " " - $allParams += $erlFlags -} - -$allParams += $erlangParams -$allParams += $beforeExtras -$allParams += "-extra" -$allParams += $allArgs - -$binSuffix = "" - -# The variable is available after PowerShell 7.2. Previous to that, PS only worked on Windows. -if ($isWindows -or ($null -eq $isWindows)) { - $binSuffix = ".exe" -} - -$binPath = "$erlExec$binSuffix" - -# We double the double-quotes because they are going to be escaped by arguments parsing. -$paramsPart = $allParams | ForEach-Object -Process { QuoteString($_ -replace "`"", "`"`"") } - -if ($env:ELIXIR_CLI_DRY_RUN) { - Write-Host "$binPath $paramsPart" -} -else { - $output = Start-Process -FilePath $binPath -ArgumentList $paramsPart -NoNewWindow -Wait -PassThru - exit $output.ExitCode -} diff --git a/bin/elixirc.ps1 b/bin/elixirc.ps1 deleted file mode 100755 index 29742f010d6..00000000000 --- a/bin/elixirc.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env pwsh - -$scriptName = Split-Path -Leaf $PSCommandPath - -if (($args.Count -eq 0) -or ($args[0] -in @("-h", "--help"))) { - Write-Host @" -Usage: $scriptName [elixir switches] [compiler switches] [.ex files] - - -h, --help Prints this message and exits - -o The directory to output compiled files - -v, --version Prints Elixir version and exits (standalone) - - --ignore-module-conflict Does not emit warnings if a module was previously defined - --no-debug-info Does not attach debug info to compiled modules - --no-docs Does not attach documentation to compiled modules - --profile time Profile the time to compile modules - --verbose Prints compilation status - --warnings-as-errors Treats warnings as errors and returns non-zero exit status - -** Options given after -- are passed down to the executed code -** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS -** Options can be passed to the Erlang compiler using ERL_COMPILER_OPTIONS -"@ - exit -} - -$scriptPath = Split-Path -Parent $PSCommandPath -$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" - -$prependedArgs = @("+elixirc") - -$allArgs = $prependedArgs + $args - -# The dot is going to evaluate the script with the vars defined here. -. $elixirMainScript diff --git a/bin/iex.ps1 b/bin/iex.ps1 deleted file mode 100755 index 870909377fb..00000000000 --- a/bin/iex.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env pwsh - -$scriptName = Split-Path -Leaf $PSCommandPath - -if ($args[0] -in @("-h", "--help")) { - Write-Host @" -Usage: $scriptName [options] [.exs file] [data] - -The following options are exclusive to IEx: - - --dbg pry Sets the backend for Kernel.dbg/2 to IEx.pry/0 - --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment. - Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists. - If FILE is empty, then no file will be loaded. - --remsh NAME Connects to a node using a remote shell. - -It accepts all other options listed by "elixir --help". -"@ - exit -} - -$scriptPath = Split-Path -Parent $PSCommandPath -$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" - -$prependedArgs = @("--no-halt", "--erl", "-user elixir", "+iex") - -$allArgs = $prependedArgs + $args - -# The dot is going to evaluate the script with the vars defined here. -. $elixirMainScript diff --git a/bin/mix.ps1 b/bin/mix.ps1 index a1d3bd29458..05b19a04746 100755 --- a/bin/mix.ps1 +++ b/bin/mix.ps1 @@ -1,13 +1,23 @@ -#!/usr/bin/env pwsh +# Store path to mix.bat as a FileInfo object +$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat')) +$newArgs = @() -$scriptPath = Split-Path -Parent $PSCommandPath -$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" +for ($i = 0; $i -lt $args.length; $i++) +{ + if ($args[$i] -is [array]) + { + # Commas created the array so we need to reintroduce those commas + for ($j = 0; $j -lt $args[$i].length - 1; $j++) + { + $newArgs += ($args[$i][$j] + ',') + } + $newArgs += $args[$i][-1] + } + else + { + $newArgs += $args[$i] + } +} -$mixFile = Join-Path -Path $scriptPath -ChildPath "mix" - -$prependedArgs = @($mixFile) - -$allArgs = $prependedArgs + $args - -# The dot is going to evaluate the script with the vars defined here. -. $elixirMainScript +# Corrected arguments are ready to pass to batch file +& $mixBatPath $newArgs \ No newline at end of file diff --git a/lib/elixir/test/elixir/kernel/cli_test.exs b/lib/elixir/test/elixir/kernel/cli_test.exs index 79602df7b72..6779161fad9 100644 --- a/lib/elixir/test/elixir/kernel/cli_test.exs +++ b/lib/elixir/test/elixir/kernel/cli_test.exs @@ -64,9 +64,7 @@ end test_parameters = if(PathHelpers.windows?(), do: [%{cli_extension: ".bat"}], - else: - [%{cli_extension: ""}] ++ - if(System.find_executable("pwsh"), do: [%{cli_extension: ".ps1"}], else: []) + else: [%{cli_extension: ""}] ) defmodule Kernel.CLI.ExecutableTest do From 4def31f8abea5fba44d2d92b11a72f3f9c855efd Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 15 Jan 2025 20:41:33 +0900 Subject: [PATCH 086/128] Mention the new type system in typespecs doc (#14188) --- lib/elixir/pages/references/typespecs.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/references/typespecs.md b/lib/elixir/pages/references/typespecs.md index 95eb40253f2..49d39f30e24 100644 --- a/lib/elixir/pages/references/typespecs.md +++ b/lib/elixir/pages/references/typespecs.md @@ -1,7 +1,12 @@ # Typespecs reference -Elixir comes with a notation for declaring types and specifications. This document is a -reference into their uses and syntax. +> #### Typespecs are not set-theoretic types {: .warning} +> +> Elixir is in the process of implementing its +> [own type system](./gradual-set-theoretic-types.md) based on set-theoretic types. +> Typespecs, which are described in the following document, are a distinct notation +> for declaring types and specifications based on Erlang. +> Typespecs may be phased out as the set-theoretic type effort moves forward. Elixir is a dynamically typed language, and as such, type specifications are never used by the compiler to optimize or modify code. Still, using type specifications is useful because: @@ -48,6 +53,13 @@ The syntax Elixir provides for type specifications is similar to [the one in Erl The notation to represent the union of types is the pipe `|`. For example, the typespec `type :: atom() | pid() | tuple()` creates a type `type` that can be either an `atom`, a `pid`, or a `tuple`. This is usually called a [sum type](https://en.wikipedia.org/wiki/Tagged_union) in other languages +> #### Differences with set-theoretic types {: .warning} +> +> While they do share some similarities, the types below do not map one-to-one +> to the new types from the set theoretic type system. +> For example, there is no plan to support subsets of the `integer()` type such +> as positive, ranges or literals. + ### Basic types type :: From 01a88e7137aa601dcce8e49952f9f7990a357eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 20 Jan 2025 18:51:29 +0100 Subject: [PATCH 087/128] Simplify token pruning, closes #14139 --- lib/elixir/src/elixir_tokenizer.erl | 119 ++++++++++++++-------------- 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 5823299a39b..33f08c0ff28 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -1763,84 +1763,81 @@ add_cursor(_Line, Column, noprune, Terminators, Tokens) -> {Column, Terminators, Tokens}; add_cursor(Line, Column, prune_and_cursor, Terminators, Tokens) -> PrePrunedTokens = prune_identifier(Tokens), - {PrunedTokens, PrunedTerminators} = prune_tokens(PrePrunedTokens, [], Terminators), + PrunedTokens = prune_tokens(PrePrunedTokens, []), CursorTokens = [ {')', {Line, Column + 11, nil}}, {'(', {Line, Column + 10, nil}}, {paren_identifier, {Line, Column, nil}, '__cursor__'} | PrunedTokens ], - {Column + 12, PrunedTerminators, CursorTokens}. + {Column + 12, Terminators, CursorTokens}. prune_identifier([{identifier, _, _} | Tokens]) -> Tokens; prune_identifier(Tokens) -> Tokens. %%% Any terminator needs to be closed -prune_tokens([{'end', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, ['end' | Opener], Terminators); -prune_tokens([{')', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, [')' | Opener], Terminators); -prune_tokens([{']', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, [']' | Opener], Terminators); -prune_tokens([{'}', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, ['}' | Opener], Terminators); -prune_tokens([{'>>', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, ['>>' | Opener], Terminators); +prune_tokens([{'end', _} | Tokens], Opener) -> + prune_tokens(Tokens, ['end' | Opener]); +prune_tokens([{')', _} | Tokens], Opener) -> + prune_tokens(Tokens, [')' | Opener]); +prune_tokens([{']', _} | Tokens], Opener) -> + prune_tokens(Tokens, [']' | Opener]); +prune_tokens([{'}', _} | Tokens], Opener) -> + prune_tokens(Tokens, ['}' | Opener]); +prune_tokens([{'>>', _} | Tokens], Opener) -> + prune_tokens(Tokens, ['>>' | Opener]); %%% Close opened terminators -prune_tokens([{'fn', _} | Tokens], ['end' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'do', _} | Tokens], ['end' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'(', _} | Tokens], [')' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'[', _} | Tokens], [']' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'{', _} | Tokens], ['}' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'<<', _} | Tokens], ['>>' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -%%% Handle anonymous functions -prune_tokens([{'(', _}, {capture_op, _, _} | Tokens], [], [{'(', _, _} | Terminators]) -> - prune_tokens(Tokens, [], Terminators); +prune_tokens([{'fn', _} | Tokens], ['end' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'do', _} | Tokens], ['end' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'(', _} | Tokens], [')' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'[', _} | Tokens], [']' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'{', _} | Tokens], ['}' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'<<', _} | Tokens], ['>>' | Opener]) -> + prune_tokens(Tokens, Opener); %%% or it is time to stop... -prune_tokens([{';', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'eol', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{',', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'fn', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'do', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'(', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'[', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'{', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'<<', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{identifier, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{block_identifier, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{kw_identifier, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{kw_identifier_safe, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{kw_identifier_unsafe, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{OpType, _, _} | _] = Tokens, [], Terminators) +prune_tokens([{';', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'eol', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{',', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'fn', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'do', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'(', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'[', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'{', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'<<', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{identifier, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{block_identifier, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{kw_identifier, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{kw_identifier_safe, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{kw_identifier_unsafe, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{OpType, _, _} | _] = Tokens, []) when OpType =:= comp_op; OpType =:= at_op; OpType =:= unary_op; OpType =:= and_op; OpType =:= or_op; OpType =:= arrow_op; OpType =:= match_op; OpType =:= in_op; OpType =:= in_match_op; OpType =:= type_op; OpType =:= dual_op; OpType =:= mult_op; OpType =:= power_op; OpType =:= concat_op; OpType =:= range_op; OpType =:= xor_op; OpType =:= pipe_op; OpType =:= stab_op; OpType =:= when_op; OpType =:= assoc_op; OpType =:= rel_op; OpType =:= ternary_op; OpType =:= capture_op; OpType =:= ellipsis_op -> - {Tokens, Terminators}; + Tokens; %%% or we traverse until the end. -prune_tokens([_ | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([], [], Terminators) -> - {[], Terminators}. +prune_tokens([_ | Tokens], Opener) -> + prune_tokens(Tokens, Opener); +prune_tokens([], _Opener) -> + []. From 329442c48173c6b4a29e95374ba13c920850e493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 21 Jan 2025 10:36:00 +0100 Subject: [PATCH 088/128] Provide more AST around cursor fragments, closes #14118 --- lib/elixir/lib/code/fragment.ex | 34 +++++++++++++------ lib/elixir/test/elixir/code_fragment_test.exs | 9 +++-- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 74ab83f594d..567f31348cc 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -1151,7 +1151,7 @@ defmodule Code.Fragment do {rev_tokens, rev_terminators} = with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens, {_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)), - true <- maybe_missing_stab?(rev_tokens), + true <- maybe_missing_stab?(rev_tokens, false), [_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do {[close, open, cursor | rev_tokens], after_fn} else @@ -1165,7 +1165,7 @@ defmodule Code.Fragment do tokens = with {before_start, [_ | _] = after_start} <- Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])), - true <- maybe_missing_stab?(rev_tokens), + true <- maybe_missing_stab?(rev_tokens, true), opts = Keyword.put(opts, :check_terminators, {:cursor, before_start}), {:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <- @@ -1173,6 +1173,14 @@ defmodule Code.Fragment do trailing_tokens = reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start) + # If the cursor has its own line, then we do not trim new lines trailing tokens. + # Otherwise we want to drop any newline so we drop the next tokens after eol. + trailing_tokens = + case rev_tokens do + [_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens + _ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1)) + end + Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0)) else _ -> reverse_tokens(line, column, rev_tokens, rev_terminators) @@ -1196,12 +1204,16 @@ defmodule Code.Fragment do Enum.reverse(tokens, terminators) end + # Otherwise we drop all tokens, trying to build a minimal AST + # for cursor completion. defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens + defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens + defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1) @@ -1220,13 +1232,13 @@ defmodule Code.Fragment do defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter) defp drop_tokens([], 0), do: [] - defp maybe_missing_stab?([{:after, _} | _]), do: true - defp maybe_missing_stab?([{:do, _} | _]), do: true - defp maybe_missing_stab?([{:fn, _} | _]), do: true - defp maybe_missing_stab?([{:else, _} | _]), do: true - defp maybe_missing_stab?([{:catch, _} | _]), do: true - defp maybe_missing_stab?([{:rescue, _} | _]), do: true - defp maybe_missing_stab?([{:stab_op, _, :->} | _]), do: false - defp maybe_missing_stab?([_ | tail]), do: maybe_missing_stab?(tail) - defp maybe_missing_stab?([]), do: false + defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice? + defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?) + defp maybe_missing_stab?([], _stab_choice?), do: false end diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs index 2736937e810..1efa1974724 100644 --- a/lib/elixir/test/elixir/code_fragment_test.exs +++ b/lib/elixir/test/elixir/code_fragment_test.exs @@ -1335,19 +1335,22 @@ defmodule CodeFragmentTest do test "do -> end" do assert cc2q!("if do\nx ->\n", trailing_fragment: "y\nz ->\nw\nend") == - s2q!("if do\nx ->\n__cursor__()\nend") + s2q!("if do\nx ->\n__cursor__()\nz -> \nw\nend") assert cc2q!("if do\nx ->\ny", trailing_fragment: "\nz ->\nw\nend") == - s2q!("if do\nx ->\n__cursor__()\nend") + s2q!("if do\nx ->\n__cursor__()\nz -> \nw\nend") assert cc2q!("if do\nx ->\ny\n", trailing_fragment: "\nz ->\nw\nend") == - s2q!("if do\nx ->\ny\n__cursor__()\nend") + s2q!("if do\nx ->\ny\n__cursor__()\nz -> \nw\nend") assert cc2q!("for x <- [], reduce: %{} do\ny, ", trailing_fragment: "-> :ok\nend") == s2q!("for x <- [], reduce: %{} do\ny, __cursor__() -> :ok\nend") assert cc2q!("for x <- [], reduce: %{} do\ny, z when ", trailing_fragment: "-> :ok\nend") == s2q!("for x <- [], reduce: %{} do\ny, z when __cursor__() -> :ok\nend") + + assert cc2q!("case do\na -> a\nb = ", trailing_fragment: "c -> c\nend") == + s2q!("case do\na -> a\nb = __cursor__() -> c\nend") end test "removes tokens until opening" do From f29e18bca1819e2245fd4660035ff48d2bf5a935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 21 Jan 2025 18:51:54 +0100 Subject: [PATCH 089/128] Do not precompile regexes on Erlang/OTP 28 --- lib/elixir/lib/kernel.ex | 26 +++++++++++++++++--------- lib/ex_unit/lib/ex_unit/doc_test.ex | 4 +--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 5a454e3281c..ed1ec8a9fc4 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6437,29 +6437,37 @@ defmodule Kernel do """ defmacro sigil_r(term, modifiers) - defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do - binary = :elixir_interpolation.unescape_string(string, ®ex_unescape_map/1) - regex = Regex.compile!(binary, :binary.list_to_bin(options)) - Macro.escape(regex) + defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do + binary = :elixir_interpolation.unescape_string(binary, ®ex_unescape_map/1) + compile_regex(binary, options) end defmacro sigil_r({:<<>>, meta, pieces}, options) do - binary = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)} - quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options)))) + tuple = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)} + compile_regex(tuple, options) end defp regex_unescape_map(:newline), do: true defp regex_unescape_map(_), do: false @doc false - defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do + defmacro sigil_R({:<<>>, _meta, [binary]}, options) when is_binary(binary) do IO.warn( "~R/.../ is deprecated, use ~r/.../ instead", Macro.Env.stacktrace(__CALLER__) ) - regex = Regex.compile!(string, :binary.list_to_bin(options)) - Macro.escape(regex) + compile_regex(binary, options) + end + + defp compile_regex(binary_or_tuple, options) do + case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do + true -> + Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options))) + + false -> + quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options)))) + end end @doc ~S""" diff --git a/lib/ex_unit/lib/ex_unit/doc_test.ex b/lib/ex_unit/lib/ex_unit/doc_test.ex index dd354b2e4f8..0b1a04a7bfb 100644 --- a/lib/ex_unit/lib/ex_unit/doc_test.ex +++ b/lib/ex_unit/lib/ex_unit/doc_test.ex @@ -149,8 +149,6 @@ defmodule ExUnit.DocTest do suite run. """ - @opaque_type_regex ~r/#[\w\.]+ """ \nIf you are planning to assert on the result of an iex> expression \ From 2c1a836db32898d2b7a17b0dd28e07425bf46213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 22 Jan 2025 17:33:00 +0100 Subject: [PATCH 090/128] Make --color/--no-color consistent with mix test --- bin/elixir | 6 +++--- bin/elixir.bat | 5 +++-- lib/elixir/lib/kernel/cli.ex | 20 ++++++-------------- lib/mix/lib/mix/tasks/test.ex | 2 +- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/bin/elixir b/bin/elixir index 676a0d21e8f..9c0af9f1b43 100755 --- a/bin/elixir +++ b/bin/elixir @@ -18,7 +18,7 @@ Usage: $(basename "$0") [options] [.exs file] [data] -pz "PATH" Appends the given path to Erlang code path (*) -v, --version Prints Erlang/OTP and Elixir versions (standalone) - --color BOOL Enables or disables ANSI coloring + --color, --no-color Enables or disables ANSI coloring --erl "SWITCHES" Switches to be passed down to Erlang (*) --eval "COMMAND" Evaluates the given command, same as -e (*) --logger-otp-reports BOOL Enables or disables OTP reporting @@ -112,10 +112,10 @@ while [ $I -le $LENGTH ]; do C=1 MODE="iex" ;; - -v|--no-halt) + -v|--no-halt|--color|--no-color) C=1 ;; - -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg|--color) + -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg) C=2 ;; --rpc-eval) diff --git a/bin/elixir.bat b/bin/elixir.bat index ddf06ee1f41..fc9392d8ca0 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -24,7 +24,7 @@ echo -pa "PATH" Prepends the given path to Erlang code path echo -pz "PATH" Appends the given path to Erlang code path (*) echo -v, --version Prints Erlang/OTP and Elixir versions (standalone) echo. -echo --color BOOL Enables or disables ANSI coloring +echo --color, --no-color Enables or disables ANSI coloring echo --erl "SWITCHES" Switches to be passed down to Erlang (*) echo --eval "COMMAND" Evaluates the given command, same as -e (*) echo --logger-otp-reports BOOL Enables or disables OTP reporting @@ -108,10 +108,11 @@ if ""==!par:-pz=! (shift && goto startloop) if ""==!par:-v=! (goto startloop) if ""==!par:--version=! (goto startloop) if ""==!par:--no-halt=! (goto startloop) +if ""==!par:--color=! (goto startloop) +if ""==!par:--no-color=! (goto startloop) if ""==!par:--remsh=! (shift && goto startloop) if ""==!par:--dot-iex=! (shift && goto startloop) if ""==!par:--dbg=! (shift && goto startloop) -if ""==!par:--color=! (shift && goto startloop) rem ******* ERLANG PARAMETERS ********************** if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop) if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index 517a47ce277..c20c5c2d766 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -295,21 +295,13 @@ defmodule Kernel.CLI do parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]}) end - defp parse_argv([~c"--color", value | t], config) do - config = - case value do - ~c"true" -> - Application.put_env(:elixir, :ansi_enabled, true) - config - - ~c"false" -> - Application.put_env(:elixir, :ansi_enabled, false) - config - - _ -> - %{config | errors: ["--color : must be a boolean" | config.errors]} - end + defp parse_argv([~c"--color" | t], config) do + Application.put_env(:elixir, :ansi_enabled, true) + parse_argv(t, config) + end + defp parse_argv([~c"--no-color" | t], config) do + Application.put_env(:elixir, :ansi_enabled, true) parse_argv(t, config) end diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index ec3a0b60c5f..e41bac222ed 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -113,7 +113,7 @@ defmodule Mix.Tasks.Test do and imports (but not local functions). You can press `n` for the next line and `c` for the next test. This automatically sets `--trace` - * `--color` - enables color in the output + * `--color` - enables color in ExUnit formatting results * `--cover` - runs coverage tool. See "Coverage" section below From 175c8243b23c4cfcaaa99e60b030085bfef8e9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 22 Jan 2025 17:38:30 +0100 Subject: [PATCH 091/128] Release v1.18.2 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ VERSION | 2 +- bin/elixir | 2 +- bin/elixir.bat | 2 +- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844bc7b8c12..a2190326ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -225,6 +225,40 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z +## v1.18.2 (2024-01-22) + +### 1. Enhancements + +#### Elixir + + * [CLI] Add `--color`/`--no-color` for enabling and disabling of ANSI colors + * [Code.Fragment] Provide more AST context when invoking `container_cursor_to_quoted` with trailing fragments + * [Regex] Ensure compatibility with Erlang/OTP 28+ new Regex engine + +#### Mix + + * [mix] Print compilation lock waiting message to stderr + * [mix] Add an environment variable to optionally disable compilation locking + +### 2. Bug fixes + +#### Elixir + + * [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang + +#### ExUnit + + * [ExUnit] Fix crash when diffing bitstring specifiers + +#### IEx + + * [IEx.Autocomplete] Fix crashing whhen autocompleting structs with runtime values + +#### Mix + + * [mix] Track compilation locks per user to avoid permission errors + * [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin + ## v1.18.1 (2024-12-24) ### 1. Enhancements diff --git a/VERSION b/VERSION index ec6d649be65..b57fc7228b6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.1 +1.18.2 diff --git a/bin/elixir b/bin/elixir index 9c0af9f1b43..226936a875b 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.18.1 +ELIXIR_VERSION=1.18.2 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index fc9392d8ca0..d9857edc4cd 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @echo off -set ELIXIR_VERSION=1.18.1 +set ELIXIR_VERSION=1.18.2 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation From 7426acb1b9d7f7583e2c37d0b582e7a921bed5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 24 Jan 2025 19:58:24 +0100 Subject: [PATCH 092/128] Use exdoc:loaded to reload mermaid graphs --- lib/elixir/scripts/elixir_docs.exs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/elixir/scripts/elixir_docs.exs b/lib/elixir/scripts/elixir_docs.exs index 11086e20a38..0fe4c3f3f8e 100644 --- a/lib/elixir/scripts/elixir_docs.exs +++ b/lib/elixir/scripts/elixir_docs.exs @@ -191,12 +191,19 @@ canonical = System.fetch_env!("CANONICAL") before_closing_body_tag: fn :html -> """ + - """ _ -> From 8e61baacabb0293fb5bdbd496a203f4415cc5db1 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sat, 25 Jan 2025 07:11:26 +0900 Subject: [PATCH 093/128] Fix String.split/3 example in guide and add note in docs (#14223) --- lib/elixir/lib/string.ex | 9 +++++ .../getting-started/keywords-and-maps.md | 34 +++++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index e8b3a61c8ca..12007e58fb3 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -488,6 +488,15 @@ defmodule String do iex> String.split(String.normalize("é", :nfc), "e") ["é"] + When using both the `:trim` and the `:parts` option, the empty values + are removed as the parts are computed (if any). No trimming happens + after all parts are computed: + + iex> String.split(" a b c ", " ", trim: true, parts: 2) + ["a", " b c "] + iex> String.split(" a b c ", " ", trim: true, parts: 3) + ["a", "b", " c "] + """ @spec split(t, pattern | Regex.t(), keyword) :: [t] def split(string, pattern, options \\ []) diff --git a/lib/elixir/pages/getting-started/keywords-and-maps.md b/lib/elixir/pages/getting-started/keywords-and-maps.md index 734597eac5f..51cb2ad7120 100644 --- a/lib/elixir/pages/getting-started/keywords-and-maps.md +++ b/lib/elixir/pages/getting-started/keywords-and-maps.md @@ -11,36 +11,40 @@ Keyword lists are a data-structure used to pass options to functions. Let's see Imagine you want to split a string of numbers. Initially, we can invoke `String.split/2` passing two strings as arguments: ```elixir -iex> String.split("1 2 3", " ") -["1", "2", "3"] +iex> String.split("1 2 3 4", " ") +["1", "2", "3", "4"] ``` -However, what happens if there is an additional space between the numbers: +What if you only want to split at most 2 times? The `String.split/3` function allows the `parts` option to be set to the maximum number of entries in the result: ```elixir -iex> String.split("1 2 3", " ") -["1", "", "2", "", "3"] +iex> String.split("1 2 3 4", " ", [parts: 3]) +["1", "2", "3 4"] ``` -As you can see, there are now empty strings in our results. Luckily, the `String.split/3` function allows the `trim` option to be set to true: +As you can see, we got 3 parts, the last one containing the remaining of the input without splitting it. + +Now imagine that some of the inputs you must split on contains additional spaces between the numbers: ```elixir -iex> String.split("1 2 3", " ", [trim: true]) -["1", "2", "3"] +iex> String.split("1 2 3 4", " ", [parts: 3]) +["1", "", "2 3 4"] ``` -We can also use options to limit the splitting algorithm to a maximum number of parts, as shown next: +As you can see, the additional spaces lead to empty entries in the output. Luckily, we can also set the `trim` option to `true` to remove them: ```elixir -iex> String.split("1 2 3", " ", [trim: true, parts: 2]) -["1", "2 3"] +iex> String.split("1 2 3 4", " ", [parts: 3, trim: true]) +["1", "2", " 3 4"] ``` -`[trim: true]` and `[trim: true, parts: 2]` are keyword lists. When a keyword list is the last argument of a function, we can skip the brackets and write: +Once again we got 3 parts, with the last one containing the leftovers. + +`[parts: 3]` and `[parts: 3, trim: true]` are keyword lists. When a keyword list is the last argument of a function, we can skip the brackets and write: ```elixir -iex> String.split("1 2 3", " ", trim: true, parts: 2) -["1", "2 3"] +iex> String.split("1 2 3 4", " ", parts: 3, trim: true) +["1", "2", " 3 4"] ``` As shown in the example above, keyword lists are mostly used as optional arguments to functions. @@ -48,7 +52,7 @@ As shown in the example above, keyword lists are mostly used as optional argumen As the name implies, keyword lists are simply lists. In particular, they are lists consisting of 2-item tuples where the first element (the key) is an atom and the second element can be any value. Both representations are the same: ```elixir -iex> [{:trim, true}, {:parts, 2}] == [trim: true, parts: 2] +iex> [{:parts, 3}, {:trim, true}] == [parts: 3, trim: true] true ``` From 55c05d943e9eee57356aaf5217f1133cbacd2237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tea=E2=98=86?= Date: Sun, 26 Jan 2025 14:10:03 +0100 Subject: [PATCH 094/128] Fix `--no-color` not setting `:ansi_enabled` to false (#14229) --- lib/elixir/lib/kernel/cli.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index c20c5c2d766..59d91e1d0bf 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -301,7 +301,7 @@ defmodule Kernel.CLI do end defp parse_argv([~c"--no-color" | t], config) do - Application.put_env(:elixir, :ansi_enabled, true) + Application.put_env(:elixir, :ansi_enabled, false) parse_argv(t, config) end From 4b09a836e011cc4e553cd6dd9a043565b8fcb152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 28 Jan 2025 10:32:10 +0100 Subject: [PATCH 095/128] Validate signature for forward compatibility (#14235) --- lib/elixir/lib/module/parallel_checker.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 5df93d783bf..a22076997cb 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -439,10 +439,16 @@ defmodule Module.ParallelChecker do defp cache_chunk(table, module, exports) do Enum.each(exports, fn {{fun, arity}, info} -> - # TODO: Match on signature directly in Elixir v1.22+ + sig = + case info do + %{sig: {:strong, _, _} = sig} -> sig + %{sig: {:infer, _} = sig} -> sig + _ -> :none + end + :ets.insert( table, - {{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)} + {{module, {fun, arity}}, Map.get(info, :deprecated), sig} ) end) From 120467f83c020af650f3648fc91689be649b3157 Mon Sep 17 00:00:00 2001 From: cevado Date: Wed, 29 Jan 2025 04:56:49 -0300 Subject: [PATCH 096/128] Add section to Process.exit/2 with differences to Kernel.exit/1 (#14238) --- lib/elixir/lib/process.ex | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/elixir/lib/process.ex b/lib/elixir/lib/process.ex index 43299a1b680..ef1c8c46a7f 100644 --- a/lib/elixir/lib/process.ex +++ b/lib/elixir/lib/process.ex @@ -219,6 +219,16 @@ defmodule Process do Inlined by the compiler. + > #### Differences to `Kernel.exit/1` {: .info } + > + > The functions `Kernel.exit/1` and `Process.exit/2` are + > named similarly but provide very different functionalities. The + > `Kernel:exit/1` function should be used when the intent is to stop the current + > process while `Process.exit/2` should be used when the intent is to send an + > exit signal to another process. Note also that `Kernel.exit/1` can be caught + > with `try/1` while `Process.exit/2` can only be handled by trapping exits and + > when the signal is different than `:kill`. + ## Examples Process.exit(pid, :kill) From b615c8435abf4a2af71c9de93222ce47a5d82bfa Mon Sep 17 00:00:00 2001 From: Ben Murden Date: Wed, 29 Jan 2025 16:59:17 +0900 Subject: [PATCH 097/128] Improved help on deprecation warning (#14239) --- lib/mix/lib/mix/tasks/cmd.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/cmd.ex b/lib/mix/lib/mix/tasks/cmd.ex index cfe43466db2..b4e3001e527 100644 --- a/lib/mix/lib/mix/tasks/cmd.ex +++ b/lib/mix/lib/mix/tasks/cmd.ex @@ -63,7 +63,7 @@ defmodule Mix.Tasks.Cmd do |> Enum.map(&String.to_atom/1) if apps != [] do - IO.warn("the --app in mix cmd is deprecated") + IO.warn("the --app in mix cmd is deprecated. Use mix do --app instead.") end if apps == [] or Mix.Project.config()[:app] in apps do From 6ecb43061476c0870e24899a23ce8921835920d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 12 Feb 2025 10:02:54 +0100 Subject: [PATCH 098/128] Do not purge on recompile if IEx is not running, closes #14260 --- lib/iex/lib/iex/mix_listener.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/iex/lib/iex/mix_listener.ex b/lib/iex/lib/iex/mix_listener.ex index 57f2e0f7f17..ccb194149d3 100644 --- a/lib/iex/lib/iex/mix_listener.ex +++ b/lib/iex/lib/iex/mix_listener.ex @@ -12,10 +12,18 @@ defmodule IEx.MixListener do @doc """ Unloads all modules invalidated by external compilations. + + Returns `:noop` if there is no module to purge or if + the listener is not running (it may happen when connecting + via --remsh to a node that was started without IEx). """ @spec purge :: :ok | :noop def purge do - GenServer.call(@name, :purge, :infinity) + if Process.whereis(@name) do + GenServer.call(@name, :purge, :infinity) + else + :noop + end end @impl true From b823f9efdfd5ee950264b93257a7517dfb7b2b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 30 Jan 2025 10:21:32 +0100 Subject: [PATCH 099/128] Allow <<_::3*8>> in typespecs --- lib/elixir/lib/code/typespec.ex | 7 +++++-- lib/elixir/lib/kernel/typespec.ex | 18 ++++++++++++++++++ lib/elixir/test/elixir/typespec_test.exs | 12 ++++++------ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/code/typespec.ex b/lib/elixir/lib/code/typespec.ex index 30f0789a479..3efa7be86e0 100644 --- a/lib/elixir/lib/code/typespec.ex +++ b/lib/elixir/lib/code/typespec.ex @@ -283,7 +283,6 @@ defmodule Code.Typespec do end defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do - [arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg) line = meta(anno)[:line] case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do @@ -329,10 +328,14 @@ defmodule Code.Typespec do {erl_to_ex_var(var), meta(anno), nil} end - defp typespec_to_quoted({:op, anno, op, arg}) do + defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do {op, meta(anno), [typespec_to_quoted(arg)]} end + defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do + {:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]} + end + defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do remote_type(anno, mod, name, args) end diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex index 023c86f9fac..149a302e95f 100644 --- a/lib/elixir/lib/kernel/typespec.ex +++ b/lib/elixir/lib/kernel/typespec.ex @@ -493,6 +493,24 @@ defmodule Kernel.Typespec do state} end + defp typespec( + {:<<>>, meta, [{:"::", _, [{:_, _, ctx1}, {:*, prod_meta, [size, unit]}]}]}, + _, + _, + state + ) + when is_atom(ctx1) and is_integer(size) and size >= 0 and unit in 1..256 do + location = location(meta) + prod_location = location(prod_meta) + + {{:type, location, :binary, + [ + {:op, prod_location, :*, {:integer, prod_location, size}, + {:integer, prod_location, unit}}, + {:integer, location, 0} + ]}, state} + end + defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state) when is_atom(ctx) and is_integer(size) and size >= 0 do location = location(meta) diff --git a/lib/elixir/test/elixir/typespec_test.exs b/lib/elixir/test/elixir/typespec_test.exs index 7cf5e714dc3..0658f2e0f28 100644 --- a/lib/elixir/test/elixir/typespec_test.exs +++ b/lib/elixir/test/elixir/typespec_test.exs @@ -393,6 +393,7 @@ defmodule TypespecTest do @type size :: <<_::3>> @type unit :: <<_::_*8>> @type size_and_unit :: <<_::3, _::_*8>> + @type size_prod_unit :: <<_::3*8>> end assert [ @@ -401,17 +402,15 @@ defmodule TypespecTest do type: {:size, {:type, _, :binary, [{:integer, _, 3}, {:integer, _, 0}]}, []}, type: {:size_and_unit, {:type, _, :binary, [{:integer, _, 3}, {:integer, _, 8}]}, []}, + type: + {:size_prod_unit, + {:type, _, :binary, + [{:op, _, :*, {:integer, _, 3}, {:integer, _, 8}}, {:integer, _, 0}]}, []}, type: {:unit, {:type, _, :binary, [{:integer, _, 0}, {:integer, _, 8}]}, []} ] = types(bytecode) end test "@type with invalid binary spec" do - assert_raise Kernel.TypespecError, ~r"invalid binary specification", fn -> - test_module do - @type my_type :: <<_::3*8>> - end - end - assert_raise Kernel.TypespecError, ~r"invalid binary specification", fn -> test_module do @type my_type :: <<_::atom()>> @@ -1217,6 +1216,7 @@ defmodule TypespecTest do quote(do: @type(binary_type1() :: <<_::_*8>>)), quote(do: @type(binary_type2() :: <<_::3>>)), quote(do: @type(binary_type3() :: <<_::3, _::_*8>>)), + quote(do: @type(binary_type4() :: <<_::3*8>>)), quote(do: @type(tuple_type() :: {integer()})), quote(do: @type(ftype() :: (-> any()) | (-> integer()) | (integer() -> integer()))), quote(do: @type(cl() :: charlist())), From d7478095e0524bffb3f058c67fe66aaedbe00245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 20 Feb 2025 18:10:23 +0100 Subject: [PATCH 100/128] Support Erlang/OTP 28 --- lib/elixir/lib/regex.ex | 15 ++-- lib/elixir/src/elixir_erl.erl | 7 +- .../fixtures/dialyzer/protocol_opaque.ex | 29 -------- .../test/elixir/kernel/dialyzer_test.exs | 17 ----- lib/elixir/test/elixir/option_parser_test.exs | 5 +- lib/elixir/test/elixir/regex_test.exs | 70 ------------------- lib/ex_unit/test/ex_unit/assertions_test.exs | 4 +- lib/iex/test/iex/helpers_test.exs | 2 +- 8 files changed, 17 insertions(+), 132 deletions(-) delete mode 100644 lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index b6cfbc0e23c..6e363325c27 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -209,17 +209,14 @@ defmodule Regex do ## Examples - iex> Regex.compile("foo") - {:ok, ~r/foo/} + Regex.compile("foo") + #=> {:ok, ~r/foo/} - iex> Regex.compile("*foo") - {:error, {~c"nothing to repeat", 0}} + Regex.compile("foo", "i") + #=>{:ok, ~r/foo/i} - iex> Regex.compile("foo", "i") - {:ok, ~r/foo/i} - - iex> Regex.compile("foo", [:caseless]) - {:ok, Regex.compile!("foo", [:caseless])} + Regex.compile("*foo") + #=> {:error, {~c"nothing to repeat", 0}} """ @spec compile(binary, binary | [term]) :: {:ok, t} | {:error, term} diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 17ed4d9f0ef..be7339147d0 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -165,7 +165,12 @@ dynamic_form(#{module := Module, relative_file := RelativeFile, {Def, Defmacro, Macros, Exports, Functions} = split_definition(Definitions, Unreachable, Line, [], [], [], [], {[], []}), - FilteredOpts = lists:filter(fun({no_warn_undefined, _}) -> false; (_) -> true end, Opts), + FilteredOpts = lists:filter(fun( + {no_warn_undefined, _}) -> false; + (debug_info) -> false; + (_) -> true + end, Opts), + Location = {elixir_utils:characters_to_list(RelativeFile), Line}, Prefix = [{attribute, Line, file, Location}, diff --git a/lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex b/lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex deleted file mode 100644 index d6a00f31229..00000000000 --- a/lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Dialyzer.ProtocolOpaque do - def circus() do - duck = Dialyzer.ProtocolOpaque.Duck.new() - Dialyzer.ProtocolOpaque.Entity.speak(duck) - end -end - -defprotocol Dialyzer.ProtocolOpaque.Entity do - @fallback_to_any true - def speak(entity) -end - -defmodule Dialyzer.ProtocolOpaque.Duck do - @opaque t :: %__MODULE__{feathers: :white_and_grey} - defstruct feathers: :white_and_grey - - @spec new :: t - def new(), do: %__MODULE__{} - - defimpl Dialyzer.ProtocolOpaque.Entity do - def speak(%Dialyzer.ProtocolOpaque.Duck{}), do: "Quack!" - end -end - -defimpl Dialyzer.ProtocolOpaque.Entity, for: Any do - def speak(_any) do - "I can be anything" - end -end diff --git a/lib/elixir/test/elixir/kernel/dialyzer_test.exs b/lib/elixir/test/elixir/kernel/dialyzer_test.exs index d055e71c618..868127c264f 100644 --- a/lib/elixir/test/elixir/kernel/dialyzer_test.exs +++ b/lib/elixir/test/elixir/kernel/dialyzer_test.exs @@ -119,23 +119,6 @@ defmodule Kernel.DialyzerTest do assert_dialyze_no_warnings!(context) end - @tag warnings: [:specdiffs] - test "no warnings on protocol calls with opaque types", context do - alias Dialyzer.ProtocolOpaque - - copy_beam!(context, ProtocolOpaque) - copy_beam!(context, ProtocolOpaque.Entity) - copy_beam!(context, ProtocolOpaque.Entity.Any) - copy_beam!(context, ProtocolOpaque.Duck) - assert_dialyze_no_warnings!(context) - - # Also ensure no warnings after consolidation. - Code.prepend_path(context.base_dir) - {:ok, binary} = Protocol.consolidate(ProtocolOpaque.Entity, [ProtocolOpaque.Duck, Any]) - File.write!(Path.join(context.outdir, "#{ProtocolOpaque.Entity}.beam"), binary) - assert_dialyze_no_warnings!(context) - end - test "no warnings on and/2 and or/2", context do copy_beam!(context, Dialyzer.BooleanCheck) assert_dialyze_no_warnings!(context) diff --git a/lib/elixir/test/elixir/option_parser_test.exs b/lib/elixir/test/elixir/option_parser_test.exs index 39df43bdcb0..f2cab66a0d1 100644 --- a/lib/elixir/test/elixir/option_parser_test.exs +++ b/lib/elixir/test/elixir/option_parser_test.exs @@ -428,10 +428,9 @@ end defmodule OptionsParserDeprecationsTest do use ExUnit.Case, async: true - @warning ~r[not passing the :switches or :strict option to OptionParser is deprecated] - def assert_deprecated(fun) do - assert ExUnit.CaptureIO.capture_io(:stderr, fun) =~ @warning + assert ExUnit.CaptureIO.capture_io(:stderr, fun) =~ + "not passing the :switches or :strict option to OptionParser is deprecated" end test "parses boolean option" do diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index 9b72e967dbe..917df6d2603 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -3,39 +3,6 @@ Code.require_file("test_helper.exs", __DIR__) defmodule RegexTest do use ExUnit.Case, async: true - @re_21_3_little %Regex{ - re_pattern: - {:re_pattern, 1, 0, 0, - <<69, 82, 67, 80, 94, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, - 255, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 131, 0, 20, 29, 99, 133, - 0, 7, 0, 1, 29, 100, 119, 0, 5, 29, 101, 120, 0, 12, 120, 0, 20, 0>>}, - re_version: {"8.42 2018-03-20", :little}, - source: "c(?d|e)" - } - - @re_21_3_big %Regex{ - re_pattern: - {:re_pattern, 1, 0, 0, - <<80, 67, 82, 69, 0, 0, 0, 86, 0, 0, 0, 0, 0, 0, 0, 17, 255, 255, 255, 255, 255, 255, 255, - 255, 0, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 56, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 131, 0, 20, 29, 99, 133, 0, 7, 0, 1, 29, 100, 119, - 0, 5, 29, 101, 120, 0, 12, 120, 0, 20, 0>>}, - re_version: {"8.42 2018-03-20", :big}, - source: "c(?d|e)" - } - - @re_19_3_little %Regex{ - re_pattern: - {:re_pattern, 1, 0, 0, - <<69, 82, 67, 80, 94, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, - 255, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 125, 0, 20, 29, 99, 127, - 0, 7, 0, 1, 29, 100, 113, 0, 5, 29, 101, 114, 0, 12, 114, 0, 20, 0>>}, - re_version: {"8.33 2013-05-29", :little}, - source: "c(?d|e)" - } - doctest Regex test "multiline" do @@ -68,16 +35,9 @@ defmodule RegexTest do test "literal source" do assert Regex.source(Regex.compile!("foo")) == "foo" assert Regex.source(~r"foo") == "foo" - assert Regex.re_pattern(Regex.compile!("foo")) == Regex.re_pattern(~r"foo") assert Regex.source(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) == "\a\b\d\e\f\n\r\s\t\v" assert Regex.source(~r<\a\b\d\e\f\n\r\s\t\v>) == "\\a\\b\\d\\e\\f\\n\\r\\s\\t\\v" - - assert Regex.re_pattern(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) == - Regex.re_pattern(~r"\x07\x08\x7F\x1B\x0C\x0A\x0D\x20\x09\x0B") - - assert Regex.re_pattern(Regex.compile!("\\a\\b\\d\e\f\\n\\r\\s\\t\\v")) == - Regex.re_pattern(~r"\a\b\d\e\f\n\r\s\t\v") end test "Unicode" do @@ -116,16 +76,6 @@ defmodule RegexTest do end end - test "recompile/1" do - new_regex = ~r/foo/ - {:ok, %Regex{}} = Regex.recompile(new_regex) - assert %Regex{} = Regex.recompile!(new_regex) - - old_regex = Map.delete(~r/foo/, :re_version) - {:ok, %Regex{}} = Regex.recompile(old_regex) - assert %Regex{} = Regex.recompile!(old_regex) - end - test "opts/1" do assert Regex.opts(Regex.compile!("foo", "i")) == [:caseless] assert Regex.opts(Regex.compile!("foo", [:ucp])) == [:ucp] @@ -179,16 +129,6 @@ defmodule RegexTest do assert Regex.run(~r"bar", "foobar", offset: 2, return: :index) == [{3, 3}] end - test "run/3 with regexes compiled in different systems" do - assert Regex.run(@re_21_3_little, "abcd abce", capture: :all_names) == ["d"] - assert Regex.run(@re_21_3_big, "abcd abce", capture: :all_names) == ["d"] - assert Regex.run(@re_19_3_little, "abcd abce", capture: :all_names) == ["d"] - end - - test "run/3 with regexes with options compiled in different systems" do - assert Regex.run(%{~r/foo/i | re_version: "bad version"}, "FOO") == ["FOO"] - end - test "scan/2" do assert Regex.scan(~r"c(d|e)", "abcd abce") == [["cd", "d"], ["ce", "e"]] assert Regex.scan(~r"c(?:d|e)", "abcd abce") == [["cd"], ["ce"]] @@ -207,16 +147,6 @@ defmodule RegexTest do assert Regex.scan(~r"^foo", "foobar", offset: 1) == [] end - test "scan/2 with regexes compiled in different systems" do - assert Regex.scan(@re_21_3_little, "abcd abce", capture: :all_names) == [["d"], ["e"]] - assert Regex.scan(@re_21_3_big, "abcd abce", capture: :all_names) == [["d"], ["e"]] - assert Regex.scan(@re_19_3_little, "abcd abce", capture: :all_names) == [["d"], ["e"]] - end - - test "scan/2 with regexes with options compiled in different systems" do - assert Regex.scan(%{~r/foo/i | re_version: "bad version"}, "FOO") == [["FOO"]] - end - test "split/2,3" do assert Regex.split(~r",", "") == [""] assert Regex.split(~r",", "", trim: true) == [] diff --git a/lib/ex_unit/test/ex_unit/assertions_test.exs b/lib/ex_unit/test/ex_unit/assertions_test.exs index e2094f44535..8a0d7107993 100644 --- a/lib/ex_unit/test/ex_unit/assertions_test.exs +++ b/lib/ex_unit/test/ex_unit/assertions_test.exs @@ -707,7 +707,7 @@ defmodule ExUnit.AssertionsTest do rescue error in [ExUnit.AssertionError] -> "foo" = error.left - ~r{a} = error.right + %Regex{} = error.right end end @@ -722,7 +722,7 @@ defmodule ExUnit.AssertionsTest do rescue error in [ExUnit.AssertionError] -> "foo" = error.left - ~r"o" = error.right + %Regex{} = error.right end end diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 1af56114bae..490c97cb5bd 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -349,7 +349,7 @@ defmodule IEx.HelpersTest do assert captured =~ "-spec sleep(Time) -> ok when Time :: timeout()." else assert captured =~ "sleep(Time)" - assert captured =~ "@spec sleep(time) :: :ok when time: timeout()" + assert captured =~ "@spec sleep(time) :: :ok when time: " end end From 930ec697398517f2b255c5660adf30ac2689ca33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 21 Feb 2025 11:01:36 +0100 Subject: [PATCH 101/128] Support --no-listeners in loadpaths --- lib/mix/lib/mix/tasks/deps.loadpaths.ex | 5 ++++- lib/mix/lib/mix/tasks/loadpaths.ex | 1 + lib/mix/test/mix/tasks/compile_test.exs | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/deps.loadpaths.ex b/lib/mix/lib/mix/tasks/deps.loadpaths.ex index d7daa070738..b8bba13e019 100644 --- a/lib/mix/lib/mix/tasks/deps.loadpaths.ex +++ b/lib/mix/lib/mix/tasks/deps.loadpaths.ex @@ -23,6 +23,7 @@ defmodule Mix.Tasks.Deps.Loadpaths do * `--no-compile` - does not compile even if files require compilation * `--no-deps-check` - does not check or compile deps, only load available ones * `--no-elixir-version-check` - does not check Elixir version + * `--no-listeners` - does not start Mix listeners * `--no-optional-deps` - does not compile or load optional deps """ @@ -71,7 +72,9 @@ defmodule Mix.Tasks.Deps.Loadpaths do # For now we only allow listeners defined in dependencies, so # we start them right after adding adding deps to the path - Mix.PubSub.start_listeners() + if "--no-listeners" not in args do + Mix.PubSub.start_listeners() + end :ok end diff --git a/lib/mix/lib/mix/tasks/loadpaths.ex b/lib/mix/lib/mix/tasks/loadpaths.ex index fffe930f5fc..5d0cc2bc4d8 100644 --- a/lib/mix/lib/mix/tasks/loadpaths.ex +++ b/lib/mix/lib/mix/tasks/loadpaths.ex @@ -21,6 +21,7 @@ defmodule Mix.Tasks.Loadpaths do * `--no-compile` - does not compile dependencies, only check and load them * `--no-deps-check` - does not check dependencies, only load available ones * `--no-elixir-version-check` - does not check Elixir version + * `--no-listeners` - does not start Mix listeners * `--no-optional-deps` - does not compile or load optional deps """ diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 2ecf29cbeb7..088887403d1 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -389,6 +389,10 @@ defmodule Mix.Tasks.CompileTest do File.write!("src/b.erl", "-module(b).") File.write!("src/c.erl", "-module(c).") + # Ensure we can boot with compilation and listeners if desired + assert mix(["loadpaths", "--no-compile", "--no-listeners"]) == "" + + # Now setup dependencies mix(["deps.compile"]) parent = self() From 69990a5d1d3cd00ed0422a87b49841c27e16ff15 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 21 Feb 2025 23:08:36 +0900 Subject: [PATCH 102/128] Fix regression when diffing nested improper lists (#14292) Close https://github.com/elixir-lang/elixir/issues/14291 --- lib/ex_unit/lib/ex_unit/diff.ex | 1 + lib/ex_unit/test/ex_unit/diff_test.exs | 2 ++ lib/ex_unit/test/ex_unit/formatter_test.exs | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/lib/ex_unit/lib/ex_unit/diff.ex b/lib/ex_unit/lib/ex_unit/diff.ex index 170e988c35a..93fe0113727 100644 --- a/lib/ex_unit/lib/ex_unit/diff.ex +++ b/lib/ex_unit/lib/ex_unit/diff.ex @@ -1156,6 +1156,7 @@ defmodule ExUnit.Diff do else other |> Map.to_list() + |> Enum.map(&escape_pair/1) |> build_map_or_struct(struct) end end diff --git a/lib/ex_unit/test/ex_unit/diff_test.exs b/lib/ex_unit/test/ex_unit/diff_test.exs index a2317cf73e2..87474a2324c 100644 --- a/lib/ex_unit/test/ex_unit/diff_test.exs +++ b/lib/ex_unit/test/ex_unit/diff_test.exs @@ -330,6 +330,8 @@ defmodule ExUnit.DiffTest do "[[[[], \"Hello-,- \"] | \"world\"] | \"!\"]", "[[[[], \"Hello \"] | \"world\"] | \"!\"]" ) + + refute_diff(:foo = %{bar: [:a | :b]}, "", "") end test "proper lists" do diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index a1e5506c1f6..6d115da9bc0 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -503,6 +503,17 @@ defmodule ExUnit.FormatterTest do """ end + test "formats assertions with nested improper list diffing" do + failure = [{:error, catch_assertion(assert :foo = %{bar: [1 | 2]}), []}] + + assert format_test_all_failure(test_module(), failure, 1, :infinity, &diff_formatter/2) =~ """ + match (=) failed + code: assert :foo = %{bar: [1 | 2]} + left: :foo + right: %{bar: [1 | 2]} + """ + end + defmodule BadInspect do defstruct key: 0 From d51153b56e506d319ff804210bf2e0f1db053080 Mon Sep 17 00:00:00 2001 From: Graham Preston Date: Wed, 26 Feb 2025 17:57:17 +0100 Subject: [PATCH 103/128] Fix typo (#14302) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2190326ee0..f72b17c8bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -252,7 +252,7 @@ You may also prefer to write using guards: #### IEx - * [IEx.Autocomplete] Fix crashing whhen autocompleting structs with runtime values + * [IEx.Autocomplete] Fix crashing when autocompleting structs with runtime values #### Mix From d89fabe5e4466dde69182c8000d9250af765983b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 27 Feb 2025 07:59:57 +0100 Subject: [PATCH 104/128] Fix date in CHANGELOG, closes #14303 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f72b17c8bca..ab679fd0c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -225,7 +225,7 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z -## v1.18.2 (2024-01-22) +## v1.18.2 (2025-01-22) ### 1. Enhancements From c63aeb9f77339f8c181d810984b8d64a5f017bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 1 Mar 2025 09:53:15 +0100 Subject: [PATCH 105/128] Clarify order on uniq/uniq_by, closes #14304 --- lib/elixir/lib/enum.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index d2181d42e7f..575c1b16c6c 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -3837,6 +3837,9 @@ defmodule Enum do @doc """ Enumerates the `enumerable`, removing all duplicate elements. + The first occurrence of each element is kept and all following + duplicates are removed. The overall order is preserved. + ## Examples iex> Enum.uniq([1, 2, 3, 3, 2, 1]) @@ -3862,7 +3865,8 @@ defmodule Enum do considered duplicates if the return value of `fun` is equal for both of them. - The first occurrence of each element is kept. + The first occurrence of each element is kept and all following + duplicates are removed. The overall order is preserved. ## Example From 8493f1934a820d9ccbe9c6fc4ad74a6d8c3c2ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 3 Mar 2025 12:54:49 +0100 Subject: [PATCH 106/128] Do not raise when Stream.cycle is explicitly halted, closes #14307 --- lib/elixir/lib/stream.ex | 2 +- lib/elixir/test/elixir/enum_test.exs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/stream.ex b/lib/elixir/lib/stream.ex index 495edcf12bd..a69e7f5d3b9 100644 --- a/lib/elixir/lib/stream.ex +++ b/lib/elixir/lib/stream.ex @@ -1444,7 +1444,7 @@ defmodule Stream do defp check_cycle_first_element(reduce) do fn acc -> case reduce.(acc) do - {state, []} when state in [:done, :halted] -> + {state, []} when state in [:done, :halted] and elem(acc, 0) != :halt -> raise ArgumentError, "cannot cycle over an empty enumerable" other -> diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 483849c4f22..e19c21381bf 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -1517,6 +1517,18 @@ defmodule EnumTest do assert Enum.zip([], []) == [] end + test "zip/2 with infinite streams" do + assert Enum.zip([], Stream.cycle([1, 2])) == [] + assert Enum.zip([], Stream.cycle(1..2)) == [] + assert Enum.zip(.., Stream.cycle([1, 2])) == [] + assert Enum.zip(.., Stream.cycle(1..2)) == [] + + assert Enum.zip(Stream.cycle([1, 2]), ..) == [] + assert Enum.zip(Stream.cycle(1..2), ..) == [] + assert Enum.zip(Stream.cycle([1, 2]), ..) == [] + assert Enum.zip(Stream.cycle(1..2), ..) == [] + end + test "zip/1" do assert Enum.zip([[:a, :b], [1, 2], ["foo", "bar"]]) == [{:a, 1, "foo"}, {:b, 2, "bar"}] From 25ab64850224229a497da8b331948f2eec1ca1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Wed, 5 Mar 2025 09:44:11 +0100 Subject: [PATCH 107/128] Fix autocomplete crash when expanding struct with `__MODULE__` (#14308) --- lib/iex/lib/iex/autocomplete.ex | 8 +++++--- lib/iex/test/iex/autocomplete_test.exs | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex index 531b129e661..5dbd9992690 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -378,7 +378,8 @@ defmodule IEx.Autocomplete do case Code.Fragment.container_cursor_to_quoted(code) do {:ok, quoted} -> case Macro.path(quoted, &match?({:__cursor__, _, []}, &1)) do - [cursor, {:%{}, _, pairs}, {:%, _, [{:__aliases__, _, aliases}, _map]} | _] -> + [cursor, {:%{}, _, pairs}, {:%, _, [{:__aliases__, _, aliases = [h | _]}, _map]} | _] + when is_atom(h) -> container_context_struct(cursor, pairs, aliases, shell) [ @@ -386,8 +387,9 @@ defmodule IEx.Autocomplete do pairs, {:|, _, _}, {:%{}, _, _}, - {:%, _, [{:__aliases__, _, aliases}, _map]} | _ - ] -> + {:%, _, [{:__aliases__, _, aliases = [h | _]}, _map]} | _ + ] + when is_atom(h) -> container_context_struct(cursor, pairs, aliases, shell) [cursor, pairs, {:|, _, [{variable, _, nil} | _]}, {:%{}, _, _} | _] -> diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index c9d7747091e..e4d14196306 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -416,6 +416,9 @@ defmodule IEx.AutocompleteTest do assert {:yes, ~c"ry: ", []} = expand(~c"%URI{path: \"foo\", que") assert {:no, [], []} = expand(~c"%URI{path: \"foo\", unkno") assert {:no, [], []} = expand(~c"%Unknown{path: \"foo\", unkno") + + assert {:yes, [], _} = expand(~c"%__MODULE__{") + assert {:yes, [], _} = expand(~c"%__MODULE__.Some{") end test "completion for struct keys in update syntax" do From 75677b9dabf4e9f1f45aa6fe328e5ca1328dd35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 5 Mar 2025 09:47:33 +0100 Subject: [PATCH 108/128] Encode any JSON key to string, closes #14305 (#14309) --- lib/elixir/lib/json.ex | 46 ++++++++++++++++++++-------- lib/elixir/test/elixir/json_test.exs | 8 ++--- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex index ec602bf1b7f..0cd9285c44f 100644 --- a/lib/elixir/lib/json.ex +++ b/lib/elixir/lib/json.ex @@ -146,8 +146,30 @@ end defimpl JSON.Encoder, for: Map do def encode(value, encoder) do - :elixir_json.encode_map(value, encoder) + case :maps.next(:maps.iterator(value)) do + :none -> + "{}" + + {key, value, iterator} -> + [?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)] + end end + + defp next(iterator, encoder) do + case :maps.next(iterator) do + :none -> + "}" + + {key, value, iterator} -> + [?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)] + end + end + + # Erlang supports only numbers, binaries, and atoms as keys, + # we support anything that implements the String.Chars protocol. + defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder) + defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder) + defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder) end defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do @@ -175,17 +197,15 @@ defmodule JSON do Elixir built-in data structures are encoded to JSON as follows: - | **Elixir** | **JSON** | - |------------------------|----------| - | `integer() \| float()` | Number | - | `true \| false ` | Boolean | - | `nil` | Null | - | `binary()` | String | - | `atom()` | String | - | `list()` | Array | - | `%{binary() => _}` | Object | - | `%{atom() => _}` | Object | - | `%{integer() => _}` | Object | + | **Elixir** | **JSON** | + |----------------------------|----------| + | `integer() \| float()` | Number | + | `true \| false ` | Boolean | + | `nil` | Null | + | `binary()` | String | + | `atom()` | String | + | `list()` | Array | + | `%{String.Chars.t() => _}` | Object | You may also implement the `JSON.Encoder` protocol for custom data structures. @@ -403,7 +423,7 @@ defmodule JSON do do: :elixir_json.encode_list(value, encoder) def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__), - do: :elixir_json.encode_map(value, encoder) + do: JSON.Encoder.Map.encode(value, encoder) def protocol_encode(value, encoder), do: JSON.Encoder.encode(value, encoder) diff --git a/lib/elixir/test/elixir/json_test.exs b/lib/elixir/test/elixir/json_test.exs index 5bbc56243f1..6411db2fbe0 100644 --- a/lib/elixir/test/elixir/json_test.exs +++ b/lib/elixir/test/elixir/json_test.exs @@ -33,8 +33,8 @@ defmodule JSONTest do end test "maps" do - assert JSON.encode!(%{1 => 2, 3.0 => 4.0, key: :bar}) == - "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\"}" + assert JSON.encode!(%{1 => 2, 3.0 => 4.0, ~c"list" => ~c"list", key: :bar}) == + "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\",\"list\":[108,105,115,116]}" end test "lists" do @@ -80,8 +80,8 @@ defmodule JSONTest do end test "maps" do - assert protocol_encode(%{1 => 2, 3.0 => 4.0, key: :bar}) == - "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\"}" + assert protocol_encode(%{1 => 2, 3.0 => 4.0, ~c"list" => ~c"list", key: :bar}) == + "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\",\"list\":[108,105,115,116]}" end test "lists" do From 178643f9eb3c684543a2fe139071e038ef264fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 6 Mar 2025 10:39:22 +0100 Subject: [PATCH 109/128] Do not discard nil on protocol concat, closes #14311 (#14314) --- lib/elixir/lib/module.ex | 6 ++++- lib/elixir/lib/protocol.ex | 33 ++++++++++++++++++------ lib/elixir/test/elixir/protocol_test.exs | 7 +++-- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index b31eeb3203f..8c388d9b487 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -946,7 +946,8 @@ defmodule Module do @doc """ Concatenates two aliases and returns a new alias. - It handles binaries and atoms. + It handles binaries and atoms. If one of the aliases + is nil, it is discarded. ## Examples @@ -956,6 +957,9 @@ defmodule Module do iex> Module.concat(Foo, "Bar") Foo.Bar + iex> Module.concat(Foo, nil) + Foo + """ @spec concat(binary | atom, binary | atom) :: atom def concat(left, right) diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 247b2b59e27..526131f5d9b 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -347,7 +347,7 @@ defmodule Protocol do end defp assert_impl!(protocol, base, extra) do - impl = Module.concat(protocol, base) + impl = Protocol.__concat__(protocol, base) try do Code.ensure_compiled!(impl) @@ -678,7 +678,7 @@ defmodule Protocol do end defp load_impl(protocol, for) do - Module.concat(protocol, for) + Protocol.__concat__(protocol, for) end # Finally compile the module and emit its bytecode. @@ -831,7 +831,7 @@ defmodule Protocol do # Define the implementation for built-ins :lists.foreach( fn {guard, mod} -> - target = Module.concat(__MODULE__, mod) + target = Protocol.__concat__(__MODULE__, mod) Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do case Code.ensure_compiled(unquote(target)) do @@ -875,7 +875,7 @@ defmodule Protocol do # Internal handler for Structs Kernel.defp struct_impl_for(struct) do - case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do + case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do {:module, module} -> module {:error, _} -> unquote(any_impl_for) end @@ -948,7 +948,7 @@ defmodule Protocol do quote do protocol = unquote(protocol) for = unquote(for) - name = Module.concat(protocol, for) + name = Protocol.__concat__(protocol, for) Protocol.assert_protocol!(protocol) Protocol.__ensure_defimpl__(protocol, for, __ENV__) @@ -994,7 +994,7 @@ defmodule Protocol do else # TODO: Deprecate this on Elixir v1.22+ assert_impl!(protocol, Any, extra) - {Module.concat(protocol, Any), [for, Macro.struct!(for, env), opts]} + {Protocol.__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]} end # Clean up variables from eval context @@ -1006,7 +1006,7 @@ defmodule Protocol do else __ensure_defimpl__(protocol, for, env) assert_impl!(protocol, Any, extra) - impl = Module.concat(protocol, Any) + impl = Protocol.__concat__(protocol, Any) funs = for {fun, arity} <- protocol.__protocol__(:functions) do @@ -1031,7 +1031,11 @@ defmodule Protocol do def __impl__(:for), do: unquote(for) end - Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env)) + Module.create( + Protocol.__concat__(protocol, for), + [quoted | funs], + Macro.Env.location(env) + ) end end) end @@ -1070,4 +1074,17 @@ defmodule Protocol do is_reference: Reference ] end + + @doc false + def __concat__(left, right) do + String.to_atom( + ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right)) + ) + end + + defp ensure_prefix("Elixir." <> _ = left), do: left + defp ensure_prefix(left), do: "Elixir." <> left + + defp remove_prefix("Elixir." <> right), do: right + defp remove_prefix(right), do: right end diff --git a/lib/elixir/test/elixir/protocol_test.exs b/lib/elixir/test/elixir/protocol_test.exs index 210f41b7088..91d20a76b98 100644 --- a/lib/elixir/test/elixir/protocol_test.exs +++ b/lib/elixir/test/elixir/protocol_test.exs @@ -106,15 +106,18 @@ defmodule ProtocolTest do assert Sample.impl_for(%ImplStruct{}) == Sample.ProtocolTest.ImplStruct assert Sample.impl_for(%ImplStructExplicitFor{}) == Sample.ProtocolTest.ImplStructExplicitFor assert Sample.impl_for(%NoImplStruct{}) == nil + assert is_nil(Sample.impl_for(%{__struct__: nil})) end test "protocol implementation with Any and struct fallbacks" do assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any - # Derived - assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct + assert WithAny.impl_for(%{__struct__: nil}) == WithAny.Any assert WithAny.impl_for(%{__struct__: "foo"}) == WithAny.Map assert WithAny.impl_for(%{}) == WithAny.Map assert WithAny.impl_for(self()) == WithAny.Any + + # Derived + assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct end test "protocol not implemented" do From e35ffc5a903bff3b595e323eb1ac12c4ecd515ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 6 Mar 2025 10:55:08 +0100 Subject: [PATCH 110/128] Release v1.18.3 --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++++----------- RELEASE.md | 2 +- VERSION | 2 +- bin/elixir | 2 +- bin/elixir.bat | 2 +- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab679fd0c90..a3c83b55b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,17 +162,15 @@ Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows: -| **Elixir** | **JSON** | -|------------------------|----------| -| `integer() \| float()` | Number | -| `true \| false ` | Boolean | -| `nil` | Null | -| `binary()` | String | -| `atom()` | String | -| `list()` | Array | -| `%{binary() => _}` | Object | -| `%{atom() => _}` | Object | -| `%{integer() => _}` | Object | +| **Elixir** | **JSON** | +|-----------------------------|----------| +| `integer() \| float()` | Number | +| `true \| false ` | Boolean | +| `nil` | Null | +| `binary()` | String | +| `atom()` | String | +| `list()` | Array | +| `%{String.Chars.t() => _}` | Object | You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types. @@ -225,6 +223,36 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z +## v1.18.3 (2025-03-06) + +### 1. Enhancements + +#### Elixir + + * [JSON] Encode any JSON key to string + * [Kernel] Allow `<<_::3*8>>` in typespecs + +#### Mix + + * [mix loadpaths] Support `--no-listeners` option + +### 2. Bug fixes + +#### Elixir + + * [CLI] Fix `--no-color` not setting `:ansi_enabled` to false + * [Protocol] Return correct implementation for an invalid struct pointing to `nil` + * [Stream] Do not raise when `Stream.cycle/1` is explicitly halted + +#### ExUnit + + * [ExUnit.Diff] Fix regression when diffing nested improper lists + +#### IEx + + * [IEx.Autocomplete] Fix autocomplete crash when expanding struct with `__MODULE__` + * [IEx.Helpers] Do not purge on `recompile` if IEx is not running + ## v1.18.2 (2025-01-22) ### 1. Enhancements diff --git a/RELEASE.md b/RELEASE.md index 83aa5ffd958..79ba58241ce 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,7 +2,7 @@ ## Shipping a new version -1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1 +1. Update version in /VERSION, bin/elixir, and bin/elixir.bat 2. Ensure /CHANGELOG.md is updated, versioned and add the current date diff --git a/VERSION b/VERSION index b57fc7228b6..72582753e34 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.2 +1.18.3 \ No newline at end of file diff --git a/bin/elixir b/bin/elixir index 226936a875b..79cb49484fe 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.18.2 +ELIXIR_VERSION=1.18.3 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index d9857edc4cd..eeec4db500a 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @echo off -set ELIXIR_VERSION=1.18.2 +set ELIXIR_VERSION=1.18.3 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation From 8115bd38a131e987b30eb5af8e7043a0ea40b24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 14 Mar 2025 20:13:07 +0100 Subject: [PATCH 111/128] Preserve backwards compatibility in elixir_erl, closes #14323 --- lib/elixir/src/elixir_erl_try.erl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/elixir/src/elixir_erl_try.erl b/lib/elixir/src/elixir_erl_try.erl index 3543203462f..2a3d32aaf0e 100644 --- a/lib/elixir/src/elixir_erl_try.erl +++ b/lib/elixir/src/elixir_erl_try.erl @@ -20,7 +20,15 @@ reduce_clauses([], Acc, OldStack, SAcc, _S) -> {lists:reverse(Acc), SAcc#elixir_erl{stacktrace=OldStack}}. each_clause({'catch', Meta, Raw, Expr}, S) -> - {Match, Guards} = elixir_utils:extract_splat_guards(Raw), + {Args, Guards} = elixir_utils:extract_splat_guards(Raw), + + Match = + %% Elixir v1.17 and earlier emitted single argument + %% and may still be processed via debug_info + case Args of + [X] -> [throw, X]; + [X, Y] -> [X, Y] + end, {{clause, Line, [TKind, TMatches], TGuards, TBody}, TS} = elixir_erl_clauses:clause(?ann(Meta), fun elixir_erl_pass:translate_args/3, Match, Expr, Guards, S), From c32f72581a0a24f46bf22bd28fa325ba84c5dd56 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sat, 15 Mar 2025 19:18:18 +0900 Subject: [PATCH 112/128] Fix handling of ErlangError when :general key is chardata (#14329) --- lib/elixir/lib/exception.ex | 2 +- lib/elixir/test/elixir/exception_test.exs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 63ec7a2407d..094e5f6a735 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -2530,7 +2530,7 @@ defmodule ErlangError do {:ok, reason, IO.iodata_to_binary([":\n\n" | Enum.map(args_errors, &arg_error/1)])} general = extra[:general] -> - {:ok, reason, ": " <> general} + {:ok, reason, ": " <> IO.chardata_to_string(general)} true -> :error diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index a0032a85f45..4059cf37054 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -46,6 +46,12 @@ defmodule ExceptionTest do {:io, :put_chars, [self(), <<222>>], [error_info: %{module: __MODULE__, function: :dummy_error_extras}]} ]) + + assert %ErlangError{original: {:failed_load_cacerts, :enoent}, reason: ": this is chardata"} = + Exception.normalize(:error, {:failed_load_cacerts, :enoent}, [ + {:pubkey_os_cacerts, :get, 0, + [error_info: %{module: __MODULE__, function: :dummy_error_chardata}]} + ]) end test "format/2 without stacktrace" do @@ -1026,4 +1032,8 @@ defmodule ExceptionTest do end def dummy_error_extras(_exception, _stacktrace), do: %{general: "foo"} + + def dummy_error_chardata(_exception, _stacktrace) do + %{general: ~c"this is " ++ [~c"chardata"], reason: ~c"this " ++ [~c"too"]} + end end From ca57cfe6f3ebaee3c33b2a520745ddb1157b73fe Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Tue, 25 Mar 2025 18:07:07 +0900 Subject: [PATCH 113/128] Handle non-binary bitstring in struct default values (#14363) --- lib/elixir/src/elixir_erl.erl | 10 ++++++++++ lib/elixir/test/elixir/map_test.exs | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index be7339147d0..342835dd360 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -92,6 +92,9 @@ elixir_to_erl(Tree, Ann) when is_binary(Tree) -> %% considers a string in a binary to be encoded in latin1, so the bytes %% are not changed in any fashion. {bin, Ann, [{bin_element, Ann, {string, Ann, binary_to_list(Tree)}, default, default}]}; +elixir_to_erl(Tree, Ann) when is_bitstring(Tree) -> + Segments = [elixir_to_erl_bitstring_segment(X, Ann) || X <- bitstring_to_list(Tree)], + {bin, Ann, Segments}; elixir_to_erl(Tree, Ann) when is_function(Tree) -> case (erlang:fun_info(Tree, type) == {type, external}) andalso (erlang:fun_info(Tree, env) == {env, []}) of @@ -111,6 +114,13 @@ elixir_to_erl(Tree, Ann) -> elixir_to_erl_cons([H | T], Ann) -> {cons, Ann, elixir_to_erl(H, Ann), elixir_to_erl_cons(T, Ann)}; elixir_to_erl_cons(T, Ann) -> elixir_to_erl(T, Ann). +elixir_to_erl_bitstring_segment(Int, Ann) when is_integer(Int) -> + {bin_element, Ann, {integer, Ann, Int}, default, [integer]}; +elixir_to_erl_bitstring_segment(Rest, Ann) when is_bitstring(Rest) -> + Size = bit_size(Rest), + <> = Rest, + {bin_element, Ann, {integer, Ann, Int}, {integer, Ann, Size}, [integer]}. + %% Returns a scope for translation. scope(_Meta, ExpandCaptures) -> diff --git a/lib/elixir/test/elixir/map_test.exs b/lib/elixir/test/elixir/map_test.exs index 553d1956f48..a2fdcbb01d8 100644 --- a/lib/elixir/test/elixir/map_test.exs +++ b/lib/elixir/test/elixir/map_test.exs @@ -395,6 +395,15 @@ defmodule MapTest do assert quoted == {:%, [], [User, {:%{}, [], [{:foo, 1}]}]} end + test "structs with bitstring defaults" do + defmodule WithBitstring do + defstruct bitstring: <<255, 127::7>> + end + + info = Macro.struct_info!(WithBitstring, __ENV__) + assert info == [%{default: <<255, 127::7>>, field: :bitstring}] + end + test "defstruct can only be used once in a module" do message = "defstruct has already been called for TestMod, " <> From 6ac1d10f777a91d43372b26dca51c4ff90285677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 26 Mar 2025 16:56:38 +0100 Subject: [PATCH 114/128] Do not run listeners when not checking the deps --- lib/mix/lib/mix/tasks/clean.ex | 8 +++++++- lib/mix/lib/mix/tasks/compile.ex | 8 +++++++- lib/mix/lib/mix/tasks/help.ex | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/clean.ex b/lib/mix/lib/mix/tasks/clean.ex index 7c0964f97d7..2f73b08c26c 100644 --- a/lib/mix/lib/mix/tasks/clean.ex +++ b/lib/mix/lib/mix/tasks/clean.ex @@ -65,7 +65,13 @@ defmodule Mix.Tasks.Clean do # Loadpaths without checks because compilers may be defined in deps. defp loadpaths! do - options = ["--no-elixir-version-check", "--no-deps-check", "--no-archives-check"] + options = [ + "--no-elixir-version-check", + "--no-deps-check", + "--no-archives-check", + "--no-listeners" + ] + Mix.Task.run("loadpaths", options) Mix.Task.reenable("loadpaths") Mix.Task.reenable("deps.loadpaths") diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index b007653b4d1..148a9ef5268 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -86,7 +86,13 @@ defmodule Mix.Tasks.Compile do @impl true def run(["--list"]) do # Loadpaths without checks because compilers may be defined in deps. - args = ["--no-elixir-version-check", "--no-deps-check", "--no-archives-check"] + args = [ + "--no-elixir-version-check", + "--no-deps-check", + "--no-archives-check", + "--no-listeners" + ] + Mix.Task.run("loadpaths", args) Mix.Task.reenable("loadpaths") Mix.Task.reenable("deps.loadpaths") diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index f52df49b33e..0b1913ad46a 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -119,7 +119,13 @@ defmodule Mix.Tasks.Help do # Loadpaths without checks because tasks may be defined in deps. defp loadpaths! do - args = ["--no-elixir-version-check", "--no-deps-check", "--no-archives-check"] + args = [ + "--no-elixir-version-check", + "--no-deps-check", + "--no-archives-check", + "--no-listeners" + ] + Mix.Task.run("loadpaths", args) Mix.Task.reenable("loadpaths") Mix.Task.reenable("deps.loadpaths") From 76c64a0f32a5993286e5bce9b00149f0a85693ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 30 Mar 2025 16:13:02 +0200 Subject: [PATCH 115/128] Recompile regexes when escaped from module attributes (#14381) --- lib/elixir/lib/kernel.ex | 37 +++++++++++++++++---------- lib/elixir/test/elixir/regex_test.exs | 19 ++++++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index ed1ec8a9fc4..f8f6c9e6fac 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -3716,26 +3716,25 @@ defmodule Kernel do case function? do true -> - value = - case Module.__get_attribute__(env.module, name, line, false) do - {_, doc} when doc_attr? -> doc - other -> other - end + case Module.__get_attribute__(env.module, name, line, false) do + {_, doc} when doc_attr? -> + do_at_escape(name, doc) + + %{__struct__: Regex, source: source, opts: opts} = regex -> + case :erlang.system_info(:otp_release) < [?2, ?8] do + true -> do_at_escape(name, regex) + false -> quote(do: Regex.compile!(unquote(source), unquote(opts))) + end - try do - :elixir_quote.escape(value, :none, false) - rescue - ex in [ArgumentError] -> - raise ArgumentError, - "cannot inject attribute @#{name} into function/macro because " <> - Exception.message(ex) + value -> + do_at_escape(name, value) end false when doc_attr? -> quote do case Module.__get_attribute__(__MODULE__, unquote(name), unquote(line), false) do {_, doc} -> doc - other -> other + value -> value end end @@ -3770,6 +3769,17 @@ defmodule Kernel do raise ArgumentError, "expected 0 or 1 argument for @#{name}, got: #{length(args)}" end + defp do_at_escape(name, value) do + try do + :elixir_quote.escape(value, :none, false) + rescue + ex in [ArgumentError] -> + raise ArgumentError, + "cannot inject attribute @#{name} into function/macro because " <> + Exception.message(ex) + end + end + # Those are always compile-time dependencies, so we can skip the trace. defp collect_traces(:before_compile, arg, _env), do: {arg, []} defp collect_traces(:after_compile, arg, _env), do: {arg, []} @@ -6461,6 +6471,7 @@ defmodule Kernel do end defp compile_regex(binary_or_tuple, options) do + # TODO: Remove this when we require Erlang/OTP 28+ case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do true -> Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options))) diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index 917df6d2603..3b7d8f781d5 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -5,6 +5,25 @@ defmodule RegexTest do doctest Regex + if System.otp_release() >= "28" do + test "module attribute" do + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + defmodule ModAttr do + @regex ~r/example/ + def regex, do: @regex + + @bare_regex :erlang.term_to_binary(@regex) + def bare_regex, do: :erlang.binary_to_term(@bare_regex) + + # We don't rewrite outside of functions + assert @regex.re_pattern == :erlang.binary_to_term(@bare_regex).re_pattern + end + + assert ModAttr.regex().re_pattern != ModAttr.bare_regex().re_pattern + end) =~ "storing and reading regexes from module attributes is deprecated" + end + end + test "multiline" do refute Regex.match?(~r/^b$/, "a\nb\nc") assert Regex.match?(~r/^b$/m, "a\nb\nc") From 65baed8681f7fd7cbc501b8134b172a0f45897b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 30 Mar 2025 16:25:38 +0200 Subject: [PATCH 116/128] Update Windows CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 978f36a2f9e..5b44545fe87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }} strategy: matrix: - otp_version: ["25.3", "26.2", "27.1"] + otp_version: ["25.3", "26.2", "27.3"] runs-on: windows-2022 steps: - name: Configure Git From 18d367c01ba4447d78633fab251f741ce939c5e5 Mon Sep 17 00:00:00 2001 From: Fabian Becker Date: Mon, 31 Mar 2025 17:02:48 +0200 Subject: [PATCH 117/128] Remove deprecation warning check for 1.18 (#14382) --- lib/elixir/test/elixir/regex_test.exs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index 3b7d8f781d5..64f9c91d6d8 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -7,20 +7,18 @@ defmodule RegexTest do if System.otp_release() >= "28" do test "module attribute" do - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - defmodule ModAttr do - @regex ~r/example/ - def regex, do: @regex + defmodule ModAttr do + @regex ~r/example/ + def regex, do: @regex - @bare_regex :erlang.term_to_binary(@regex) - def bare_regex, do: :erlang.binary_to_term(@bare_regex) + @bare_regex :erlang.term_to_binary(@regex) + def bare_regex, do: :erlang.binary_to_term(@bare_regex) - # We don't rewrite outside of functions - assert @regex.re_pattern == :erlang.binary_to_term(@bare_regex).re_pattern - end + # We don't rewrite outside of functions + assert @regex.re_pattern == :erlang.binary_to_term(@bare_regex).re_pattern + end - assert ModAttr.regex().re_pattern != ModAttr.bare_regex().re_pattern - end) =~ "storing and reading regexes from module attributes is deprecated" + assert ModAttr.regex().re_pattern != ModAttr.bare_regex().re_pattern end end From f89c5076f96b2032e02d1fcc54d0ad574e4421e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 3 Apr 2025 11:50:05 +0200 Subject: [PATCH 118/128] Do not crash on nested bitstrings Closes #14391. --- lib/elixir/lib/module/types/pattern.ex | 4 ++++ lib/elixir/test/elixir/module/types/pattern_test.exs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 77f51a16d7c..9f4a76f678a 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -335,6 +335,10 @@ defmodule Module.Types.Pattern do {type, context} end + def of_match_var({:<<>>, _meta, args}, _expected, _expr, stack, context) do + {binary(), Of.binary(args, :match, stack, context)} + end + def of_match_var(ast, expected, expr, stack, context) do of_match(ast, expected, expr, :default, stack, context) end diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index b6e789e5422..8e940d2ea06 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -251,6 +251,10 @@ defmodule Module.Types.PatternTest do assert typecheck!([<>], x) == integer() end + test "nested" do + assert typecheck!([<<0, <>::binary>>], x) == binary() + end + test "error" do assert typeerror!([<>], x) == ~l""" incompatible types assigned to "x": From ef002e2b15b92246b349a833d50862141c0106c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 8 Apr 2025 09:25:20 +0200 Subject: [PATCH 119/128] Properly track imported function calls in tracer Closes #13878. --- lib/elixir/src/elixir_dispatch.erl | 4 +- .../elixir/kernel/lexical_tracker_test.exs | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index 121b1683642..779880a41e8 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -204,7 +204,9 @@ do_expand_import(Result, Meta, Name, Arity, Module, E, Trace) -> {import, Receiver} -> case expand_require(true, Meta, Receiver, Name, Arity, E, Trace) of {macro, _, _} = Response -> Response; - error -> {function, Receiver, Name} + error -> + Trace andalso elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E), + {function, Receiver, Name} end; false when Module == ?kernel -> case elixir_rewrite:inline(Module, Name, Arity) of diff --git a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs index c4e10309073..c464899b042 100644 --- a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs +++ b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs @@ -517,5 +517,55 @@ defmodule Kernel.LexicalTrackerTest do refute URI in exports refute URI in runtime end + + test "imported functions from quote adds dependencies" do + {{compile, exports, runtime, _}, _binding} = + Code.eval_string(""" + defmodule Kernel.LexicalTrackerTest.QuotedFun do + import URI + + defmacro parse_root() do + quote do + parse("/") + end + end + end + + defmodule Kernel.LexicalTrackerTest.UsingQuotedFun do + require Kernel.LexicalTrackerTest.QuotedFun, as: QF + QF.parse_root() + Kernel.LexicalTracker.references(__ENV__.lexical_tracker) + end |> elem(3) + """) + + assert URI in compile + refute URI in exports + refute URI in runtime + end + + test "imported macro from quote adds dependencies" do + {{compile, exports, runtime, _}, _binding} = + Code.eval_string(""" + defmodule Kernel.LexicalTrackerTest.QuotedMacro do + import Config + + defmacro config_env() do + quote do + config_env() + end + end + end + + defmodule Kernel.LexicalTrackerTest.UsingQuotedMacro do + require Kernel.LexicalTrackerTest.QuotedMacro, as: QM + def fun(), do: QM.config_env() + Kernel.LexicalTracker.references(__ENV__.lexical_tracker) + end |> elem(3) + """) + + assert Config in compile + refute Config in exports + refute Config in runtime + end end end From 64c4ecf9c77f46da3eb7efb4c7aa43d0847a9bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jan 2025 15:42:50 +0100 Subject: [PATCH 120/128] Do not add duplicates on maps and tuples intersections --- lib/elixir/lib/module/types/descr.ex | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 175e3e315d2..4f836b9e4d2 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1275,7 +1275,16 @@ defmodule Module.Types.Descr do acc -> try do {tag, fields} = map_literal_intersection(tag1, pos1, tag2, pos2) - [{tag, fields, negs1 ++ negs2} | acc] + entry = {tag, fields, negs1 ++ negs2} + + # Imagine a, b, c, where a is closed and b and c are open with + # no keys in common. The result in both cases will be a and we + # want to avoid adding duplicates, especially as intersection + # is a cartesian product. + case :lists.member(entry, acc) do + true -> acc + false -> [entry | acc] + end catch :empty -> acc end @@ -1931,8 +1940,16 @@ defmodule Module.Types.Descr do reduce: [] do acc -> case tuple_literal_intersection(tag1, elements1, tag2, elements2) do - {tag, fields} -> [{tag, fields, negs1 ++ negs2} | acc] - :empty -> acc + {tag, elements} -> + entry = {tag, elements, negs1 ++ negs2} + + case :lists.member(entry, acc) do + true -> acc + false -> [entry | acc] + end + + :empty -> + acc end end |> case do From e34495e304de1b1758b71d20c6859584b365fee4 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 24 Jan 2025 17:12:13 +0900 Subject: [PATCH 121/128] Optimize map unions to avoid building long lists (#14215) --- lib/elixir/lib/module/types/descr.ex | 139 +++++++++++++++--- .../test/elixir/module/types/descr_test.exs | 40 +++++ 2 files changed, 158 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 4f836b9e4d2..3cc5b20c551 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1264,8 +1264,115 @@ defmodule Module.Types.Descr do defp map_only?(descr), do: empty?(Map.delete(descr, :map)) - # Union is list concatenation - defp map_union(dnf1, dnf2), do: dnf1 ++ (dnf2 -- dnf1) + defp map_union(dnf1, dnf2) do + # Union is just concatenation, but we rely on some optimization strategies to + # avoid the list to grow when possible + + # first pass trying to identify patterns where two maps can be fused as one + with [map1] <- dnf1, + [map2] <- dnf2, + optimized when optimized != nil <- maybe_optimize_map_union(map1, map2) do + [optimized] + else + # otherwise we just concatenate and remove structural duplicates + _ -> dnf1 ++ (dnf2 -- dnf1) + end + end + + defp maybe_optimize_map_union({tag1, pos1, []} = map1, {tag2, pos2, []} = map2) do + case map_union_optimization_strategy(tag1, pos1, tag2, pos2) do + :all_equal -> + map1 + + :any_map -> + {:open, %{}, []} + + {:one_key_difference, key, v1, v2} -> + new_pos = Map.put(pos1, key, union(v1, v2)) + {tag1, new_pos, []} + + :left_subtype_of_right -> + map2 + + :right_subtype_of_left -> + map1 + + nil -> + nil + end + end + + defp maybe_optimize_map_union(_, _), do: nil + + defp map_union_optimization_strategy(tag1, pos1, tag2, pos2) + defp map_union_optimization_strategy(tag, pos, tag, pos), do: :all_equal + defp map_union_optimization_strategy(:open, empty, _, _) when empty == %{}, do: :any_map + defp map_union_optimization_strategy(_, _, :open, empty) when empty == %{}, do: :any_map + + defp map_union_optimization_strategy(tag, pos1, tag, pos2) + when map_size(pos1) == map_size(pos2) do + :maps.iterator(pos1) + |> :maps.next() + |> do_map_union_optimization_strategy(pos2, :all_equal) + end + + defp map_union_optimization_strategy(:open, pos1, _, pos2) + when map_size(pos1) <= map_size(pos2) do + :maps.iterator(pos1) + |> :maps.next() + |> do_map_union_optimization_strategy(pos2, :right_subtype_of_left) + end + + defp map_union_optimization_strategy(_, pos1, :open, pos2) + when map_size(pos1) >= map_size(pos2) do + :maps.iterator(pos2) + |> :maps.next() + |> do_map_union_optimization_strategy(pos1, :right_subtype_of_left) + |> case do + :right_subtype_of_left -> :left_subtype_of_right + nil -> nil + end + end + + defp map_union_optimization_strategy(_, _, _, _), do: nil + + defp do_map_union_optimization_strategy(:none, _, status), do: status + + defp do_map_union_optimization_strategy({key, v1, iterator}, pos2, status) do + with %{^key => v2} <- pos2, + next_status when next_status != nil <- map_union_next_strategy(key, v1, v2, status) do + do_map_union_optimization_strategy(:maps.next(iterator), pos2, next_status) + else + _ -> nil + end + end + + defp map_union_next_strategy(key, v1, v2, status) + + # structurally equal values do not impact the ongoing strategy + defp map_union_next_strategy(_key, same, same, status), do: status + + defp map_union_next_strategy(key, v1, v2, :all_equal) do + if key != :__struct__, do: {:one_key_difference, key, v1, v2} + end + + defp map_union_next_strategy(_key, v1, v2, {:one_key_difference, _, d1, d2}) do + # we have at least two key differences now, we switch strategy + # if both are subtypes in one direction, keep checking + cond do + subtype?(d1, d2) and subtype?(v1, v2) -> :left_subtype_of_right + subtype?(d2, d1) and subtype?(v2, v1) -> :right_subtype_of_left + true -> nil + end + end + + defp map_union_next_strategy(_key, v1, v2, :left_subtype_of_right) do + if subtype?(v1, v2), do: :left_subtype_of_right + end + + defp map_union_next_strategy(_key, v1, v2, :right_subtype_of_left) do + if subtype?(v2, v1), do: :right_subtype_of_left + end # Given two unions of maps, intersects each pair of maps. defp map_intersection(dnf1, dnf2) do @@ -1747,29 +1854,19 @@ defmodule Module.Types.Descr do defp map_non_negated_fuse(maps) do Enum.reduce(maps, [], fn map, acc -> - case Enum.split_while(acc, &non_fusible_maps?(map, &1)) do - {_, []} -> - [map | acc] - - {others, [match | rest]} -> - fused = map_non_negated_fuse_pair(map, match) - others ++ [fused | rest] - end + fuse_with_first_fusible(map, acc) end) end - # Two maps are fusible if they differ in at most one element. - defp non_fusible_maps?({_, fields1, []}, {_, fields2, []}) do - Enum.count_until(fields1, fn {key, value} -> Map.fetch!(fields2, key) != value end, 2) > 1 - end - - defp map_non_negated_fuse_pair({tag, fields1, []}, {_, fields2, []}) do - fields = - symmetrical_merge(fields1, fields2, fn _k, v1, v2 -> - if v1 == v2, do: v1, else: union(v1, v2) - end) + defp fuse_with_first_fusible(map, []), do: [map] - {tag, fields, []} + defp fuse_with_first_fusible(map, [candidate | rest]) do + if fused = maybe_optimize_map_union(map, candidate) do + # we found a fusible candidate, we're done + [fused | rest] + else + [candidate | fuse_with_first_fusible(map, rest)] + end end # If all fields are the same except one, we can optimize map difference. diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 3011da51345..e63ed883735 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -101,6 +101,46 @@ defmodule Module.Types.DescrTest do assert union(difference(list(term()), list(integer())), list(integer())) |> equal?(list(term())) end + + test "optimizations" do + # The tests are checking the actual implementation, not the semantics. + # This is why we are using structural comparisons. + # It's fine to remove these if the implementation changes, but breaking + # these might have an important impact on compile times. + + # Optimization one: same tags, all but one key are structurally equal + assert union( + open_map(a: float(), b: atom()), + open_map(a: integer(), b: atom()) + ) == open_map(a: union(float(), integer()), b: atom()) + + assert union( + closed_map(a: float(), b: atom()), + closed_map(a: integer(), b: atom()) + ) == closed_map(a: union(float(), integer()), b: atom()) + + # Optimization two: we can tell that one map is a trivial subtype of the other: + + assert union( + closed_map(a: term(), b: term()), + closed_map(a: float(), b: binary()) + ) == closed_map(a: term(), b: term()) + + assert union( + open_map(a: term()), + closed_map(a: float(), b: binary()) + ) == open_map(a: term()) + + assert union( + closed_map(a: float(), b: binary()), + open_map(a: term()) + ) == open_map(a: term()) + + assert union( + closed_map(a: term(), b: tuple([term(), term()])), + closed_map(a: float(), b: tuple([atom(), binary()])) + ) == closed_map(a: term(), b: tuple([term(), term()])) + end end describe "intersection" do From 6432e8655c995075af96e70bdc20dd61cefdcd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 25 Apr 2025 20:14:01 +0200 Subject: [PATCH 122/128] Write modules before verification so modules can be found during verification --- lib/elixir/lib/kernel/parallel_compiler.ex | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 23d79179f3c..ec64c107d50 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -258,13 +258,6 @@ defmodule Kernel.ParallelCompiler do {status, modules_or_errors, info} = try do spawn_workers(schedulers, cache, files, output, options) - else - {:ok, outcome, info} -> - beam_timestamp = Keyword.get(options, :beam_timestamp) - {:ok, write_module_binaries(outcome, output, beam_timestamp), info} - - {:error, errors, info} -> - {:error, errors, info} after Module.ParallelChecker.stop(cache) end @@ -288,6 +281,7 @@ defmodule Kernel.ParallelCompiler do {outcome, state} = spawn_workers(files, %{}, %{}, [], %{}, [], [], %{ + beam_timestamp: Keyword.get(options, :beam_timestamp), dest: Keyword.get(options, :dest), each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end), each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(), @@ -345,9 +339,10 @@ defmodule Kernel.ParallelCompiler do ## Verification defp verify_modules(result, compile_warnings, dependent_modules, state) do + modules = write_module_binaries(result, state.output, state.beam_timestamp) runtime_warnings = maybe_check_modules(result, dependent_modules, state) info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings} - {{:ok, result, info}, state} + {{:ok, modules, info}, state} end defp maybe_check_modules(result, runtime_modules, state) do From 7e01628b5d771ca7cb69058b09c13c383e3eb9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 25 Apr 2025 19:57:25 +0200 Subject: [PATCH 123/128] Compute beam location early --- lib/elixir/src/elixir_module.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 4725cfd2a72..66455a23cb4 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -155,6 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> put_compiler_modules([Module | CompilerModules]), {Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E), CheckerInfo = checker_info(), + BeamLocation = beam_location(ModuleAsCharlist), {Binary, PersistedAttributes, Autoload} = elixir_erl_compiler:spawn(fun() -> @@ -219,7 +220,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> {Binary, PersistedAttributes, Autoload} end), - Autoload andalso code:load_binary(Module, beam_location(ModuleAsCharlist), Binary), + Autoload andalso code:load_binary(Module, BeamLocation, Binary), put_compiler_modules(CompilerModules), eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE), elixir_env:trace({on_module, Binary, none}, ModuleE), From b41f0f1528b390cb7d2f8233453942fb17e1558f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 25 Apr 2025 19:22:22 +0200 Subject: [PATCH 124/128] Load modules lazily --- lib/elixir/lib/kernel/parallel_compiler.ex | 56 ++++++++++++++++++---- lib/elixir/lib/module.ex | 8 ++-- lib/elixir/src/elixir_erl_compiler.erl | 2 + lib/elixir/src/elixir_module.erl | 18 +++---- lib/mix/lib/mix/compilers/elixir.ex | 3 -- 5 files changed, 65 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index ec64c107d50..053c49550c7 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -181,7 +181,7 @@ defmodule Kernel.ParallelCompiler do {:ok, [atom], [warning] | info()} | {:error, [error] | [Code.diagnostic(:error)], [warning] | info()} def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do - spawn_workers(files, {:compile, path}, options) + spawn_workers(files, {:compile, path}, Keyword.put(options, :dest, path)) end @doc """ @@ -320,6 +320,9 @@ defmodule Kernel.ParallelCompiler do end defp write_module_binaries(result, {:compile, path}, timestamp) do + File.mkdir_p!(path) + Code.prepend_path(path) + Enum.flat_map(result, fn {{:module, module}, binary} when is_binary(binary) -> full_path = Path.join(path, Atom.to_string(module) <> ".beam") @@ -420,8 +423,8 @@ defmodule Kernel.ParallelCompiler do try do case output do - {:compile, path} -> compile_file(file, path, parent) - :compile -> compile_file(file, dest, parent) + {:compile, _} -> compile_file(file, dest, false, parent) + :compile -> compile_file(file, dest, true, parent) :require -> require_file(file, parent) end catch @@ -527,9 +530,9 @@ defmodule Kernel.ParallelCompiler do wait_for_messages([], spawned, waiting, files, result, warnings, errors, state) end - defp compile_file(file, path, parent) do + defp compile_file(file, path, force_load?, parent) do :erlang.process_flag(:error_handler, Kernel.ErrorHandler) - :erlang.put(:elixir_compiler_dest, path) + :erlang.put(:elixir_compiler_dest, {path, force_load?}) :elixir_compiler.file(file, &each_file(&1, &2, parent)) end @@ -630,19 +633,30 @@ defmodule Kernel.ParallelCompiler do state ) - {:module_available, child, ref, file, module, binary} -> + {:module_available, child, ref, file, module, binary, loaded?} -> state.each_module.(file, module, binary) + available = + case Map.get(result, {:module, module}) do + [_ | _] = pids -> + # We prefer to load in the client, if possible, + # to avoid locking the compilation server. + loaded? or load_module(module, binary, state) + Enum.map(pids, &{&1, :found}) + + _ -> + [] + end + # Release the module loader which is waiting for an ack send(child, {ref, :ack}) - {available, result} = update_result(result, :module, module, binary) spawn_workers( available ++ queue, spawned, waiting, files, - result, + Map.put(result, {:module, module}, binary), warnings, errors, state @@ -661,6 +675,8 @@ defmodule Kernel.ParallelCompiler do {waiting, files, result} = if not is_list(available_or_pending) or on in defining do + # If what we are waiting on was defined but not loaded, we do it now. + load_pending(kind, on, result, state) send(child_pid, {ref, :found}) {waiting, files, result} else @@ -755,6 +771,30 @@ defmodule Kernel.ParallelCompiler do {{:error, Enum.reverse(errors, fun.()), info}, state} end + defp load_pending(kind, module, result, state) do + with true <- kind in [:module, :struct], + %{{:module, ^module} => binary} when is_binary(binary) <- result, + false <- :erlang.module_loaded(module) do + load_module(module, binary, state) + end + end + + defp load_module(module, binary, state) do + beam_location = + case state.dest do + nil -> + [] + + dest -> + :filename.join( + :elixir_utils.characters_to_list(dest), + Atom.to_charlist(module) ++ ~c".beam" + ) + end + + :code.load_binary(module, beam_location, binary) + end + defp update_result(result, kind, module, value) do available = case Map.get(result, {kind, module}) do diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 8c388d9b487..7eeb4f1bcb6 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -586,9 +586,11 @@ defmodule Module do name/arity pairs. Inlining is applied locally, calls from another module are not affected by this option - * `@compile {:autoload, false}` - disables automatic loading of - modules after compilation. Instead, the module will be loaded after - it is dispatched to + * `@compile {:autoload, true}` - configures if modules are automatically + loaded after definition. It defaults to `false` when compiling modules + to `.beam` files in disk (as the modules are then lazily loaded from + disk). If modules are not compiled to disk, then they are always loaded, + regardless of this flag * `@compile {:no_warn_undefined, Mod}` or `@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if diff --git a/lib/elixir/src/elixir_erl_compiler.erl b/lib/elixir/src/elixir_erl_compiler.erl index 90848fb3b48..7650f9dcb24 100644 --- a/lib/elixir/src/elixir_erl_compiler.erl +++ b/lib/elixir/src/elixir_erl_compiler.erl @@ -4,6 +4,7 @@ spawn(Fun) -> CompilerInfo = get(elixir_compiler_info), + {error_handler, ErrorHandler} = erlang:process_info(self(), error_handler), CodeDiagnostics = case get(elixir_code_diagnostics) of @@ -13,6 +14,7 @@ spawn(Fun) -> {_, Ref} = spawn_monitor(fun() -> + erlang:process_flag(error_handler, ErrorHandler), put(elixir_compiler_info, CompilerInfo), put(elixir_code_diagnostics, CodeDiagnostics), diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 66455a23cb4..d34e2c88c83 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -155,7 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> put_compiler_modules([Module | CompilerModules]), {Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E), CheckerInfo = checker_info(), - BeamLocation = beam_location(ModuleAsCharlist), + {BeamLocation, Forceload} = beam_location(ModuleAsCharlist), {Binary, PersistedAttributes, Autoload} = elixir_erl_compiler:spawn(fun() -> @@ -215,17 +215,17 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> compile_error_if_tainted(DataSet, E), Binary = elixir_erl:compile(ModuleMap), - Autoload = proplists:get_value(autoload, CompileOpts, true), + Autoload = Forceload or proplists:get_value(autoload, CompileOpts, false), spawn_parallel_checker(CheckerInfo, Module, ModuleMap), {Binary, PersistedAttributes, Autoload} end), Autoload andalso code:load_binary(Module, BeamLocation, Binary), + make_module_available(Module, Binary, Autoload), put_compiler_modules(CompilerModules), eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE), elixir_env:trace({on_module, Binary, none}, ModuleE), warn_unused_attributes(DataSet, DataBag, PersistedAttributes, E), - make_module_available(Module, Binary), (element(2, CheckerInfo) == nil) andalso [VerifyMod:VerifyFun(Module) || {VerifyMod, VerifyFun} <- bag_lookup_element(DataBag, {accumulate, after_verify}, 2)], @@ -544,10 +544,12 @@ bag_lookup_element(Table, Name, Pos) -> beam_location(ModuleAsCharlist) -> case get(elixir_compiler_dest) of - Dest when is_binary(Dest) -> - filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"); + {Dest, ForceLoad} when is_binary(Dest) -> + BeamLocation = + filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"), + {BeamLocation, ForceLoad}; _ -> - "" + {"", true} end. %% Integration with elixir_compiler that makes the module available @@ -568,7 +570,7 @@ spawn_parallel_checker(CheckerInfo, Module, ModuleMap) -> end, 'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Log). -make_module_available(Module, Binary) -> +make_module_available(Module, Binary, Loaded) -> case get(elixir_module_binaries) of Current when is_list(Current) -> put(elixir_module_binaries, [{Module, Binary} | Current]); @@ -581,7 +583,7 @@ make_module_available(Module, Binary) -> ok; {PID, _} -> Ref = make_ref(), - PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary}, + PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary, Loaded}, receive {Ref, ack} -> ok end end. diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 629bc73cd04..5df2c1286e1 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -166,9 +166,6 @@ defmodule Mix.Compilers.Elixir do Mix.Utils.compiling_n(length(stale), :ex) Mix.Project.ensure_structure() - - # We don't want to cache this path as we will write to it - true = Code.prepend_path(dest) previous_opts = set_compiler_opts(opts) try do From d0fb3570310ec386b881accb7935ed8a63336ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 26 Apr 2025 09:01:07 +0200 Subject: [PATCH 125/128] Avoid purging non-loaded modules --- lib/mix/lib/mix/compilers/elixir.ex | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 5df2c1286e1..6dd57e45c1b 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -735,16 +735,22 @@ defmodule Mix.Compilers.Elixir do defp remove_and_purge(beam, module) do _ = File.rm(beam) - :code.purge(module) - :code.delete(module) + + if Code.loaded?(module) do + :code.purge(module) + :code.delete(module) + end end defp purge_modules_in_path(path) do with {:ok, beams} <- File.ls(path) do Enum.each(beams, fn beam -> module = beam |> Path.rootname() |> String.to_atom() - :code.purge(module) - :code.delete(module) + + if Code.loaded?(module) do + :code.purge(module) + :code.delete(module) + end end) end end @@ -922,9 +928,7 @@ defmodule Mix.Compilers.Elixir do end for {module, _} <- data do - File.rm(beam_path(compile_path, module)) - :code.purge(module) - :code.delete(module) + remove_and_purge(beam_path(compile_path, module), module) end rescue _ -> From 7d76b18c4384a1f8f23b9f076288a5aad350f8d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 26 Apr 2025 16:57:31 +0200 Subject: [PATCH 126/128] Perform more loading on the client if possible --- lib/elixir/lib/kernel/parallel_compiler.ex | 24 ++++++++++++++++------ lib/elixir/src/elixir_module.erl | 22 +++++++++++++------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 053c49550c7..6f9cabdf57e 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -422,10 +422,10 @@ defmodule Kernel.ParallelCompiler do :erlang.put(:elixir_compiler_file, file) try do - case output do - {:compile, _} -> compile_file(file, dest, false, parent) - :compile -> compile_file(file, dest, true, parent) - :require -> require_file(file, parent) + if output == :require do + require_file(file, parent) + else + compile_file(file, dest, parent) end catch kind, reason -> @@ -530,9 +530,9 @@ defmodule Kernel.ParallelCompiler do wait_for_messages([], spawned, waiting, files, result, warnings, errors, state) end - defp compile_file(file, path, force_load?, parent) do + defp compile_file(file, path, parent) do :erlang.process_flag(:error_handler, Kernel.ErrorHandler) - :erlang.put(:elixir_compiler_dest, {path, force_load?}) + :erlang.put(:elixir_compiler_dest, path) :elixir_compiler.file(file, &each_file(&1, &2, parent)) end @@ -633,6 +633,18 @@ defmodule Kernel.ParallelCompiler do state ) + {:load_module?, child, ref, module} -> + # If compiling files to disk, we only load the module + # if other modules are waiting for it. + load? = + case state.output do + {:compile, _} -> match?(%{{:module, ^module} => [_ | _]}, result) + _ -> true + end + + send(child, {ref, load?}) + spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) + {:module_available, child, ref, file, module, binary, loaded?} -> state.each_module.(file, module, binary) diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index d34e2c88c83..6b97501b4d5 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -155,7 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> put_compiler_modules([Module | CompilerModules]), {Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E), CheckerInfo = checker_info(), - {BeamLocation, Forceload} = beam_location(ModuleAsCharlist), + BeamLocation = beam_location(ModuleAsCharlist), {Binary, PersistedAttributes, Autoload} = elixir_erl_compiler:spawn(fun() -> @@ -215,7 +215,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> compile_error_if_tainted(DataSet, E), Binary = elixir_erl:compile(ModuleMap), - Autoload = Forceload or proplists:get_value(autoload, CompileOpts, false), + Autoload = proplists:get_value(autoload, CompileOpts, false) or load_module(Module), spawn_parallel_checker(CheckerInfo, Module, ModuleMap), {Binary, PersistedAttributes, Autoload} end), @@ -544,12 +544,10 @@ bag_lookup_element(Table, Name, Pos) -> beam_location(ModuleAsCharlist) -> case get(elixir_compiler_dest) of - {Dest, ForceLoad} when is_binary(Dest) -> - BeamLocation = - filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"), - {BeamLocation, ForceLoad}; + Dest when is_binary(Dest) -> + filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"); _ -> - {"", true} + "" end. %% Integration with elixir_compiler that makes the module available @@ -587,6 +585,16 @@ make_module_available(Module, Binary, Loaded) -> receive {Ref, ack} -> ok end end. +load_module(Module) -> + case get(elixir_compiler_info) of + undefined -> + true; + {PID, _} -> + Ref = make_ref(), + PID ! {'load_module?', self(), Ref, Module}, + receive {Ref, Boolean} -> Boolean end + end. + %% Error handling and helpers. %% We've reached the elixir_module or eval internals, skip it with the rest From aa51b1c61fe653306f9c5484450549090865531e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 26 Apr 2025 18:31:36 +0200 Subject: [PATCH 127/128] Use separate process for code loading in parallel compiler --- lib/elixir/lib/kernel/error_handler.ex | 10 ++- lib/elixir/lib/kernel/parallel_compiler.ex | 91 +++++++++++++++------- 2 files changed, 70 insertions(+), 31 deletions(-) diff --git a/lib/elixir/lib/kernel/error_handler.ex b/lib/elixir/lib/kernel/error_handler.ex index 8fe1a2058a9..8152a2f009e 100644 --- a/lib/elixir/lib/kernel/error_handler.ex +++ b/lib/elixir/lib/kernel/error_handler.ex @@ -37,7 +37,15 @@ defmodule Kernel.ErrorHandler do :erlang.garbage_collect(self()) receive do - {^ref, value} -> value + {^ref, {:loading, pid}} -> + ref = :erlang.monitor(:process, pid) + + receive do + {:DOWN, ^ref, _, _, _} -> :found + end + + {^ref, value} -> + value end end end diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 6f9cabdf57e..5c5b80789ca 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -324,7 +324,7 @@ defmodule Kernel.ParallelCompiler do Code.prepend_path(path) Enum.flat_map(result, fn - {{:module, module}, binary} when is_binary(binary) -> + {{:module, module}, {binary, _}} when is_binary(binary) -> full_path = Path.join(path, Atom.to_string(module) <> ".beam") File.write!(full_path, binary) if timestamp, do: File.touch!(full_path, timestamp) @@ -336,7 +336,7 @@ defmodule Kernel.ParallelCompiler do end defp write_module_binaries(result, _output, _timestamp) do - for {{:module, module}, binary} when is_binary(binary) <- result, do: module + for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module end ## Verification @@ -350,7 +350,7 @@ defmodule Kernel.ParallelCompiler do defp maybe_check_modules(result, runtime_modules, state) do compiled_modules = - for {{:module, module}, binary} when is_binary(binary) <- result, + for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module profile( @@ -566,7 +566,7 @@ defmodule Kernel.ParallelCompiler do end defp count_modules(result) do - Enum.count(result, &match?({{:module, _}, binary} when is_binary(binary), &1)) + Enum.count(result, &match?({{:module, _}, {binary, _}} when is_binary(binary), &1)) end defp each_cycle_return({kind, modules, warnings}), do: {kind, modules, warnings} @@ -645,19 +645,28 @@ defmodule Kernel.ParallelCompiler do send(child, {ref, load?}) spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) + {{:module_loaded, module}, _ref, _type, _pid, _reason} -> + result = + Map.update!(result, {:module, module}, fn {binary, _loader} -> {binary, true} end) + + spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) + {:module_available, child, ref, file, module, binary, loaded?} -> state.each_module.(file, module, binary) - available = + {available, load_status} = case Map.get(result, {:module, module}) do + # We prefer to load in the client, if possible, + # to avoid locking the compilation server. + [_ | _] = pids when loaded? -> + {Enum.map(pids, &{&1, :found}), loaded?} + [_ | _] = pids -> - # We prefer to load in the client, if possible, - # to avoid locking the compilation server. - loaded? or load_module(module, binary, state) - Enum.map(pids, &{&1, :found}) + pid = load_module(module, binary, state.dest) + {Enum.map(pids, &{&1, {:loading, pid}}), pid} _ -> - [] + {[], loaded?} end # Release the module loader which is waiting for an ack @@ -668,7 +677,7 @@ defmodule Kernel.ParallelCompiler do spawned, waiting, files, - Map.put(result, {:module, module}, binary), + Map.put(result, {:module, module}, {binary, load_status}), warnings, errors, state @@ -688,8 +697,8 @@ defmodule Kernel.ParallelCompiler do {waiting, files, result} = if not is_list(available_or_pending) or on in defining do # If what we are waiting on was defined but not loaded, we do it now. - load_pending(kind, on, result, state) - send(child_pid, {ref, :found}) + {reply, result} = load_pending(kind, on, result, state) + send(child_pid, {ref, reply}) {waiting, files, result} else waiting = Map.put(waiting, child_pid, {kind, ref, file_pid, on, defining, deadlock}) @@ -784,27 +793,49 @@ defmodule Kernel.ParallelCompiler do end defp load_pending(kind, module, result, state) do - with true <- kind in [:module, :struct], - %{{:module, ^module} => binary} when is_binary(binary) <- result, - false <- :erlang.module_loaded(module) do - load_module(module, binary, state) + case result do + %{{:module, ^module} => {binary, load_status}} + when kind in [:module, :struct] and is_binary(binary) -> + case load_status do + true -> + {:found, result} + + false -> + pid = load_module(module, binary, state.dest) + result = Map.put(result, {:module, module}, {binary, pid}) + {{:loading, pid}, result} + + pid when is_pid(pid) -> + {{:loading, pid}, result} + end + + _ -> + {:found, result} end end - defp load_module(module, binary, state) do - beam_location = - case state.dest do - nil -> - [] - - dest -> - :filename.join( - :elixir_utils.characters_to_list(dest), - Atom.to_charlist(module) ++ ~c".beam" - ) - end + defp load_module(module, binary, dest) do + {pid, _ref} = + :erlang.spawn_opt( + fn -> + beam_location = + case dest do + nil -> + [] + + dest -> + :filename.join( + :elixir_utils.characters_to_list(dest), + Atom.to_charlist(module) ++ ~c".beam" + ) + end + + :code.load_binary(module, beam_location, binary) + end, + monitor: [tag: {:module_loaded, module}] + ) - :code.load_binary(module, beam_location, binary) + pid end defp update_result(result, kind, module, value) do From 3b34eaa4a71aa664e7d7ec22121bbb741beed63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 26 Apr 2025 23:06:57 +0200 Subject: [PATCH 128/128] Do not block module process for loading --- lib/elixir/lib/kernel/parallel_compiler.ex | 24 ++++++---------------- lib/elixir/src/elixir_module.erl | 21 ++++++------------- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 5c5b80789ca..98f9088f0dc 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -422,10 +422,10 @@ defmodule Kernel.ParallelCompiler do :erlang.put(:elixir_compiler_file, file) try do - if output == :require do - require_file(file, parent) - else - compile_file(file, dest, parent) + case output do + {:compile, _} -> compile_file(file, dest, false, parent) + :compile -> compile_file(file, dest, true, parent) + :require -> require_file(file, parent) end catch kind, reason -> @@ -530,9 +530,9 @@ defmodule Kernel.ParallelCompiler do wait_for_messages([], spawned, waiting, files, result, warnings, errors, state) end - defp compile_file(file, path, parent) do + defp compile_file(file, path, force_load?, parent) do :erlang.process_flag(:error_handler, Kernel.ErrorHandler) - :erlang.put(:elixir_compiler_dest, path) + :erlang.put(:elixir_compiler_dest, {path, force_load?}) :elixir_compiler.file(file, &each_file(&1, &2, parent)) end @@ -633,18 +633,6 @@ defmodule Kernel.ParallelCompiler do state ) - {:load_module?, child, ref, module} -> - # If compiling files to disk, we only load the module - # if other modules are waiting for it. - load? = - case state.output do - {:compile, _} -> match?(%{{:module, ^module} => [_ | _]}, result) - _ -> true - end - - send(child, {ref, load?}) - spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) - {{:module_loaded, module}, _ref, _type, _pid, _reason} -> result = Map.update!(result, {:module, module}, fn {binary, _loader} -> {binary, true} end) diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 6b97501b4d5..ef14c919108 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -155,7 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> put_compiler_modules([Module | CompilerModules]), {Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E), CheckerInfo = checker_info(), - BeamLocation = beam_location(ModuleAsCharlist), + {BeamLocation, Forceload} = beam_location(ModuleAsCharlist), {Binary, PersistedAttributes, Autoload} = elixir_erl_compiler:spawn(fun() -> @@ -215,7 +215,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> compile_error_if_tainted(DataSet, E), Binary = elixir_erl:compile(ModuleMap), - Autoload = proplists:get_value(autoload, CompileOpts, false) or load_module(Module), + Autoload = Forceload or proplists:get_value(autoload, CompileOpts, false), spawn_parallel_checker(CheckerInfo, Module, ModuleMap), {Binary, PersistedAttributes, Autoload} end), @@ -544,10 +544,11 @@ bag_lookup_element(Table, Name, Pos) -> beam_location(ModuleAsCharlist) -> case get(elixir_compiler_dest) of - Dest when is_binary(Dest) -> - filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"); + {Dest, Forceload} when is_binary(Dest) -> + {filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"), + Forceload}; _ -> - "" + {"", true} end. %% Integration with elixir_compiler that makes the module available @@ -585,16 +586,6 @@ make_module_available(Module, Binary, Loaded) -> receive {Ref, ack} -> ok end end. -load_module(Module) -> - case get(elixir_compiler_info) of - undefined -> - true; - {PID, _} -> - Ref = make_ref(), - PID ! {'load_module?', self(), Ref, Module}, - receive {Ref, Boolean} -> Boolean end - end. - %% Error handling and helpers. %% We've reached the elixir_module or eval internals, skip it with the rest