diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cea41b109e3..4aeec8b9486 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -113,6 +113,7 @@ jobs:
 
   sign:
     needs: [build]
+    environment: release
     strategy:
       fail-fast: true
       matrix:
@@ -126,6 +127,7 @@ jobs:
 
     permissions:
       contents: write
+      id-token: write
 
     steps:
       - name: "Download build"
@@ -133,23 +135,20 @@ jobs:
         with:
           name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
 
+      - name: Log in to Azure
+        if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
+        uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
+        with:
+          client-id: ${{ secrets.AZURE_CLIENT_ID }}
+          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
       - name: "Sign files with Trusted Signing"
         uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
-        if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
+        if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
         with:
-          azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
-          azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
-          # AZURE_TENANT_ID and AZURE_CLIENT_ID should stay the same,
-          # but AZURE_CLIENT_SECRET has expiration date. When it expires go to
-          # App Registrations / <app> / Certificates & secrets,
-          # click (+) New client secret, note the "Value" (not "Secret ID")
-          # and update it:
-          #
-          #     $ gh --repo elixir-lang/elixir secret set AZURE_CLIENT_SECRET
-          azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
           endpoint: https://eus.codesigning.azure.net/
-          trusted-signing-account-name: trusted-signing-elixir
-          certificate-profile-name: Elixir
+          trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
+          certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
           files-folder: ${{ github.workspace }}
           files-folder-filter: exe
           file-digest: SHA256
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f62a2da8da..c54f40b4af6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,28 +8,6 @@
 
 ## Type system improvements
 
-### More type inference
-
-Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
-
-```elixir
-def add_foo_and_bar(data) do
-  data.foo + data.bar
-end
-```
-
-Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
-
-Here is another example:
-
-```elixir
-def sum_to_string(a, b) do
-  Integer.to_string(a + b)
-end
-```
-
-Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
-
 ### Type checking of protocol dispatch and implementations
 
 This release also adds type checking when dispatching and implementing protocols.
@@ -216,7 +194,7 @@ These additions offer greater transparency into the components and licenses of e
 
 This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
 
-## v1.19.0-dev
+## v1.19.0-rc.0 (2025-06-09)
 
 ### 1. Enhancements
 
@@ -275,6 +253,7 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
   * [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
   * [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
   * [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
+  * [Protocol] `defstruct/1` and `defexception/1` are now disabled inside `defprotocol` as to not allow defining structs/exceptions alongside a protocol
   * [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
   * [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
   * [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
@@ -306,7 +285,7 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
   * [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
   * [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
   * [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
-  * [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}` is deprecated in favor of pattern matching on the struct when the variable is defined and then using the map update syntax `%{uri | path: "/foo/bar"}`. Thanks to the type system, pattern matching on structs can find more errors, more reliably
+  * [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}`, now requires the given variable (or expression) to explicitly pattern match on the struct before it can be updated. This is because, thanks to the type system, pattern matching on structs can find more errors, more reliably, and we want to promote its usage. Once pattern matching is added, you may optionally convert the struct update syntax into the map update syntax `%{uri | path: "/foo/bar"}` with no less of typing guarantees
   * [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
 
 #### Logger
diff --git a/Makefile b/Makefile
index 24d02701acc..a067a602996 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
 TEST_FILES ?= "*_test.exs"
 SHARE_PREFIX ?= $(PREFIX)/share
 MAN_PREFIX ?= $(SHARE_PREFIX)/man
-CANONICAL := main/
+# CANONICAL := main/
 ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
 ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
 ERLC := erlc -I lib/elixir/include
diff --git a/RELEASE.md b/RELEASE.md
index 20fce539404..8adcda37511 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -8,7 +8,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
    - If this release addresses any publicly known security vulnerabilities with
@@ -34,7 +34,7 @@
 
 ### In the new branch
 
-1. Comment the `CANONICAL=` in /Makefile
+1. Comment out `CANONICAL := main/` in /Makefile
 
 2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
 
diff --git a/SECURITY.md b/SECURITY.md
index 159deb6cafe..46d87a85184 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -12,12 +12,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
 
 Elixir version | Support
 :------------- | :-----------------------------
-1.19           | Development
-1.18           | Bug fixes and security patches
+1.19           | Bug fixes and security patches
+1.18           | Security patches only
 1.17           | Security patches only
 1.16           | Security patches only
 1.15           | Security patches only
-1.14           | Security patches only
 
 ## Announcements
 
diff --git a/VERSION b/VERSION
index ff32b92aad5..d3de7a2e908 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.19.0-dev
+1.19.0-rc.0
diff --git a/bin/elixir b/bin/elixir
index a232cf1da9a..f991e0323a7 100755
--- a/bin/elixir
+++ b/bin/elixir
@@ -6,7 +6,7 @@
 
 set -e
 
-ELIXIR_VERSION=1.19.0-dev
+ELIXIR_VERSION=1.19.0-rc.0
 
 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
   cat <<USAGE >&2
diff --git a/bin/elixir.bat b/bin/elixir.bat
index 3eec7b7119d..ded0ad23af9 100644
--- a/bin/elixir.bat
+++ b/bin/elixir.bat
@@ -4,7 +4,7 @@
 :: SPDX-FileCopyrightText: 2021 The Elixir Team
 :: SPDX-FileCopyrightText: 2012 Plataformatec
 
-set ELIXIR_VERSION=1.19.0-dev
+set ELIXIR_VERSION=1.19.0-rc.0
 
 if    ""%1""==""""                if ""%2""=="""" goto documentation
 if /I ""%1""==""--help""          if ""%2""=="""" goto documentation
diff --git a/lib/eex/lib/eex.ex b/lib/eex/lib/eex.ex
index 2e3c07f5eed..38a29e65328 100644
--- a/lib/eex/lib/eex.ex
+++ b/lib/eex/lib/eex.ex
@@ -118,6 +118,19 @@ defmodule EEx do
           | {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
           | {:eof, metadata}
 
+  @type tokenize_opt ::
+          {:file, binary()}
+          | {:line, line}
+          | {:column, column}
+          | {:indentation, non_neg_integer}
+          | {:trim, boolean()}
+
+  @type compile_opt ::
+          tokenize_opt
+          | {:engine, module()}
+          | {:parser_options, Code.parser_opts()}
+          | {atom(), term()}
+
   @doc """
   Generates a function definition from the given string.
 
@@ -128,6 +141,7 @@ defmodule EEx do
   template.
 
   The supported `options` are described [in the module docs](#module-options).
+  Additional options are passed to the underlying engine.
 
   ## Examples
 
@@ -220,7 +234,7 @@ defmodule EEx do
       "3"
 
   """
-  @spec compile_string(String.t(), keyword) :: Macro.t()
+  @spec compile_string(String.t(), [compile_opt]) :: Macro.t()
   def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
     case tokenize(source, options) do
       {:ok, tokens} ->
@@ -259,7 +273,7 @@ defmodule EEx do
       #=> "3"
 
   """
-  @spec compile_file(Path.t(), keyword) :: Macro.t()
+  @spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
   def compile_file(filename, options \\ []) when is_list(options) do
     filename = IO.chardata_to_string(filename)
     options = Keyword.merge([file: filename, line: 1], options)
@@ -277,7 +291,7 @@ defmodule EEx do
       "foo baz"
 
   """
-  @spec eval_string(String.t(), keyword, keyword) :: String.t()
+  @spec eval_string(String.t(), keyword, [compile_opt]) :: String.t()
   def eval_string(source, bindings \\ [], options \\ [])
       when is_binary(source) and is_list(bindings) and is_list(options) do
     compiled = compile_string(source, options)
@@ -299,7 +313,7 @@ defmodule EEx do
       #=> "foo baz"
 
   """
-  @spec eval_file(Path.t(), keyword, keyword) :: String.t()
+  @spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
   def eval_file(filename, bindings \\ [], options \\ [])
       when is_list(bindings) and is_list(options) do
     filename = IO.chardata_to_string(filename)
@@ -339,7 +353,7 @@ defmodule EEx do
   Note new tokens may be added in the future.
   """
   @doc since: "1.14.0"
-  @spec tokenize([char()] | String.t(), opts :: keyword) ::
+  @spec tokenize([char()] | String.t(), [tokenize_opt]) ::
           {:ok, [token()]} | {:error, String.t(), metadata()}
   def tokenize(contents, opts \\ []) do
     EEx.Compiler.tokenize(contents, opts)
diff --git a/lib/eex/lib/eex/engine.ex b/lib/eex/lib/eex/engine.ex
index 10b01b82b9f..12234f048c8 100644
--- a/lib/eex/lib/eex/engine.ex
+++ b/lib/eex/lib/eex/engine.ex
@@ -17,6 +17,10 @@ defmodule EEx.Engine do
   @doc """
   Called at the beginning of every template.
 
+  It receives the options during compilation, including the
+  ones managed by EEx, such as `:line` and `:file`, as well
+  as custom engine options.
+
   It must return the initial state.
   """
   @callback init(opts :: keyword) :: state
diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex
index 48420bd5f08..89160389b8d 100644
--- a/lib/elixir/lib/calendar.ex
+++ b/lib/elixir/lib/calendar.ex
@@ -162,6 +162,22 @@ defmodule Calendar do
   """
   @type time_zone_database :: module()
 
+  @typedoc """
+  Options for formatting dates and times with `strftime/3`.
+  """
+  @type strftime_opts :: [
+          preferred_datetime: String.t(),
+          preferred_date: String.t(),
+          preferred_time: String.t(),
+          am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
+          month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
+          abbreviated_month_names:
+            (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
+          day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
+          abbreviated_day_of_week_names:
+            (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
+        ]
+
   @doc """
   Returns how many days there are in the given month of the given year.
   """
@@ -617,7 +633,7 @@ defmodule Calendar do
 
   """
   @doc since: "1.11.0"
-  @spec strftime(map(), String.t(), keyword()) :: String.t()
+  @spec strftime(map(), String.t(), strftime_opts()) :: String.t()
   def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
       when is_map(date_or_time_or_datetime) and is_binary(string_format) do
     parse(
diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex
index 0fb91ebce07..b4e365bea46 100644
--- a/lib/elixir/lib/calendar/date.ex
+++ b/lib/elixir/lib/calendar/date.ex
@@ -321,7 +321,7 @@ defmodule Date do
   @doc """
   Converts the given date to a string according to its calendar.
 
-  ### Examples
+  ## Examples
 
       iex> Date.to_string(~D[2000-02-28])
       "2000-02-28"
@@ -399,7 +399,7 @@ defmodule Date do
   or other calendars in which the days also start at midnight.
   Attempting to convert dates from other calendars will raise an `ArgumentError`.
 
-  ### Examples
+  ## Examples
 
       iex> Date.to_iso8601(~D[2000-02-28])
       "2000-02-28"
@@ -633,7 +633,7 @@ defmodule Date do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
@@ -667,7 +667,7 @@ defmodule Date do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex
index b945c0ab99b..201696886fb 100644
--- a/lib/elixir/lib/calendar/datetime.ex
+++ b/lib/elixir/lib/calendar/datetime.ex
@@ -1046,7 +1046,7 @@ defmodule DateTime do
   its abbreviation, which means information is lost when converting to such
   format.
 
-  ### Examples
+  ## Examples
 
       iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
       ...>                hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1390,7 +1390,7 @@ defmodule DateTime do
   custom (but relatively common) representation which appends the time
   zone abbreviation and full name to the datetime.
 
-  ### Examples
+  ## Examples
 
       iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
       ...>                hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1636,7 +1636,7 @@ defmodule DateTime do
   not currently supported in Elixir's standard library but it is available
   by third-party libraries.
 
-  ### Examples
+  ## Examples
 
       iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
       iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
@@ -1922,7 +1922,7 @@ defmodule DateTime do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -1969,7 +1969,7 @@ defmodule DateTime do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex
index 520b39d59c4..b96e8b8f030 100644
--- a/lib/elixir/lib/calendar/duration.ex
+++ b/lib/elixir/lib/calendar/duration.ex
@@ -161,6 +161,22 @@ defmodule Duration do
   """
   @type duration :: t | [unit_pair]
 
+  @typedoc """
+  Options for `Duration.to_string/2`.
+  """
+  @type to_string_opts :: [
+          units: [
+            year: String.t(),
+            month: String.t(),
+            week: String.t(),
+            day: String.t(),
+            hour: String.t(),
+            minute: String.t(),
+            second: String.t()
+          ],
+          separator: String.t()
+        ]
+
   @microseconds_per_second 1_000_000
 
   @doc """
@@ -436,6 +452,7 @@ defmodule Duration do
 
   """
   @doc since: "1.18.0"
+  @spec to_string(t, to_string_opts) :: String.t()
   def to_string(%Duration{} = duration, opts \\ []) do
     units = Keyword.get(opts, :units, [])
     separator = Keyword.get(opts, :separator, " ")
diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex
index 76e1337b97b..00b4da3e409 100644
--- a/lib/elixir/lib/calendar/naive_datetime.ex
+++ b/lib/elixir/lib/calendar/naive_datetime.ex
@@ -761,7 +761,7 @@ defmodule NaiveDateTime do
   For readability, this function follows the RFC3339 suggestion of removing
   the "T" separator between the date and time components.
 
-  ### Examples
+  ## Examples
 
       iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
       "2000-02-28 23:00:13"
@@ -908,7 +908,7 @@ defmodule NaiveDateTime do
   Only supports converting naive datetimes which are in the ISO calendar,
   attempting to convert naive datetimes from other calendars will raise.
 
-  ### Examples
+  ## Examples
 
       iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
       "2000-02-28T23:00:13"
@@ -1261,7 +1261,7 @@ defmodule NaiveDateTime do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1327,7 +1327,7 @@ defmodule NaiveDateTime do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex
index e3b4fce96c3..0ae043c658d 100644
--- a/lib/elixir/lib/calendar/time.ex
+++ b/lib/elixir/lib/calendar/time.ex
@@ -225,7 +225,7 @@ defmodule Time do
   @doc """
   Converts the given `time` to a string.
 
-  ### Examples
+  ## Examples
 
       iex> Time.to_string(~T[23:00:00])
       "23:00:00"
@@ -334,7 +334,7 @@ defmodule Time do
   format, for human readability. It also supports the "basic" format through
   passing the `:basic` option.
 
-  ### Examples
+  ## Examples
 
       iex> Time.to_iso8601(~T[23:00:13])
       "23:00:13"
@@ -781,7 +781,7 @@ defmodule Time do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> Time.convert(~T[13:30:15], Calendar.Holocene)
@@ -837,7 +837,7 @@ defmodule Time do
   ## Examples
 
   Imagine someone implements `Calendar.Holocene`, a calendar based on the
-  Gregorian calendar that adds exactly 10,000 years to the current Gregorian
+  Gregorian calendar that adds exactly 10 000 years to the current Gregorian
   year:
 
       iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex
index 8e5c91f5da6..b0463e48d41 100644
--- a/lib/elixir/lib/code.ex
+++ b/lib/elixir/lib/code.ex
@@ -248,6 +248,58 @@ defmodule Code do
   """
   @type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
 
+  @typedoc """
+  Options for code formatting functions.
+  """
+  @type format_opt ::
+          {:file, binary()}
+          | {:line, pos_integer()}
+          | {:line_length, pos_integer()}
+          | {:locals_without_parens, keyword()}
+          | {:force_do_end_blocks, boolean()}
+          | {:migrate, boolean()}
+          | {:migrate_bitstring_modifiers, boolean()}
+          | {:migrate_call_parens_on_pipe, boolean()}
+          | {:migrate_charlists_as_sigils, boolean()}
+          | {:migrate_unless, boolean()}
+          | {atom(), term()}
+
+  @typedoc """
+  Options for `quoted_to_algebra/2`.
+  """
+  @type quoted_to_algebra_opt ::
+          {:line, pos_integer() | nil}
+          | {:escape, boolean()}
+          | {:locals_without_parens, keyword()}
+          | {:comments, [term()]}
+
+  @typedoc """
+  Options for parsing functions that convert strings to quoted expressions.
+  """
+  @type parser_opts :: [
+          file: binary(),
+          line: pos_integer(),
+          column: pos_integer(),
+          indentation: non_neg_integer(),
+          columns: boolean(),
+          unescape: boolean(),
+          existing_atoms_only: boolean(),
+          token_metadata: boolean(),
+          literal_encoder: (term(), Macro.metadata() -> term()),
+          static_atoms_encoder: (atom() -> term()),
+          emit_warnings: boolean()
+        ]
+
+  @typedoc """
+  Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
+  """
+  @type env_eval_opts :: [
+          file: binary(),
+          line: pos_integer(),
+          module: module(),
+          prune_binding: boolean()
+        ]
+
   @boolean_compiler_options [
     :docs,
     :debug_info,
@@ -560,7 +612,7 @@ defmodule Code do
       [a: 1, b: 2]
 
   """
-  @spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
+  @spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
   def eval_string(string, binding \\ [], opts \\ [])
 
   def eval_string(string, binding, %Macro.Env{} = env) do
@@ -615,7 +667,8 @@ defmodule Code do
 
   """
   @doc since: "1.15.0"
-  @spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
+  @spec with_diagnostics([log: boolean()], (-> result)) ::
+          {result, [diagnostic(:warning | :error)]}
         when result: term()
   def with_diagnostics(opts \\ [], fun) do
     value = :erlang.get(:elixir_code_diagnostics)
@@ -648,7 +701,7 @@ defmodule Code do
       Defaults to `true`.
   """
   @doc since: "1.15.0"
-  @spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
+  @spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok
   def print_diagnostic(diagnostic, opts \\ []) do
     read_snippet? = Keyword.get(opts, :snippet, true)
     :elixir_errors.print_diagnostic(diagnostic, read_snippet?)
@@ -672,7 +725,7 @@ defmodule Code do
     * `:line` - the line the string starts, used for error reporting
 
     * `:line_length` - the line length to aim for when formatting
-      the document. Defaults to 98. This value indicates when an expression
+      the document. Defaults to `98`. This value indicates when an expression
       should be broken over multiple lines but it is not guaranteed
       to do so. See the "Line length" section below for more information
 
@@ -1035,7 +1088,7 @@ defmodule Code do
   address the deprecation warnings.
   """
   @doc since: "1.6.0"
-  @spec format_string!(binary, keyword) :: iodata
+  @spec format_string!(binary, [format_opt]) :: iodata
   def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
     line_length = Keyword.get(opts, :line_length, 98)
 
@@ -1060,7 +1113,7 @@ defmodule Code do
   available options.
   """
   @doc since: "1.6.0"
-  @spec format_file!(binary, keyword) :: iodata
+  @spec format_file!(binary, [format_opt]) :: iodata
   def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
     string = File.read!(file)
     formatted = format_string!(string, [file: file, line: 1] ++ opts)
@@ -1098,7 +1151,7 @@ defmodule Code do
       [a: 1, b: 2]
 
   """
-  @spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
+  @spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
   def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
     {value, binding, _env} =
       eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
@@ -1129,8 +1182,15 @@ defmodule Code do
     * `:line` - the line on which the script starts
 
     * `:module` - the module to run the environment on
+
+    * `:prune_binding` - (since v1.14.2) prune binding to keep only
+      variables read or written by the evaluated code. Note that
+      variables used by modules are always pruned, even if later used
+      by the modules. You can submit to the `:on_module` tracer event
+      and access the variables used by the module from its environment.
   """
   @doc since: "1.14.0"
+  @spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
   def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
 
   @doc """
@@ -1144,15 +1204,11 @@ defmodule Code do
 
   ## Options
 
-    * `:prune_binding` - (since v1.14.2) prune binding to keep only
-      variables read or written by the evaluated code. Note that
-      variables used by modules are always pruned, even if later used
-      by the modules. You can submit to the `:on_module` tracer event
-      and access the variables used by the module from its environment.
+  It accepts the same options as `env_for_eval/1`.
 
   """
   @doc since: "1.14.0"
-  @spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
+  @spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
           {term, binding, Macro.Env.t()}
   def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
       when is_list(binding) do
@@ -1171,14 +1227,14 @@ defmodule Code do
       Defaults to `"nofile"`.
 
     * `:line` - the starting line of the string being parsed.
-      Defaults to 1.
+      Defaults to `1`.
 
     * `:column` - (since v1.11.0) the starting column of the string being parsed.
-      Defaults to 1.
+      Defaults to `1`.
 
     * `:indentation` - (since v1.19.0) the indentation for the string being parsed.
       This is useful when the code parsed is embedded within another document.
-      Defaults to 0.
+      Defaults to `0`.
 
     * `:columns` - when `true`, attach a `:column` key to the quoted
       metadata. Defaults to `false`.
@@ -1263,7 +1319,7 @@ defmodule Code do
       {:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
 
   """
-  @spec string_to_quoted(List.Chars.t(), keyword) ::
+  @spec string_to_quoted(List.Chars.t(), parser_opts) ::
           {:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
   def string_to_quoted(string, opts \\ []) when is_list(opts) do
     file = Keyword.get(opts, :file, "nofile")
@@ -1290,7 +1346,7 @@ defmodule Code do
 
   Check `string_to_quoted/2` for options information.
   """
-  @spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
+  @spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t()
   def string_to_quoted!(string, opts \\ []) when is_list(opts) do
     file = Keyword.get(opts, :file, "nofile")
     line = Keyword.get(opts, :line, 1)
@@ -1341,7 +1397,7 @@ defmodule Code do
 
   """
   @doc since: "1.13.0"
-  @spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
+  @spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) ::
           {:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
   def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
     charlist = to_charlist(string)
@@ -1371,7 +1427,7 @@ defmodule Code do
   Check `string_to_quoted/2` for options information.
   """
   @doc since: "1.13.0"
-  @spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
+  @spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())}
   def string_to_quoted_with_comments!(string, opts \\ []) do
     charlist = to_charlist(string)
 
@@ -1456,6 +1512,9 @@ defmodule Code do
 
   ## Options
 
+  This function accepts all options supported by `format_string!/2` for controlling
+  code formatting, plus these additional options:
+
     * `:comments` - the list of comments associated with the quoted expression.
       Defaults to `[]`. It is recommended that both `:token_metadata` and
       `:literal_encoder` options are given to `string_to_quoted_with_comments/2`
@@ -1466,17 +1525,13 @@ defmodule Code do
       `string_to_quoted/2`, setting this option to `false` will prevent it from
       escaping the sequences twice. Defaults to `true`.
 
-    * `:locals_without_parens` - a keyword list of name and arity
-      pairs that should be kept without parens whenever possible.
-      The arity may be the atom `:*`, which implies all arities of
-      that name. The formatter already includes a list of functions
-      and this option augments this list.
-
-    * `:syntax_colors` - a keyword list of colors the output is colorized.
-      See `Inspect.Opts` for more information.
+  See `format_string!/2` for the full list of formatting options including
+  `:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
+  `:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
   """
   @doc since: "1.13.0"
-  @spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
+  @spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
+          Inspect.Algebra.t()
   def quoted_to_algebra(quoted, opts \\ []) do
     quoted
     |> Code.Normalizer.normalize(opts)
diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex
index 170edf71299..70ea70ae0e8 100644
--- a/lib/elixir/lib/code/formatter.ex
+++ b/lib/elixir/lib/code/formatter.ex
@@ -158,6 +158,7 @@ defmodule Code.Formatter do
   @doc """
   Converts the quoted expression into an algebra document.
   """
+  @spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
   def to_algebra(quoted, opts \\ []) do
     comments = Keyword.get(opts, :comments, [])
 
diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex
index 255a3640cad..7e5323c5fad 100644
--- a/lib/elixir/lib/code/fragment.ex
+++ b/lib/elixir/lib/code/fragment.ex
@@ -11,6 +11,26 @@ defmodule Code.Fragment do
 
   @type position :: {line :: pos_integer(), column :: pos_integer()}
 
+  @typedoc """
+  Options for cursor context functions.
+
+  Currently, these options are not used but reserved for future extensibility.
+  """
+  @type cursor_opts :: []
+
+  @typedoc """
+  Options for converting code fragments to quoted expressions.
+  """
+  @type container_cursor_to_quoted_opts :: [
+          file: String.t(),
+          line: pos_integer(),
+          column: pos_integer(),
+          columns: boolean(),
+          token_metadata: boolean(),
+          literal_encoder: (term(), Macro.metadata() -> term()),
+          trailing_fragment: String.t()
+        ]
+
   @doc ~S"""
   Returns the list of lines in the given string, preserving their line endings.
 
@@ -172,7 +192,7 @@ defmodule Code.Fragment do
   references, and more.
   """
   @doc since: "1.13.0"
-  @spec cursor_context(List.Chars.t(), keyword()) ::
+  @spec cursor_context(List.Chars.t(), cursor_opts()) ::
           {:alias, charlist}
           | {:alias, inside_alias, charlist}
           | {:block_keyword_or_binary_operator, charlist}
@@ -282,7 +302,8 @@ defmodule Code.Fragment do
       {{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
       {{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
       {{:operator, acc}, count} -> {{:operator_arity, acc}, count}
-      {_, _} -> {:none, 0}
+      {{:sigil, _}, _} -> {:none, 0}
+      {_, _} -> {{:operator, ~c"/"}, 1}
     end
   end
 
@@ -315,7 +336,7 @@ defmodule Code.Fragment do
   end
 
   defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
-  defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
+  defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
   defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
   defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
 
@@ -662,7 +683,7 @@ defmodule Code.Fragment do
   of examples and their return values.
   """
   @doc since: "1.13.0"
-  @spec surround_context(List.Chars.t(), position(), keyword()) ::
+  @spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
           %{begin: position, end: position, context: context} | :none
         when context:
                {:alias, charlist}
@@ -771,6 +792,12 @@ defmodule Code.Fragment do
           {{:local_or_var, acc}, offset} ->
             build_surround({:local_or_var, acc}, reversed, line, offset)
 
+          {{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
+            build_surround({:operator, acc}, reversed, line, offset)
+
+          {{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
+            build_surround({:keyword, acc}, reversed, line, offset)
+
           {{:module_attribute, ~c""}, offset} ->
             build_surround({:operator, ~c"@"}, reversed, line, offset)
 
@@ -1187,10 +1214,10 @@ defmodule Code.Fragment do
       Defaults to `"nofile"`.
 
     * `:line` - the starting line of the string being parsed.
-      Defaults to 1.
+      Defaults to `1`.
 
     * `:column` - the starting column of the string being parsed.
-      Defaults to 1.
+      Defaults to `1`.
 
     * `:columns` - when `true`, attach a `:column` key to the quoted
       metadata. Defaults to `false`.
@@ -1209,7 +1236,7 @@ defmodule Code.Fragment do
 
   """
   @doc since: "1.13.0"
-  @spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
+  @spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
           {:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
   def container_cursor_to_quoted(fragment, opts \\ []) do
     {trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex
index c3dd67a3af9..a4aa9492ec1 100644
--- a/lib/elixir/lib/code/normalizer.ex
+++ b/lib/elixir/lib/code/normalizer.ex
@@ -14,6 +14,7 @@ defmodule Code.Normalizer do
   Wraps literals in the quoted expression to conform to the AST format expected
   by the formatter.
   """
+  @spec normalize(Macro.t(), keyword()) :: Macro.t()
   def normalize(quoted, opts \\ []) do
     line = Keyword.get(opts, :line, nil)
     escape = Keyword.get(opts, :escape, true)
diff --git a/lib/elixir/lib/config.ex b/lib/elixir/lib/config.ex
index b18fbd271c3..409512b84f2 100644
--- a/lib/elixir/lib/config.ex
+++ b/lib/elixir/lib/config.ex
@@ -98,6 +98,12 @@ defmodule Config do
   (assembled with `mix release`).
   """
 
+  @type config_opts :: [
+          imports: [Path.t()] | :disabled,
+          env: atom(),
+          target: atom()
+        ]
+
   @opts_key {__MODULE__, :opts}
   @config_key {__MODULE__, :config}
   @imports_key {__MODULE__, :imports}
@@ -306,7 +312,7 @@ defmodule Config do
   end
 
   @doc false
-  @spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
+  @spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
   def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
     env = Keyword.get(opts, :env)
     target = Keyword.get(opts, :target)
diff --git a/lib/elixir/lib/config/provider.ex b/lib/elixir/lib/config/provider.ex
index 756d583e074..b039a2233c7 100644
--- a/lib/elixir/lib/config/provider.ex
+++ b/lib/elixir/lib/config/provider.ex
@@ -111,6 +111,16 @@ defmodule Config.Provider do
   """
   @type config_path :: {:system, binary(), binary()} | binary()
 
+  @typedoc """
+  Options for `init/3`.
+  """
+  @type init_opts :: [
+          extra_config: config(),
+          prune_runtime_sys_config_after_boot: boolean(),
+          reboot_system_after_config: boolean(),
+          validate_compile_env: [{atom(), [atom()], term()}]
+        ]
+
   @doc """
   Invoked when initializing a config provider.
 
@@ -196,6 +206,7 @@ defmodule Config.Provider do
   @reboot_mode_key :config_provider_reboot_mode
 
   @doc false
+  @spec init([{module(), term()}], config_path(), init_opts()) :: config()
   def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
     validate_config_path!(config_path)
     providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
diff --git a/lib/elixir/lib/config/reader.ex b/lib/elixir/lib/config/reader.ex
index 1997bb1cb0e..3a259d3602e 100644
--- a/lib/elixir/lib/config/reader.ex
+++ b/lib/elixir/lib/config/reader.ex
@@ -46,6 +46,12 @@ defmodule Config.Reader do
 
   @behaviour Config.Provider
 
+  @type config_opts :: [
+          imports: [Path.t()] | :disabled,
+          env: atom(),
+          target: atom()
+        ]
+
   @impl true
   def init(opts) when is_list(opts) do
     {path, opts} = Keyword.pop!(opts, :path)
@@ -68,7 +74,7 @@ defmodule Config.Reader do
   Accepts the same options as `read!/2`.
   """
   @doc since: "1.11.0"
-  @spec eval!(Path.t(), binary, keyword) :: keyword
+  @spec eval!(Path.t(), binary, config_opts) :: keyword
   def eval!(file, contents, opts \\ [])
       when is_binary(file) and is_binary(contents) and is_list(opts) do
     Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
@@ -90,7 +96,7 @@ defmodule Config.Reader do
 
   """
   @doc since: "1.9.0"
-  @spec read!(Path.t(), keyword) :: keyword
+  @spec read!(Path.t(), config_opts) :: keyword
   def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
     file = Path.expand(file)
     Config.__eval__!(file, File.read!(file), opts) |> elem(0)
@@ -104,7 +110,7 @@ defmodule Config.Reader do
   option cannot be disabled in `read_imports!/2`.
   """
   @doc since: "1.9.0"
-  @spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
+  @spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
   def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
     if opts[:imports] == :disabled do
       raise ArgumentError, ":imports must be a list of paths"
diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex
index 9c0175865dd..62e039c938c 100644
--- a/lib/elixir/lib/inspect/algebra.ex
+++ b/lib/elixir/lib/inspect/algebra.ex
@@ -46,7 +46,7 @@ defmodule Inspect.Opts do
     * `:limit` - limits the number of items that are inspected for tuples,
       bitstrings, maps, lists and any other collection of items, with the exception of
       printable strings and printable charlists which use the `:printable_limit` option.
-      It accepts a positive integer or `:infinity`. It defaults to 100 since
+      It accepts a positive integer or `:infinity`. It defaults to `100` since
       `Elixir v1.19.0`, as it has better defaults to deal with nested collections.
 
     * `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
@@ -115,11 +115,28 @@ defmodule Inspect.Opts do
           width: non_neg_integer | :infinity
         }
 
+  @typedoc """
+  Options for building an `Inspect.Opts` struct with `new/1`.
+  """
+  @type new_opt ::
+          {:base, :decimal | :binary | :hex | :octal}
+          | {:binaries, :infer | :as_binaries | :as_strings}
+          | {:charlists, :infer | :as_lists | :as_charlists}
+          | {:custom_options, keyword}
+          | {:inspect_fun, (any, t -> Inspect.Algebra.t())}
+          | {:limit, non_neg_integer | :infinity}
+          | {:pretty, boolean}
+          | {:printable_limit, non_neg_integer | :infinity}
+          | {:safe, boolean}
+          | {:structs, boolean}
+          | {:syntax_colors, [{color_key, IO.ANSI.ansidata()}]}
+          | {:width, non_neg_integer | :infinity}
+
   @doc """
   Builds an `Inspect.Opts` struct.
   """
   @doc since: "1.13.0"
-  @spec new(keyword()) :: t
+  @spec new([new_opt()]) :: t
   def new(opts) do
     struct(%Inspect.Opts{inspect_fun: default_inspect_fun()}, opts)
   end
@@ -324,6 +341,14 @@ defmodule Inspect.Algebra do
     quote do: {:doc_color, unquote(doc), unquote(color)}
   end
 
+  @typedoc """
+  Options for container documents.
+  """
+  @type container_opts :: [
+          separator: String.t(),
+          break: :strict | :flex | :maybe
+        ]
+
   @docs [
     :doc_break,
     :doc_collapse,
@@ -440,7 +465,14 @@ defmodule Inspect.Algebra do
   updated options from inspection.
   """
   @doc since: "1.6.0"
-  @spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
+  @spec container_doc(
+          t,
+          [term],
+          t,
+          Inspect.Opts.t(),
+          (term, Inspect.Opts.t() -> t),
+          container_opts()
+        ) ::
           t
   def container_doc(left, collection, right, inspect_opts, fun, opts \\ []) do
     container_doc_with_opts(left, collection, right, inspect_opts, fun, opts) |> elem(0)
@@ -496,7 +528,7 @@ defmodule Inspect.Algebra do
           t,
           Inspect.Opts.t(),
           (term, Inspect.Opts.t() -> t),
-          keyword()
+          container_opts()
         ) ::
           {t, Inspect.Opts.t()}
   def container_doc_with_opts(left, collection, right, inspect_opts, fun, opts \\ [])
diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex
index e5c012ffd35..36fbe6573c7 100644
--- a/lib/elixir/lib/io.ex
+++ b/lib/elixir/lib/io.ex
@@ -128,6 +128,22 @@ defmodule IO do
   @type nodata :: {:error, term} | :eof
   @type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
 
+  @type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
+
+  @typedoc """
+  Stacktrace information as keyword options for `warn/2`.
+
+  At least `:file` is required. Other options are optional and used
+  to provide more precise location information.
+  """
+  @type warn_stacktrace_opts :: [
+          file: String.t(),
+          line: pos_integer(),
+          column: pos_integer(),
+          module: module(),
+          function: {atom(), arity()}
+        ]
+
   defguardp is_device(term) when is_atom(term) or is_pid(term)
   defguardp is_iodata(data) when is_list(data) or is_binary(data)
 
@@ -346,7 +362,10 @@ defmodule IO do
       #=>   my_app.ex:4: MyApp.main/1
 
   """
-  @spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
+  @spec warn(
+          chardata | String.Chars.t(),
+          Exception.stacktrace() | warn_stacktrace_opts() | Macro.Env.t()
+        ) ::
           :ok
   def warn(message, stacktrace_info)
 
@@ -448,13 +467,15 @@ defmodule IO do
 
   ## Examples
 
+  The following code:
+
       IO.inspect(<<0, 1, 2>>, width: 40)
 
   Prints:
 
       <<0, 1, 2>>
 
-  We can use the `:label` option to decorate the output:
+  You can use the `:label` option to decorate the output:
 
       IO.inspect(1..100, label: "a wonderful range")
 
@@ -462,21 +483,23 @@ defmodule IO do
 
       a wonderful range: 1..100
 
-  The `:label` option is especially useful with pipelines:
+  Inspect truncates large inputs by default. The `:printable_limit` controls
+  the limit for strings and other string-like constructs (such as charlists):
 
-      [1, 2, 3]
-      |> IO.inspect(label: "before")
-      |> Enum.map(&(&1 * 2))
-      |> IO.inspect(label: "after")
-      |> Enum.sum()
+      "abc"
+      |> String.duplicate(9001)
+      |> IO.inspect(printable_limit: :infinity)
 
-  Prints:
+  For containers such as lists, maps, and tuples, the number of entries
+  is managed by the `:limit` option:
 
-      before: [1, 2, 3]
-      after: [2, 4, 6]
+      1..100
+      |> Enum.map(& {&1, &1})
+      |> Enum.into(%{})
+      |> IO.inspect(limit: :infinity)
 
   """
-  @spec inspect(item, keyword) :: item when item: var
+  @spec inspect(item, inspect_opts) :: item when item: var
   def inspect(item, opts \\ []) do
     inspect(:stdio, item, opts)
   end
@@ -486,7 +509,7 @@ defmodule IO do
 
   See `inspect/2` for a full list of options.
   """
-  @spec inspect(device, item, keyword) :: item when item: var
+  @spec inspect(device, item, inspect_opts) :: item when item: var
   def inspect(device, item, opts) when is_device(device) and is_list(opts) do
     label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
     opts = Inspect.Opts.new(opts)
diff --git a/lib/elixir/lib/io/ansi/docs.ex b/lib/elixir/lib/io/ansi/docs.ex
index f47d5365415..fe3a04bc1e4 100644
--- a/lib/elixir/lib/io/ansi/docs.ex
+++ b/lib/elixir/lib/io/ansi/docs.ex
@@ -5,6 +5,20 @@
 defmodule IO.ANSI.Docs do
   @moduledoc false
 
+  @type print_opts :: [
+          enabled: boolean(),
+          doc_bold: [IO.ANSI.ansicode()],
+          doc_code: [IO.ANSI.ansicode()],
+          doc_headings: [IO.ANSI.ansicode()],
+          doc_metadata: [IO.ANSI.ansicode()],
+          doc_quote: [IO.ANSI.ansicode()],
+          doc_inline_code: [IO.ANSI.ansicode()],
+          doc_table_heading: [IO.ANSI.ansicode()],
+          doc_title: [IO.ANSI.ansicode()],
+          doc_underline: [IO.ANSI.ansicode()],
+          width: pos_integer()
+        ]
+
   @bullet_text_unicode "• "
   @bullet_text_ascii "* "
   @bullets [?*, ?-, ?+]
@@ -30,7 +44,7 @@ defmodule IO.ANSI.Docs do
   Values for the color settings are strings with
   comma-separated ANSI values.
   """
-  @spec default_options() :: keyword
+  @spec default_options() :: print_opts
   def default_options do
     [
       enabled: true,
@@ -52,7 +66,7 @@ defmodule IO.ANSI.Docs do
 
   See `default_options/0` for docs on the supported options.
   """
-  @spec print_headings([String.t()], keyword) :: :ok
+  @spec print_headings([String.t()], print_opts) :: :ok
   def print_headings(headings, options \\ []) do
     # It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
     # breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
@@ -77,7 +91,7 @@ defmodule IO.ANSI.Docs do
 
   See `default_options/0` for docs on the supported options.
   """
-  @spec print_metadata(map, keyword) :: :ok
+  @spec print_metadata(map, print_opts) :: :ok
   def print_metadata(metadata, options \\ []) when is_map(metadata) do
     options = Keyword.merge(default_options(), options)
     print_each_metadata(metadata, options) && IO.write("\n")
@@ -115,7 +129,7 @@ defmodule IO.ANSI.Docs do
 
   It takes a set of `options` defined in `default_options/0`.
   """
-  @spec print(term(), String.t(), keyword) :: :ok
+  @spec print(term(), String.t(), print_opts) :: :ok
   def print(doc, format, options \\ [])
 
   def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex
index 8b143965f9e..6f431d616a9 100644
--- a/lib/elixir/lib/json.ex
+++ b/lib/elixir/lib/json.ex
@@ -328,6 +328,22 @@ defmodule JSON do
           | {:invalid_byte, non_neg_integer(), byte()}
           | {:unexpected_sequence, non_neg_integer(), binary()}
 
+  @typedoc """
+  Decoders for customizing JSON decoding behavior.
+  """
+  @type decoders :: [
+          array_start: (term() -> term()),
+          array_push: (term(), term() -> term()),
+          array_finish: (term(), term() -> {term(), term()}),
+          object_start: (term() -> term()),
+          object_push: (term(), term(), term() -> term()),
+          object_finish: (term(), term() -> {term(), term()}),
+          float: (String.t() -> term()),
+          integer: (String.t() -> term()),
+          string: (String.t() -> term()),
+          null: term()
+        ]
+
   @doc ~S"""
   Decodes the given JSON.
 
@@ -381,7 +397,7 @@ defmodule JSON do
 
   For streaming decoding, see Erlang's [`:json`](`:json`) module.
   """
-  @spec decode(binary(), term(), keyword()) ::
+  @spec decode(binary(), term(), decoders()) ::
           {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)
diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex
index d14399fd499..7c8e8545494 100644
--- a/lib/elixir/lib/kernel.ex
+++ b/lib/elixir/lib/kernel.ex
@@ -231,7 +231,7 @@ defmodule Kernel do
 
   Finally, note there is an overall structural sorting order, called
   "Term Ordering", defined below. This order is provided for reference
-  purposes, it is not required by Elixir developers to know it by heart.
+  purposes, it is not required for Elixir developers to know it by heart.
 
   ### Term ordering
 
@@ -2456,7 +2456,7 @@ defmodule Kernel do
   See the "Deriving" section of the documentation of the `Inspect`
   protocol for more information.
   """
-  @spec inspect(Inspect.t(), keyword) :: String.t()
+  @spec inspect(Inspect.t(), [Inspect.Opts.new_opt()]) :: String.t()
   def inspect(term, opts \\ []) when is_list(opts) do
     opts = Inspect.Opts.new(opts)
 
@@ -2815,7 +2815,7 @@ defmodule Kernel do
   This is most commonly used in pipelines, using the `|>/2` operator, allowing you
   to pipe a value to a function outside of its first argument.
 
-  ### Examples
+  ## Examples
 
       iex> 1 |> then(fn x -> x * 2 end)
       2
@@ -3815,13 +3815,7 @@ defmodule Kernel do
             do_at_escape(name, doc)
 
           %{__struct__: Regex, source: source, opts: opts} = regex ->
-            # TODO: Remove this in Elixir v2.0
-            IO.warn(
-              "storing and reading regexes from module attributes is deprecated, " <>
-                "inline the regex inside the function definition instead",
-              env
-            )
-
+            # TODO: Automatically deal with exported regexes
             case :erlang.system_info(:otp_release) < [?2, ?8] do
               true -> do_at_escape(name, regex)
               false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex
index 0621817f3f4..925b8237717 100644
--- a/lib/elixir/lib/kernel/parallel_compiler.ex
+++ b/lib/elixir/lib/kernel/parallel_compiler.ex
@@ -16,6 +16,38 @@ defmodule Kernel.ParallelCompiler do
   @type warning() :: {file :: Path.t(), Code.position(), message :: String.t()}
   @type error() :: {file :: Path.t(), Code.position(), message :: String.t()}
 
+  @typedoc """
+  Options for parallel compilation functions.
+  """
+  @type compile_opts :: [
+          after_compile: (-> term()),
+          each_file: (Path.t() -> term()),
+          each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
+          each_long_verification: (module() -> term()) | (module(), pid() -> term()),
+          each_module: (Path.t(), module(), binary() -> term()),
+          each_cycle: ([module()], [Code.diagnostic(:warning)] ->
+                         {:compile, [module()], [Code.diagnostic(:warning)]}
+                         | {:runtime, [module()], [Code.diagnostic(:warning)]}),
+          long_compilation_threshold: pos_integer(),
+          long_verification_threshold: pos_integer(),
+          verification: boolean(),
+          profile: :time,
+          dest: Path.t(),
+          beam_timestamp: term(),
+          return_diagnostics: boolean(),
+          max_concurrency: pos_integer()
+        ]
+
+  @typedoc """
+  Options for requiring files in parallel.
+  """
+  @type require_opts :: [
+          each_file: (Path.t() -> term()),
+          each_module: (Path.t(), module(), binary() -> term()),
+          max_concurrency: pos_integer(),
+          return_diagnostics: boolean()
+        ]
+
   @doc """
   Starts a task for parallel compilation.
   """
@@ -181,11 +213,11 @@ defmodule Kernel.ParallelCompiler do
 
     * `:max_concurrency` - the maximum number of files to compile in parallel.
       Setting this option to 1 will compile files sequentially.
-      Defaults to the number of schedulers online, or at least 2.
+      Defaults to the number of schedulers online, or at least `2`.
 
   """
   @doc since: "1.6.0"
-  @spec compile([Path.t()], keyword()) ::
+  @spec compile([Path.t()], compile_opts()) ::
           {:ok, [atom], [warning] | info()}
           | {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
   def compile(files, options \\ []) when is_list(options) do
@@ -198,7 +230,7 @@ defmodule Kernel.ParallelCompiler do
   See `compile/2` for more information.
   """
   @doc since: "1.6.0"
-  @spec compile_to_path([Path.t()], Path.t(), keyword()) ::
+  @spec compile_to_path([Path.t()], Path.t(), compile_opts()) ::
           {: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
@@ -231,11 +263,14 @@ defmodule Kernel.ParallelCompiler do
 
     * `:max_concurrency` - the maximum number of files to compile in parallel.
       Setting this option to 1 will compile files sequentially.
-      Defaults to the number of schedulers online, or at least 2.
+      Defaults to the number of schedulers online, or at least `2`.
+
+    * `:return_diagnostics` - when `true`, returns structured diagnostics
+      as maps instead of the legacy format. Defaults to `false`.
 
   """
   @doc since: "1.6.0"
-  @spec require([Path.t()], keyword()) ::
+  @spec require([Path.t()], require_opts()) ::
           {:ok, [atom], [warning] | info()}
           | {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
   def require(files, options \\ []) when is_list(options) do
diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex
index 485a367b123..1f3539bcf93 100644
--- a/lib/elixir/lib/kernel/typespec.ex
+++ b/lib/elixir/lib/kernel/typespec.ex
@@ -877,7 +877,15 @@ defmodule Kernel.Typespec do
 
   defp typespec({:fun, meta, args}, vars, caller, state) do
     {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
-    {{:type, location(meta), :fun, args}, state}
+
+    if args != [] do
+      IO.warn(
+        "fun/#{length(args)} is not valid in typespecs. Either specify fun() or use (... -> return) instead",
+        caller
+      )
+    end
+
+    {{:type, location(meta), :fun, []}, state}
   end
 
   defp typespec({:..., _meta, _args}, _vars, caller, _state) do
diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex
index 7fc379e66ec..c126ef10cf9 100644
--- a/lib/elixir/lib/kernel/utils.ex
+++ b/lib/elixir/lib/kernel/utils.ex
@@ -217,7 +217,7 @@ defmodule Kernel.Utils do
     case enforce_keys -- :maps.keys(struct) do
       [] ->
         mapper = fn {key, val} ->
-          %{field: key, default: val}
+          %{field: key, default: val, required: :lists.member(key, enforce_keys)}
         end
 
         :ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
diff --git a/lib/elixir/lib/list.ex b/lib/elixir/lib/list.ex
index efad0d2e1bb..e964099fd63 100644
--- a/lib/elixir/lib/list.ex
+++ b/lib/elixir/lib/list.ex
@@ -915,7 +915,7 @@ defmodule List do
 
   If `prefix` is an empty list, it returns `true`.
 
-  ### Examples
+  ## Examples
 
       iex> List.starts_with?([1, 2, 3], [1, 2])
       true
@@ -945,7 +945,7 @@ defmodule List do
 
   If `suffix` is an empty list, it returns `true`.
 
-  ### Examples
+  ## Examples
 
       iex> List.ends_with?([1, 2, 3], [2, 3])
       true
diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex
index 30cce303e71..b09a418f7e1 100644
--- a/lib/elixir/lib/macro.ex
+++ b/lib/elixir/lib/macro.ex
@@ -197,6 +197,15 @@ defmodule Macro do
   @typedoc "A captured remote function in the format of &Mod.fun/arity"
   @type captured_remote_function :: fun
 
+  @type escape_opts :: [
+          unquote: boolean(),
+          prune_metadata: boolean()
+        ]
+
+  @type inspect_atom_opts :: [
+          escape: (binary(), char() -> binary())
+        ]
+
   @doc """
   Breaks a pipeline expression into a list.
 
@@ -835,7 +844,7 @@ defmodule Macro do
   bound), while `quote/2` produces syntax trees for
   expressions.
   """
-  @spec escape(term, keyword) :: t()
+  @spec escape(term, escape_opts) :: t()
   def escape(expr, opts \\ []) do
     unquote = Keyword.get(opts, :unquote, false)
     kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
@@ -852,21 +861,39 @@ defmodule Macro do
   end
 
   @doc """
-  Extracts the struct information (equivalent to calling
-  `module.__info__(:struct)`).
+  Extracts the struct information.
 
   This is useful when a struct needs to be expanded at
   compilation time and the struct being expanded may or may
-  not have been compiled. This function is also capable of
-  expanding structs defined under the module being compiled.
+  not have been compiled (including structs in the defined
+  under the module being compiled). For compiled modules,
+  it will invoke `module.__info__(:struct)`.
+
   Calling this function also adds an export dependency on the
   given struct.
 
   It will raise `ArgumentError` if the struct is not available.
+
+  ## Compatibility considerations
+
+  This function currently returns both `:required` and `:default`
+  entries for each field. While this naming is inconsistent
+  (a required field should not have a default), this is done for
+  backwards compatibility purposes.
+
+  In future releases, Elixir may introduce truly required struct
+  fields, and therefore only one of required or default will be
+  present. Your code should prepare for such scenario accordingly.
   """
   @doc since: "1.18.0"
   @spec struct_info!(module(), Macro.Env.t()) ::
-          [%{field: atom(), required: boolean(), default: term()}]
+          [
+            %{
+              required(:field) => atom(),
+              optional(:required) => boolean(),
+              optional(:default) => term()
+            }
+          ]
   def struct_info!(module, env) when is_atom(module) do
     case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
       {:ok, info} -> info
@@ -2399,7 +2426,7 @@ defmodule Macro do
 
   """
   @doc since: "1.14.0"
-  @spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
+  @spec inspect_atom(:literal | :key | :remote_call, atom, inspect_atom_opts) :: binary
   def inspect_atom(source_format, atom, opts \\ [])
 
   def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
diff --git a/lib/elixir/lib/macro/env.ex b/lib/elixir/lib/macro/env.ex
index f48ca1a31bd..94f0212cc32 100644
--- a/lib/elixir/lib/macro/env.ex
+++ b/lib/elixir/lib/macro/env.ex
@@ -70,6 +70,42 @@ defmodule Macro.Env do
   @typep tracers :: [module]
   @typep versioned_vars :: %{optional(variable) => var_version :: non_neg_integer}
 
+  @type define_import_opts :: [
+          trace: boolean(),
+          emit_warnings: boolean(),
+          info_callback: (atom() -> [{atom(), arity()}]),
+          only: :functions | :macros | [{atom(), arity()}],
+          except: [{atom(), arity()}],
+          warn: boolean()
+        ]
+
+  @type define_alias_opts :: [
+          trace: boolean(),
+          as: atom(),
+          warn: boolean()
+        ]
+
+  @type define_require_opts :: [
+          trace: boolean(),
+          as: atom(),
+          warn: boolean()
+        ]
+
+  @type expand_alias_opts :: [
+          trace: boolean()
+        ]
+
+  @type expand_import_opts :: [
+          allow_locals: boolean() | (-> function() | false),
+          check_deprecations: boolean(),
+          trace: boolean()
+        ]
+
+  @type expand_require_opts :: [
+          check_deprecations: boolean(),
+          trace: boolean()
+        ]
+
   @type t :: %{
           __struct__: __MODULE__,
           aliases: aliases,
@@ -331,7 +367,7 @@ defmodule Macro.Env do
 
   """
   @doc since: "1.17.0"
-  @spec define_require(t, Macro.metadata(), module) :: {:ok, t}
+  @spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
   def define_require(env, meta, module, opts \\ [])
       when is_list(meta) and is_atom(module) and is_list(opts) do
     {trace, opts} = Keyword.pop(opts, :trace, true)
@@ -391,7 +427,8 @@ defmodule Macro.Env do
 
   """
   @doc since: "1.17.0"
-  @spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
+  @spec define_import(t, Macro.metadata(), module, define_import_opts) ::
+          {:ok, t} | {:error, String.t()}
   def define_import(env, meta, module, opts \\ [])
       when is_list(meta) and is_atom(module) and is_list(opts) do
     {trace, opts} = Keyword.pop(opts, :trace, true)
@@ -441,7 +478,8 @@ defmodule Macro.Env do
 
   """
   @doc since: "1.17.0"
-  @spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
+  @spec define_alias(t, Macro.metadata(), module, define_alias_opts) ::
+          {:ok, t} | {:error, String.t()}
   def define_alias(env, meta, module, opts \\ [])
       when is_list(meta) and is_atom(module) and is_list(opts) do
     {trace, opts} = Keyword.pop(opts, :trace, true)
@@ -487,7 +525,7 @@ defmodule Macro.Env do
 
   """
   @doc since: "1.17.0"
-  @spec expand_alias(t, keyword, [atom()], keyword) ::
+  @spec expand_alias(t, keyword, [atom()], expand_alias_opts) ::
           {:alias, atom()} | :error
   def expand_alias(env, meta, list, opts \\ [])
       when is_list(meta) and is_list(list) and is_list(opts) do
@@ -517,8 +555,15 @@ defmodule Macro.Env do
 
   ## Options
 
-    * `:allow_locals` - when set to `false`, it does not attempt to capture
-      local macros defined in the current module in `env`
+    * `:allow_locals` - controls how local macros are resolved.
+      Defaults to `true`.
+
+      - When `false`, does not attempt to capture local macros defined in the
+        current module in `env`
+      - When `true`, uses a default resolver that looks for public macros in
+        the current module
+      - When a function, it will be invoked to lazily compute a local function
+        (or return false). It has signature `(-> function() | false)`
 
     * `:check_deprecations` - when set to `false`, does not check for deprecations
       when expanding macros
@@ -527,7 +572,7 @@ defmodule Macro.Env do
 
   """
   @doc since: "1.17.0"
-  @spec expand_import(t, keyword, atom(), arity(), keyword) ::
+  @spec expand_import(t, keyword, atom(), arity(), expand_import_opts) ::
           {:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
           | {:function, module(), atom()}
           | {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
@@ -542,10 +587,16 @@ defmodule Macro.Env do
         trace = Keyword.get(opts, :trace, true)
         module = env.module
 
+        # When allow_locals is a callback, we don't need to pass module macros as extra
+        # because the callback will handle local macro resolution
         extra =
-          case allow_locals and function_exported?(module, :__info__, 1) do
-            true -> [{module, module.__info__(:macros)}]
-            false -> []
+          if is_function(allow_locals, 0) do
+            []
+          else
+            case allow_locals and function_exported?(module, :__info__, 1) do
+              true -> [{module, module.__info__(:macros)}]
+              false -> []
+            end
           end
 
         case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
@@ -583,7 +634,7 @@ defmodule Macro.Env do
 
   """
   @doc since: "1.17.0"
-  @spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
+  @spec expand_require(t, keyword, module(), atom(), arity(), expand_require_opts) ::
           {:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
           | :error
   def expand_require(env, meta, module, name, arity, opts \\ [])
diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex
index 067668ddc70..19ca9fe9b6a 100644
--- a/lib/elixir/lib/module.ex
+++ b/lib/elixir/lib/module.ex
@@ -681,12 +681,21 @@ defmodule Module do
   This function is generated for all modules. It's similar to `module_info/1` but
   includes some additional Elixir-specific information, such as struct and macro
   information. For documentation, see `c:Module.__info__/1`.
-
   '''
 
   @type definition :: {atom, arity}
   @type def_kind :: :def | :defp | :defmacro | :defmacrop
 
+  @type create_opts :: [
+          file: binary(),
+          line: pos_integer(),
+          generated: boolean()
+        ]
+
+  @type get_definition_opts :: [
+          skip_clauses: boolean()
+        ]
+
   @extra_error_msg_defines? "Use Kernel.function_exported?/3 and Kernel.macro_exported?/3 " <>
                               "to check for public functions and macros instead"
 
@@ -711,7 +720,8 @@ defmodule Module do
 
     * `:module` - the module atom name
 
-    * `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order
+    * `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order.
+      See `Macro.struct_info!/2` for more information
 
   """
   @callback __info__(:attributes) :: keyword()
@@ -721,7 +731,14 @@ defmodule Module do
   @callback __info__(:md5) :: binary()
   @callback __info__(:module) :: module()
   @callback __info__(:struct) ::
-              list(%{required(:field) => atom(), optional(:default) => term()}) | nil
+              [
+                %{
+                  required(:field) => atom(),
+                  optional(:required) => boolean(),
+                  optional(:default) => term()
+                }
+              ]
+              | nil
 
   @doc """
   Returns information about module attributes used by Elixir.
@@ -918,7 +935,7 @@ defmodule Module do
   when defining the module, while `Kernel.defmodule/2`
   automatically uses the environment it is invoked at.
   """
-  @spec create(module, Macro.t(), Macro.Env.t() | keyword) :: {:module, module, binary, term}
+  @spec create(module, Macro.t(), Macro.Env.t() | create_opts) :: {:module, module, binary, term}
   def create(module, quoted, opts)
 
   def create(module, quoted, %Macro.Env{} = env) when is_atom(module) do
@@ -1423,7 +1440,7 @@ defmodule Module do
       only an interest in fetching the kind and the metadata
 
   """
-  @spec get_definition(module, definition, keyword) ::
+  @spec get_definition(module, definition, get_definition_opts) ::
           {:v1, def_kind, meta :: keyword,
            [{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
           | nil
diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex
index 5841a8b6718..2fdd5c3d5d2 100644
--- a/lib/elixir/lib/module/parallel_checker.ex
+++ b/lib/elixir/lib/module/parallel_checker.ex
@@ -11,9 +11,20 @@ defmodule Module.ParallelChecker do
   @type warning() :: term()
   @type mode() :: :erlang | :elixir | :protocol
 
+  @typedoc """
+  Options for `start_link/1`.
+  """
+  @type start_link_opts :: [
+          {:max_concurrency, pos_integer()}
+          | {:long_verification_threshold, pos_integer()}
+          | {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
+          | {atom(), term()}
+        ]
+
   @doc """
   Initializes the parallel checker process.
   """
+  @spec start_link(start_link_opts()) :: {:ok, cache()}
   def start_link(opts \\ []) do
     :proc_lib.start_link(__MODULE__, :init, [opts])
   end
@@ -412,7 +423,7 @@ defmodule Module.ParallelChecker do
       mode =
         with {^module, binary, _filename} <- object_code,
              {:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
-             {:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
+             {:elixir_checker_v2, contents} <- :erlang.binary_to_term(chunk) do
           # The chunk has more information, so that's our preference
           cache_chunk(table, module, contents)
         else
diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex
index 3917ecf1c0f..10a8c30c10b 100644
--- a/lib/elixir/lib/module/types.ex
+++ b/lib/elixir/lib/module/types.ex
@@ -419,7 +419,7 @@ defmodule Module.Types do
       local_handler: handler,
       # Control if variable refinement is enabled.
       # It is disabled only on dynamic dispatches.
-      refine_vars: true
+      refine_vars: false
     }
   end
 
diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex
index b03889546d6..7fc02f5d719 100644
--- a/lib/elixir/lib/module/types/apply.ex
+++ b/lib/elixir/lib/module/types/apply.ex
@@ -487,7 +487,7 @@ defmodule Module.Types.Apply do
                 {union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
 
               {{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
-                {union(type, fun_from_overlapping_clauses(clauses)), fallback?, context}
+                {union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
 
               {_, context} ->
                 {type, true, context}
@@ -705,7 +705,7 @@ defmodule Module.Types.Apply do
         result =
           case info do
             {:infer, _, clauses} when length(clauses) <= @max_clauses ->
-              fun_from_overlapping_clauses(clauses)
+              fun_from_inferred_clauses(clauses)
 
             _ ->
               dynamic(fun(arity))
@@ -1031,11 +1031,21 @@ defmodule Module.Types.Apply do
 
         Code.ensure_loaded?(mod) and
             Keyword.has_key?(mod.module_info(:attributes), :__protocol__) ->
-          # Protocol errors can be very verbose, so we collapse structs
-          """
-          but expected a type that implements the #{inspect(mod)} protocol, it must be one of:
-          #{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
-          """
+          if function_exported?(mod, :__protocol__, 1) and
+               mod.__protocol__(:impls) == {:consolidated, []} do
+            """
+            but the protocol was not yet implemented for any type and therefore will always fail. \
+            This error typically happens within libraries that define protocols and will disappear as \
+            soon as there is one implementation. If you expect the protocol to be implemented later on, \
+            you can define an implementation specific for development/test.
+            """
+          else
+            # Protocol errors can be very verbose, so we collapse structs
+            """
+            but expected a type that implements the #{inspect(mod)} protocol, it must be one of:
+            #{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
+            """
+          end
 
         true ->
           """
diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex
index 5c18b28a891..765e076467b 100644
--- a/lib/elixir/lib/module/types/descr.ex
+++ b/lib/elixir/lib/module/types/descr.ex
@@ -26,11 +26,28 @@ defmodule Module.Types.Descr do
   @bit_top (1 <<< 7) - 1
   @bit_number @bit_integer ||| @bit_float
 
+  defmacrop domain_key(key), do: {:domain_key, key}
+
+  @domain_key_types [
+    {:domain_key, :binary},
+    {:domain_key, :empty_list},
+    {:domain_key, :integer},
+    {:domain_key, :float},
+    {:domain_key, :pid},
+    {:domain_key, :port},
+    {:domain_key, :reference},
+    {:domain_key, :fun},
+    {:domain_key, :atom},
+    {:domain_key, :tuple},
+    {:domain_key, :map},
+    {:domain_key, :list}
+  ]
+
   @fun_top :fun_top
   @atom_top {:negation, :sets.new(version: 2)}
   @map_top [{:open, %{}, []}]
   @non_empty_list_top [{:term, :term, []}]
-  @tuple_top [{:open, [], []}]
+  @tuple_top [{:open, []}]
   @map_empty [{:closed, %{}, []}]
 
   @none %{}
@@ -46,10 +63,13 @@ defmodule Module.Types.Descr do
   @not_non_empty_list Map.delete(@term, :list)
   @not_list Map.replace!(@not_non_empty_list, :bitmap, @bit_top - @bit_empty_list)
 
-  @empty_intersection [0, @none]
-  @empty_difference [0, []]
+  @not_set %{optional: 1}
+  @term_or_optional Map.put(@term, :optional, 1)
+  @term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1})
+  @not_atom_or_optional Map.delete(@term_or_optional, :atom)
 
-  # Type definitions
+  @empty_intersection [0, []]
+  @empty_difference [0, []]
 
   defguard is_descr(descr) when is_map(descr) or descr == :term
 
@@ -68,7 +88,7 @@ defmodule Module.Types.Descr do
   def atom(as), do: %{atom: atom_new(as)}
   def atom(), do: %{atom: @atom_top}
   def binary(), do: %{bitmap: @bit_binary}
-  def closed_map(pairs), do: map_descr(:closed, pairs)
+  def closed_map(pairs), do: map_descr(:closed, pairs, @term_or_optional, false)
   def empty_list(), do: %{bitmap: @bit_empty_list}
   def empty_map(), do: %{map: @map_empty}
   def integer(), do: %{bitmap: @bit_integer}
@@ -76,7 +96,8 @@ defmodule Module.Types.Descr do
   def list(type), do: list_descr(type, @empty_list, true)
   def non_empty_list(type, tail \\ @empty_list), do: list_descr(type, tail, false)
   def open_map(), do: %{map: @map_top}
-  def open_map(pairs), do: map_descr(:open, pairs)
+  def open_map(pairs), do: map_descr(:open, pairs, @term_or_optional, false)
+  def open_map(pairs, default), do: map_descr(:open, pairs, if_set(default), true)
   def open_tuple(elements, _fallback \\ term()), do: tuple_descr(:open, elements)
   def pid(), do: %{bitmap: @bit_pid}
   def port(), do: %{bitmap: @bit_port}
@@ -137,16 +158,17 @@ defmodule Module.Types.Descr do
   @doc """
   Creates a function from overlapping function clauses.
   """
-  def fun_from_overlapping_clauses(args_clauses) do
+  def fun_from_inferred_clauses(args_clauses) do
     domain_clauses =
       Enum.reduce(args_clauses, [], fn {args, return}, acc ->
-        pivot_overlapping_clause(args_to_domain(args), return, acc)
+        domain = args |> Enum.map(&upper_bound/1) |> args_to_domain()
+        pivot_overlapping_clause(domain, upper_bound(return), acc)
       end)
 
     funs =
       for {domain, return} <- domain_clauses,
           args <- domain_to_args(domain),
-          do: fun(args, return)
+          do: fun(args, dynamic(return))
 
     Enum.reduce(funs, &intersection/2)
   end
@@ -193,9 +215,6 @@ defmodule Module.Types.Descr do
   * For gradual tuple types: processes both dynamic and static components separately,
     then combines them.
 
-  Internally it uses `tuple_reduce/4` with concatenation as the join function
-  and a transform that is simply the identity.
-
   The list of arguments can be flattened into a broad domain by calling:
 
       |> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
@@ -203,24 +222,19 @@ defmodule Module.Types.Descr do
   def domain_to_args(descr) do
     case :maps.take(:dynamic, descr) do
       :error ->
-        tuple_elim_negations_static(descr, &Function.identity/1)
+        unwrap_domain_tuple(descr, fn {:closed, elems} -> elems end)
 
       {dynamic, static} ->
-        tuple_elim_negations_static(static, &Function.identity/1) ++
-          tuple_elim_negations_static(dynamic, fn elems -> Enum.map(elems, &dynamic/1) end)
+        unwrap_domain_tuple(static, fn {:closed, elems} -> elems end) ++
+          unwrap_domain_tuple(dynamic, fn {:closed, elems} -> Enum.map(elems, &dynamic/1) end)
     end
   end
 
-  # Call tuple_reduce to build the simple union of tuples that come from each map literal.
-  # Thus, initial is `[]`, join is concatenation, and the transform of a map literal
-  # with no negations is just to keep the map literal as is.
-  defp tuple_elim_negations_static(%{tuple: dnf} = descr, transform) when map_size(descr) == 1 do
-    tuple_reduce(dnf, [], &Kernel.++/2, fn :closed, elements ->
-      [transform.(elements)]
-    end)
+  defp unwrap_domain_tuple(%{tuple: dnf} = descr, transform) when map_size(descr) == 1 do
+    Enum.map(dnf, transform)
   end
 
-  defp tuple_elim_negations_static(descr, _transform) when descr == %{}, do: []
+  defp unwrap_domain_tuple(descr, _transform) when descr == %{}, do: []
 
   defp domain_to_flat_args(domain, arity) do
     case domain_to_args(domain) do
@@ -240,15 +254,18 @@ defmodule Module.Types.Descr do
   # is equivalent to `%{:foo => integer() or not_set()}`.
   #
   # `not_set()` has no meaning outside of map types.
-
-  @not_set %{optional: 1}
-  @term_or_optional Map.put(@term, :optional, 1)
-  @term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1})
-  @not_atom_or_optional Map.delete(@term_or_optional, :atom)
-
   def not_set(), do: @not_set
+
   def if_set(:term), do: term_or_optional()
-  def if_set(type), do: Map.put(type, :optional, 1)
+
+  # If type contains a :dynamic part, :optional gets added there.
+  def if_set(type) do
+    case type do
+      %{dynamic: dyn} -> Map.put(%{type | dynamic: Map.put(dyn, :optional, 1)}, :optional, 1)
+      _ -> Map.put(type, :optional, 1)
+    end
+  end
+
   defp term_or_optional(), do: @term_or_optional
 
   @compile {:inline,
@@ -257,7 +274,7 @@ defmodule Module.Types.Descr do
     case descr do
       %{dynamic: %{optional: 1}} -> %{dynamic: %{optional: 1}}
       %{optional: 1} -> %{optional: 1}
-      _ -> %{}
+      _ -> @none
     end
   end
 
@@ -407,12 +424,20 @@ defmodule Module.Types.Descr do
   # Returning 0 from the callback is taken as none() for that subtype.
   defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2)
   defp intersection(:bitmap, v1, v2), do: v1 &&& v2
-  defp intersection(:dynamic, v1, v2), do: dynamic_intersection(v1, v2)
   defp intersection(:list, v1, v2), do: list_intersection(v1, v2)
   defp intersection(:map, v1, v2), do: map_intersection(v1, v2)
   defp intersection(:optional, 1, 1), do: 1
   defp intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2)
-  defp intersection(:fun, v1, v2), do: fun_intersection(v1, v2)
+
+  defp intersection(:fun, v1, v2) do
+    bdd = fun_intersection(v1, v2)
+    if bdd == :fun_bottom, do: 0, else: bdd
+  end
+
+  defp intersection(:dynamic, v1, v2) do
+    descr = dynamic_intersection(v1, v2)
+    if descr == @none, do: 0, else: descr
+  end
 
   @doc """
   Computes the difference between two types.
@@ -464,13 +489,21 @@ defmodule Module.Types.Descr do
 
   defp iterator_difference_static(:none, map), do: map
 
-  defp empty_difference_static?(left, :term), do: not Map.has_key?(left, :optional)
+  # This function is designed to compute the difference during subtyping efficiently.
+  # Do not use it for anything else.
+  defp empty_difference_subtype?(%{dynamic: dyn_left} = left, %{dynamic: dyn_right} = right) do
+    # Dynamic will either exist on both sides or on none
+    empty_difference_subtype?(dyn_left, dyn_right) and
+      empty_difference_subtype?(Map.delete(left, :dynamic), Map.delete(right, :dynamic))
+  end
+
+  defp empty_difference_subtype?(left, :term), do: keep_optional(left) == @none
 
-  defp empty_difference_static?(left, right) do
-    iterator_empty_difference_static?(:maps.next(:maps.iterator(unfold(left))), unfold(right))
+  defp empty_difference_subtype?(left, right) do
+    iterator_empty_difference_subtype?(:maps.next(:maps.iterator(unfold(left))), unfold(right))
   end
 
-  defp iterator_empty_difference_static?({key, v1, iterator}, map) do
+  defp iterator_empty_difference_subtype?({key, v1, iterator}, map) do
     case map do
       %{^key => v2} ->
         value = difference(key, v1, v2)
@@ -479,20 +512,23 @@ defmodule Module.Types.Descr do
       %{} ->
         empty_key?(key, v1)
     end and
-      iterator_empty_difference_static?(:maps.next(iterator), map)
+      iterator_empty_difference_subtype?(:maps.next(iterator), map)
   end
 
-  defp iterator_empty_difference_static?(:none, _map), do: true
+  defp iterator_empty_difference_subtype?(:none, _map), do: true
 
   # Returning 0 from the callback is taken as none() for that subtype.
   defp difference(:atom, v1, v2), do: atom_difference(v1, v2)
   defp difference(:bitmap, v1, v2), do: v1 - (v1 &&& v2)
-  defp difference(:dynamic, v1, v2), do: dynamic_difference(v1, v2)
   defp difference(:list, v1, v2), do: list_difference(v1, v2)
   defp difference(:map, v1, v2), do: map_difference(v1, v2)
   defp difference(:optional, 1, 1), do: 0
   defp difference(:tuple, v1, v2), do: tuple_difference(v1, v2)
-  defp difference(:fun, v1, v2), do: fun_difference(v1, v2)
+
+  defp difference(:fun, v1, v2) do
+    bdd = fun_difference(v1, v2)
+    if bdd == :fun_bottom, do: 0, else: bdd
+  end
 
   @doc """
   Compute the negation of a type.
@@ -522,20 +558,20 @@ defmodule Module.Types.Descr do
         not Map.has_key?(descr, :atom) and
           not Map.has_key?(descr, :bitmap) and
           not Map.has_key?(descr, :optional) and
+          not Map.has_key?(descr, :tuple) and
           (not Map.has_key?(descr, :map) or map_empty?(descr.map)) and
           (not Map.has_key?(descr, :list) or list_empty?(descr.list)) and
-          (not Map.has_key?(descr, :tuple) or tuple_empty?(descr.tuple)) and
           (not Map.has_key?(descr, :fun) or fun_empty?(descr.fun))
     end
   end
 
-  # For atom, bitmap, and optional, if the key is present,
+  defp empty_or_optional?(type), do: empty?(remove_optional(type))
+
+  # For atom, bitmap, tuple, and optional, if the key is present,
   # then they are not empty,
   defp empty_key?(:fun, value), do: fun_empty?(value)
   defp empty_key?(:map, value), do: map_empty?(value)
   defp empty_key?(:list, value), do: list_empty?(value)
-  defp empty_key?(:tuple, value), do: tuple_empty?(value)
-  defp empty_key?(:dynamic, value), do: empty?(value)
   defp empty_key?(_, _value), do: false
 
   @doc """
@@ -614,6 +650,11 @@ defmodule Module.Types.Descr do
 
   @doc """
   Converts a descr to its quoted string representation.
+
+  ## Options
+
+    * `:collapse_structs` - do not show struct fields that match
+      their default type
   """
   def to_quoted_string(descr, opts \\ []) do
     descr
@@ -662,7 +703,7 @@ defmodule Module.Types.Descr do
   end
 
   defp subtype_static?(same, same), do: true
-  defp subtype_static?(left, right), do: empty_difference_static?(left, right)
+  defp subtype_static?(left, right), do: empty_difference_subtype?(left, right)
 
   @doc """
   Check if a type is equal to another.
@@ -1158,7 +1199,7 @@ defmodule Module.Types.Descr do
     with {:ok, domain, static_arrows, dynamic_arrows} <-
            fun_normalize_both(fun_static, fun_dynamic, arity) do
       cond do
-        empty?(args_domain) ->
+        Enum.any?(arguments, &empty?/1) ->
           {:badarg, domain_to_flat_args(domain, arity)}
 
         not subtype?(args_domain, domain) ->
@@ -1169,25 +1210,21 @@ defmodule Module.Types.Descr do
           end
 
         static? ->
-          {:ok, fun_apply_static(arguments, static_arrows, false)}
+          {:ok, fun_apply_static(arguments, static_arrows)}
 
         static_arrows == [] ->
           # TODO: We need to validate this within the theory
-          {:ok, dynamic(fun_apply_static(arguments, dynamic_arrows, false))}
+          arguments = Enum.map(arguments, &upper_bound/1)
+          {:ok, dynamic(fun_apply_static(arguments, dynamic_arrows))}
 
         true ->
           # For dynamic cases, combine static and dynamic results
-          {static_args, dynamic_args, maybe_empty?} =
-            if args_dynamic? do
-              {Enum.map(arguments, &upper_bound/1), Enum.map(arguments, &lower_bound/1), true}
-            else
-              {arguments, arguments, false}
-            end
+          arguments = Enum.map(arguments, &upper_bound/1)
 
           {:ok,
            union(
-             fun_apply_static(static_args, static_arrows, false),
-             dynamic(fun_apply_static(dynamic_args, dynamic_arrows, maybe_empty?))
+             fun_apply_static(arguments, static_arrows),
+             dynamic(fun_apply_static(arguments, dynamic_arrows))
            )}
       end
     end
@@ -1287,26 +1324,12 @@ defmodule Module.Types.Descr do
     :badfun
   end
 
-  defp fun_apply_static(arguments, arrows, maybe_empty?) do
+  defp fun_apply_static(arguments, arrows) do
     type_args = args_to_domain(arguments)
 
-    # Optimization: short-circuits when inner loop is none() or outer loop is term()
-    if maybe_empty? and empty?(type_args) do
-      Enum.reduce_while(arrows, none(), fn intersection_of_arrows, acc ->
-        Enum.reduce_while(intersection_of_arrows, term(), fn
-          {_dom, _ret}, acc when acc == @none -> {:halt, acc}
-          {_dom, ret}, acc -> {:cont, intersection(acc, ret)}
-        end)
-        |> case do
-          :term -> {:halt, :term}
-          inner -> {:cont, union(inner, acc)}
-        end
-      end)
-    else
-      Enum.reduce(arrows, none(), fn intersection_of_arrows, acc ->
-        aux_apply(acc, type_args, term(), intersection_of_arrows)
-      end)
-    end
+    Enum.reduce(arrows, none(), fn intersection_of_arrows, acc ->
+      aux_apply(acc, type_args, term(), intersection_of_arrows)
+    end)
   end
 
   # Helper function for function application that handles the application of
@@ -1327,9 +1350,9 @@ defmodule Module.Types.Descr do
     if subtype?(rets_reached, result), do: result, else: union(result, rets_reached)
   end
 
-  defp aux_apply(result, input, returns_reached, [{dom, ret} | arrow_intersections]) do
+  defp aux_apply(result, input, returns_reached, [{args, ret} | arrow_intersections]) do
     # Calculate the part of the input not covered by this arrow's domain
-    dom_subtract = difference(input, args_to_domain(dom))
+    dom_subtract = difference(input, args_to_domain(args))
 
     # Refine the return type by intersecting with this arrow's return type
     ret_refine = intersection(returns_reached, ret)
@@ -1426,7 +1449,7 @@ defmodule Module.Types.Descr do
           # determines emptiness.
           length(neg_arguments) == positive_arity and
             subtype?(args_to_domain(neg_arguments), positive_domain) and
-            phi_starter(neg_arguments, negation(neg_return), positives)
+            phi_starter(neg_arguments, neg_return, positives)
         end)
     end
   end
@@ -1464,27 +1487,74 @@ defmodule Module.Types.Descr do
   #
   # See [Castagna and Lanvin (2024)](https://arxiv.org/abs/2408.14345), Theorem 4.2.
   defp phi_starter(arguments, return, positives) do
-    n = length(arguments)
-    # Arity mismatch: if there is one positive function with a different arity,
-    # then it cannot be a subtype of the (arguments->type) functions.
-    if Enum.any?(positives, fn {args, _ret} -> length(args) != n end) do
-      false
+    # Optimization: When all positive functions have non-empty domains,
+    # we can simplify the phi function check to a direct subtyping test.
+    # This avoids the expensive recursive phi computation by checking only that applying the
+    # input to the positive intersection yields a subtype of the return
+    if all_non_empty_domains?([{arguments, return} | positives]) do
+      fun_apply_static(arguments, [positives])
+      |> subtype?(return)
     else
-      arguments = Enum.map(arguments, &{false, &1})
-      phi(arguments, {false, return}, positives)
+      n = length(arguments)
+      # Arity mismatch: functions with different arities cannot be subtypes
+      # of the target function type (arguments -> return)
+      if Enum.any?(positives, fn {args, _ret} -> length(args) != n end) do
+        false
+      else
+        # Initialize memoization cache for the recursive phi computation
+        arguments = Enum.map(arguments, &{false, &1})
+        {result, _cache} = phi(arguments, {false, negation(return)}, positives, %{})
+        result
+      end
     end
   end
 
-  defp phi(args, {b, t}, []) do
-    Enum.any?(args, fn {bool, typ} -> bool and empty?(typ) end) or (b and empty?(t))
+  defp phi(args, {b, t}, [], cache) do
+    {Enum.any?(args, fn {bool, typ} -> bool and empty?(typ) end) or (b and empty?(t)), cache}
   end
 
-  defp phi(args, {b, ret}, [{arguments, return} | rest_positive]) do
-    phi(args, {true, intersection(ret, return)}, rest_positive) and
-      Enum.all?(Enum.with_index(arguments), fn {type, index} ->
-        List.update_at(args, index, fn {_, arg} -> {true, difference(arg, type)} end)
-        |> phi({b, ret}, rest_positive)
-      end)
+  defp phi(args, {b, ret}, [{arguments, return} | rest_positive], cache) do
+    # Create cache key from function arguments
+    cache_key = {args, {b, ret}, [{arguments, return} | rest_positive]}
+
+    case cache do
+      %{^cache_key => value} ->
+        {value, cache}
+
+      %{} ->
+        # Compute result and cache it
+        {result1, cache} = phi(args, {true, intersection(ret, return)}, rest_positive, cache)
+
+        if not result1 do
+          cache = Map.put(cache, cache_key, false)
+          {false, cache}
+        else
+          {_index, result2, cache} =
+            Enum.reduce_while(arguments, {0, true, cache}, fn
+              type, {index, acc_result, acc_cache} ->
+                {new_result, new_cache} =
+                  args
+                  |> List.update_at(index, fn {_, arg} -> {true, difference(arg, type)} end)
+                  |> phi({b, ret}, rest_positive, acc_cache)
+
+                if new_result do
+                  {:cont, {index + 1, acc_result and new_result, new_cache}}
+                else
+                  {:halt, {index + 1, false, new_cache}}
+                end
+            end)
+
+          result = result1 and result2
+          cache = Map.put(cache, cache_key, result)
+          {result, cache}
+        end
+    end
+  end
+
+  defp all_non_empty_domains?(positives) do
+    Enum.all?(positives, fn {args, _ret} ->
+      Enum.all?(args, fn arg -> not empty?(arg) end)
+    end)
   end
 
   defp fun_union(bdd1, bdd2) do
@@ -1809,17 +1879,14 @@ defmodule Module.Types.Descr do
       acc ->
         inter = intersection(list_type1, list_type2)
         last = intersection(last_type1, last_type2)
+        negs = negs1 ++ negs2
 
-        if empty?(inter) or empty?(last) do
-          acc
-        else
-          [{inter, last, negs1 ++ negs2} | acc]
+        cond do
+          :lists.member({inter, last}, negs) -> acc
+          empty?(inter) or empty?(last) -> acc
+          true -> [{inter, last, negs} | acc]
         end
     end
-    |> case do
-      [] -> 0
-      dnf -> dnf
-    end
   end
 
   # Computes the difference between two DNF (Disjunctive Normal Form) list types.
@@ -1831,6 +1898,10 @@ defmodule Module.Types.Descr do
   #    b) If only the last type differs, subtracts it
   # 3. Base case: adds dnf2 type to negations of dnf1 type
   # The result may be larger than the initial dnf1, which is maintained in the accumulator.
+  defp list_difference(_, dnf) when dnf == @non_empty_list_top do
+    []
+  end
+
   defp list_difference(dnf1, dnf2) do
     Enum.reduce(dnf2, dnf1, fn {t2, last2, negs2}, acc_dnf1 ->
       last2 = list_tail_unfold(last2)
@@ -1842,7 +1913,12 @@ defmodule Module.Types.Descr do
           Enum.reduce(negs2, [], fn {nt, nlast}, nacc ->
             t = intersection(t1, nt)
             last = intersection(last1, nlast)
-            if empty?(t) or empty?(last), do: nacc, else: [{t, last, negs1} | nacc]
+
+            cond do
+              :lists.member({t, last}, negs1) -> nacc
+              empty?(t) or empty?(last) -> nacc
+              true -> [{t, last, negs1} | nacc]
+            end
           end)
 
         i = intersection(t1, t2)
@@ -1858,6 +1934,8 @@ defmodule Module.Types.Descr do
     end)
   end
 
+  defp list_empty?(@non_empty_list_top), do: false
+
   defp list_empty?(dnf) do
     Enum.all?(dnf, fn {list_type, last_type, negs} ->
       last_type = list_tail_unfold(last_type)
@@ -2116,18 +2194,8 @@ defmodule Module.Types.Descr do
   defp dynamic_intersection(left, right),
     do: symmetrical_intersection(unfold(left), unfold(right), &intersection/3)
 
-  defp dynamic_difference(left, right) do
-    case difference_static(left, right) do
-      value when value == @none -> 0
-      value -> value
-    end
-  end
-
   defp dynamic_to_quoted(descr, opts) do
     cond do
-      descr == %{} ->
-        []
-
       # We check for :term literally instead of using term_type?
       # because we check for term_type? in to_quoted before we
       # compute the difference(dynamic, static).
@@ -2137,6 +2205,9 @@ defmodule Module.Types.Descr do
       single = indivisible_bitmap(descr, opts) ->
         [single]
 
+      empty?(descr) ->
+        []
+
       true ->
         case non_term_type_to_quoted(descr, opts) do
           {:none, _meta, []} = none -> [none]
@@ -2175,34 +2246,79 @@ defmodule Module.Types.Descr do
   # is the union of `%{..., a: atom(), b: if_set(not integer())}` and
   # `%{..., a: if_set(not atom()), b: integer()}`. For maps with more keys,
   # each key in a negated literal may create a new union when eliminated.
+  #
+  # Instead of a tag :open or :closed, we can also use a map of domains which
+  # specifies for each defined key domain (@domain_key_types) the type associated with
+  # those keys.
+  #
+  # For instance, the type `%{atom() => if_set(integer())}` is the type of maps where atom keys
+  # map to integers, without any non-atom keys. It is represented using the map literal
+  # `{%{atom: if_set(integer())}, [], []}`, with no defined keys or negations.
+  #
+  # The type `%{..., atom() => integer()}` represents maps with atom keys bound to integers,
+  # and other keys bound to any type. It will be represented using a map domain that maps
+  # atom to `if_set(integer())`, and every other domain key to `term_or_optional()`.
+
+  defp map_descr(tag, pairs, default, force?) do
+    {fields, domains, dynamic?} = map_descr_pairs(pairs, [], %{}, false)
+
+    map_new =
+      if domains != %{} or force? do
+        domains =
+          if tag == :open do
+            Enum.reduce(@domain_key_types, domains, &Map.put_new(&2, &1, default))
+          else
+            domains
+          end
 
-  defp map_descr(tag, fields) do
-    case map_descr_pairs(fields, [], false) do
-      {fields, true} ->
-        %{dynamic: %{map: map_new(tag, fields |> Enum.reverse() |> :maps.from_list())}}
+        map_new(domains, fields)
+      else
+        map_new(tag, fields)
+      end
 
-      {_, false} ->
-        %{map: map_new(tag, :maps.from_list(fields))}
+    case dynamic? do
+      true -> %{dynamic: %{map: map_new}}
+      false -> %{map: map_new}
     end
   end
 
-  defp map_descr_pairs([{key, :term} | rest], acc, dynamic?) do
-    map_descr_pairs(rest, [{key, :term} | acc], dynamic?)
+  # TODO: Double check if we indeed want the union here
+  # when we start using domain types from Elixir itself
+  defp map_put_domain(domain, key, value) do
+    Map.update(domain, key, if_set(value), &union(&1, value))
   end
 
-  defp map_descr_pairs([{key, value} | rest], acc, dynamic?) do
-    case :maps.take(:dynamic, value) do
-      :error -> map_descr_pairs(rest, [{key, value} | acc], dynamic?)
-      {dynamic, _static} -> map_descr_pairs(rest, [{key, dynamic} | acc], true)
+  defp map_descr_pairs([{key, :term} | rest], fields, domain, dynamic?) do
+    case is_atom(key) do
+      true -> map_descr_pairs(rest, [{key, :term} | fields], domain, dynamic?)
+      false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, :term), dynamic?)
     end
   end
 
-  defp map_descr_pairs([], acc, dynamic?) do
-    {acc, dynamic?}
+  defp map_descr_pairs([{key, value} | rest], fields, domain, dynamic?) do
+    {value, dynamic?} =
+      case :maps.take(:dynamic, value) do
+        :error -> {value, dynamic?}
+        {dynamic, _static} -> {dynamic, true}
+      end
+
+    case is_atom(key) do
+      true -> map_descr_pairs(rest, [{key, value} | fields], domain, dynamic?)
+      false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, value), dynamic?)
+    end
+  end
+
+  defp map_descr_pairs([], fields, domain, dynamic?) do
+    {fields |> Enum.reverse() |> :maps.from_list(), domain, dynamic?}
   end
 
-  defp tag_to_type(:open), do: term_or_optional()
-  defp tag_to_type(:closed), do: not_set()
+  defp tuple_tag_to_type(:open), do: term_or_optional()
+  defp tuple_tag_to_type(:closed), do: not_set()
+
+  # Gets the default type associated to atom keys in a map.
+  defp map_key_tag_to_type(:open), do: term_or_optional()
+  defp map_key_tag_to_type(:closed), do: not_set()
+  defp map_key_tag_to_type(domains = %{}), do: Map.get(domains, domain_key(:atom), not_set())
 
   defguardp is_optional_static(map)
             when is_map(map) and is_map_key(map, :optional)
@@ -2329,24 +2445,11 @@ defmodule Module.Types.Descr do
       acc ->
         try do
           {tag, fields} = map_literal_intersection(tag1, pos1, tag2, pos2)
-          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
+          prepend_to_map_dnf(tag, fields, negs1 ++ (negs2 -- negs1), acc)
         catch
           :empty -> acc
         end
     end
-    |> case do
-      [] -> 0
-      acc -> acc
-    end
   end
 
   # Intersects two map literals; throws if their intersection is empty.
@@ -2383,6 +2486,58 @@ defmodule Module.Types.Descr do
     :maps.iterator(open) |> :maps.next() |> map_literal_intersection_loop(closed)
   end
 
+  # At least one tag is a tag-domain pair.
+  defp map_literal_intersection(tag_or_domains1, map1, tag_or_domains2, map2) do
+    # For a closed map with domains intersected with an open map with domains:
+    # 1. The result is closed (more restrictive)
+    # 2. We need to check each domain in the open map against the closed map
+    default1 = map_key_tag_to_type(tag_or_domains1)
+    default2 = map_key_tag_to_type(tag_or_domains2)
+
+    # Compute the new domain
+    tag_or_domains = map_domain_intersection(tag_or_domains1, tag_or_domains2)
+
+    # Go over all fields in map1 and map2 with default atom types atom1 and atom2
+    # 1. If key is in both maps, compute non empty intersection (:error if it is none)
+    # 2. If key is only in map1, compute non empty intersection with atom2
+    # 3. If key is only in map2, compute non empty intersection with atom1
+    # We do that by computing intersection on all key labels in both map1 and map2,
+    # using default values when a key is not present.
+    {tag_or_domains,
+     symmetrical_merge(map1, default1, map2, default2, fn _key, v1, v2 ->
+       non_empty_intersection!(v1, v2)
+     end)}
+  end
+
+  # Compute the intersection of two tags or tag-domain pairs.
+  defp map_domain_intersection(:closed, _), do: :closed
+  defp map_domain_intersection(_, :closed), do: :closed
+  defp map_domain_intersection(:open, tag_or_domains), do: tag_or_domains
+  defp map_domain_intersection(tag_or_domains, :open), do: tag_or_domains
+
+  defp map_domain_intersection(domains1 = %{}, domains2 = %{}) do
+    new_domains =
+      for {domain_key(_) = domain_key, type1} <- domains1, reduce: %{} do
+        acc_domains ->
+          case domains2 do
+            %{^domain_key => type2} ->
+              inter = intersection(type1, type2)
+
+              if empty_or_optional?(inter) do
+                acc_domains
+              else
+                Map.put(acc_domains, domain_key, inter)
+              end
+
+            _ ->
+              acc_domains
+          end
+      end
+
+    # If the explicit domains are empty, use simple atom tags
+    if map_size(new_domains) == 0, do: :closed, else: new_domains
+  end
+
   defp map_literal_intersection_loop(:none, acc), do: {:closed, acc}
 
   defp map_literal_intersection_loop({key, type1, iterator}, acc) do
@@ -2405,38 +2560,61 @@ defmodule Module.Types.Descr do
     if empty?(type), do: throw(:empty), else: type
   end
 
+  defp map_difference(_, dnf) when dnf == @map_top, do: []
+  defp map_difference(dnf, dnf), do: []
+
   defp map_difference(dnf1, dnf2) do
     Enum.reduce(dnf2, dnf1, fn
-      # Optimization: we are removing an open map with one field.
-      {:open, fields2, []}, dnf1 when map_size(fields2) == 1 ->
-        Enum.reduce(dnf1, [], fn {tag1, fields1, negs1}, acc ->
+      {:open, fields2, []}, current_dnf when map_size(fields2) == 1 ->
+        # Optimization: we are removing an open map with one field.
+        Enum.reduce(current_dnf, [], fn {tag1, fields1, negs1}, acc ->
           {key, value, _rest} = :maps.next(:maps.iterator(fields2))
-          t_diff = difference(Map.get(fields1, key, tag_to_type(tag1)), value)
+          t_diff = difference(Map.get(fields1, key, map_key_tag_to_type(tag1)), value)
 
           if empty?(t_diff) do
             acc
           else
-            [{tag1, Map.put(fields1, key, t_diff), negs1} | acc]
+            {tag, pos} = {tag1, Map.put(fields1, key, t_diff)}
+            entry = {tag, pos, negs1}
+
+            cond do
+              :lists.member({tag, pos}, negs1) -> acc
+              :lists.member(entry, acc) -> acc
+              true -> [entry | acc]
+            end
           end
         end)
 
-      {tag2, fields2, negs2}, dnf1 ->
-        Enum.reduce(dnf1, [], fn {tag1, fields1, negs1}, acc ->
-          acc = [{tag1, fields1, [{tag2, fields2} | negs1]} | acc]
+      {tag2, fields2, negs2}, current_dnf ->
+        Enum.reduce(current_dnf, [], fn {tag1, fields1, negs1}, acc ->
+          negs =
+            if :lists.member({tag2, fields2}, negs1) do
+              negs1
+            else
+              [{tag2, fields2} | negs1]
+            end
+
+          acc = prepend_to_map_dnf(tag1, fields1, negs, acc)
 
           Enum.reduce(negs2, acc, fn {neg_tag2, neg_fields2}, acc ->
             try do
               {tag, fields} = map_literal_intersection(tag1, fields1, neg_tag2, neg_fields2)
-              [{tag, fields, negs1} | acc]
+              prepend_to_map_dnf(tag, fields, negs1, acc)
             catch
               :empty -> acc
             end
           end)
         end)
     end)
-    |> case do
-      [] -> 0
-      acc -> acc
+  end
+
+  defp prepend_to_map_dnf(tag, fields, negs, acc) do
+    entry = {tag, fields, negs}
+
+    cond do
+      :lists.member({tag, fields}, negs) -> acc
+      :lists.member(entry, acc) -> acc
+      true -> [entry | acc]
     end
   end
 
@@ -2483,11 +2661,9 @@ defmodule Module.Types.Descr do
 
   # Optimization: if the key does not exist in the map, avoid building
   # if_set/not_set pairs and return the popped value directly.
-  defp map_fetch_static(%{map: [{tag, fields, []}]}, key) when not is_map_key(fields, key) do
-    case tag do
-      :open -> {true, term()}
-      :closed -> {true, none()}
-    end
+  defp map_fetch_static(%{map: [{tag_or_domains, fields, []}]}, key)
+       when not is_map_key(fields, key) do
+    map_key_tag_to_type(tag_or_domains) |> pop_optional_static()
   end
 
   # Takes a map dnf and returns the union of types it can take for a given key.
@@ -2495,15 +2671,13 @@ defmodule Module.Types.Descr do
   defp map_fetch_static(%{map: dnf}, key) do
     dnf
     |> Enum.reduce(none(), fn
-      # Optimization: if there are no negatives,
-      # we can return the value directly.
+      # Optimization: if there are no negatives and key exists, return its value
       {_tag, %{^key => value}, []}, acc ->
         value |> union(acc)
 
-      # Optimization: if there are no negatives
-      # and the key does not exist, return the default one.
+      # Optimization: if there are no negatives and the key does not exist, return the default one.
       {tag, %{}, []}, acc ->
-        tag_to_type(tag) |> union(acc)
+        map_key_tag_to_type(tag) |> union(acc)
 
       {tag, fields, negs}, acc ->
         {fst, snd} = map_pop_key(tag, fields, key)
@@ -2559,6 +2733,154 @@ defmodule Module.Types.Descr do
     end
   end
 
+  @doc """
+  Refreshes the type of map after assuming some type was given to a key of a given type.
+  Assuming that the descr is exclusively a map (or dynamic).
+  """
+  # TODO: Figure out how this operation will be used from Elixir
+  def map_refresh(:term, _key, _type), do: :badmap
+
+  def map_refresh(descr, key_descr, type) do
+    {dynamic_descr, static_descr} = Map.pop(descr, :dynamic)
+    key_descr = unfold(key_descr)
+    type = unfold(type)
+
+    cond do
+      # Either 1) static part is a map, or 2) static part is empty and dynamic part contains maps
+      not map_only?(static_descr) ->
+        :badmap
+
+      empty?(static_descr) and not (not is_nil(dynamic_descr) and descr_key?(dynamic_descr, :map)) ->
+        :badmap
+
+      # Either of those three types could be dynamic.
+      not (not is_nil(dynamic_descr) or Map.has_key?(key_descr, :dynamic) or
+               Map.has_key?(type, :dynamic)) ->
+        map_refresh_static(descr, key_descr, type)
+
+      true ->
+        # If one of those is dynamic, we just compute the union
+        {descr_dynamic, descr_static} = Map.pop(descr, :dynamic, descr)
+        {key_dynamic, key_static} = Map.pop(key_descr, :dynamic, key_descr)
+        {type_dynamic, type_static} = Map.pop(type, :dynamic, type)
+
+        with {:ok, new_static} <- map_refresh_static(descr_static, key_static, type_static),
+             {:ok, new_dynamic} <- map_refresh_static(descr_dynamic, key_dynamic, type_dynamic) do
+          {:ok, union(new_static, dynamic(new_dynamic))}
+        end
+    end
+  end
+
+  def map_refresh_static(%{map: _} = descr, key_descr = %{}, type) do
+    # Check if descr is a valid map,
+    case atom_fetch(key_descr) do
+      # If the key_descr is a singleton, we directly put the type into the map.
+      {:finite, [single_key]} ->
+        map_put(descr, single_key, type)
+
+      # In this case, we iterate on key_descr to add type to each key type it covers.
+      # Since we do not know which key will be used, we do the union with previous types.
+      _ ->
+        new_descr =
+          key_descr
+          |> covered_key_types()
+          |> Enum.reduce(descr, fn
+            {:atom, atom_key}, acc ->
+              map_refresh_atom(acc, atom_key, type)
+
+            domain_key, acc ->
+              map_refresh_domain(acc, domain_key, type)
+          end)
+
+        {:ok, new_descr}
+    end
+  end
+
+  def map_refresh_static(:term, _key_descr, _type), do: {:ok, open_map()}
+  def map_refresh_static(_, _, _), do: {:ok, none()}
+
+  @doc """
+  Updates a key in a map type by fetching its current type, unioning it with a
+  `new_additional_type`, and then putting the resulting union type back.
+
+  Returns:
+    - `{:ok, new_map_descr}`: If successful.
+    - `:badmap`: If the input `descr` is not a valid map type.
+    - `:badkey`: If the key is considered invalid during the take operation (e.g.,
+      an optional key that resolves to an empty type).
+  """
+  # TODO: Figure out how this operation will be used from Elixir
+  def map_refresh_key(descr, key, new_additional_type) when is_atom(key) do
+    case map_fetch(descr, key) do
+      :badmap ->
+        :badmap
+
+      # Key is not present: we just add the new one and make it optional.
+      :badkey ->
+        with {:ok, descr} <- map_put(descr, key, if_set(new_additional_type)) do
+          descr
+        end
+
+      {_optional?, current_key_type} ->
+        type_to_put = union(current_key_type, new_additional_type)
+
+        case map_fetch_and_put(descr, key, type_to_put) do
+          {_taken_type, new_map_descr} -> new_map_descr
+          # Propagates :badmap or :badkey from map_fetch_and_put
+          error -> error
+        end
+    end
+  end
+
+  def map_refresh_domain(%{map: [{tag, fields, []}]}, domain, type) do
+    %{map: [{map_refresh_tag(tag, domain, type), fields, []}]}
+  end
+
+  def map_refresh_domain(%{map: dnf}, domain, type) do
+    Enum.map(dnf, fn
+      {tag, fields, []} ->
+        {map_refresh_tag(tag, domain, type), fields, []}
+
+      {tag, fields, negs} ->
+        # For negations, we count on the idea that a negation will not remove any
+        # type from a domain unless it completely cancels out the type.
+        # So for any non-empty map dnf, we just update the domain with the new type,
+        # as well as its negations to keep them accurate.
+        {map_refresh_tag(tag, domain, type), fields,
+         Enum.map(negs, fn {neg_tag, neg_fields} ->
+           {map_refresh_tag(neg_tag, domain, type), neg_fields}
+         end)}
+    end)
+  end
+
+  def map_refresh_atom(descr = %{map: dnf}, atom_key, type) do
+    case atom_key do
+      {:union, keys} ->
+        keys
+        |> :sets.to_list()
+        |> Enum.reduce(descr, fn key, acc -> map_refresh_key(acc, key, type) end)
+
+      {:negation, keys} ->
+        # 1) Fetch all the possible keys in the dnf
+        # 2) Get them all, except the ones in neg_atoms
+        possible_keys = map_fetch_all_key_names(dnf)
+        considered_keys = :sets.subtract(possible_keys, keys)
+
+        considered_keys
+        |> :sets.to_list()
+        |> Enum.reduce(descr, fn key, acc -> map_refresh_key(acc, key, type) end)
+        |> map_refresh_domain(domain_key(:atom), type)
+    end
+  end
+
+  def map_refresh_tag(tag_or_domains, domain_key, type) do
+    case tag_or_domains do
+      :open -> :open
+      :closed -> %{domain_key => if_set(type)}
+      domains = %{} -> Map.update(domains, domain_key, if_set(type), &union(&1, type))
+    end
+  end
+
   defp map_put_shared(descr, key, type) do
     with {nil, descr} <- map_take(descr, key, nil, &map_put_static(&1, key, type)) do
       {:ok, descr}
@@ -2591,6 +2913,249 @@ defmodule Module.Types.Descr do
     end
   end
 
+  @doc """
+  Computes the union of types for keys matching `key_type` within the `map_type`.
+
+  This generalizes `map_fetch/2` (which operates on a single literal key) to
+  work with a key type (e.g., `atom()`, `integer()`, `:a or :b`). It's based
+  on the map-selection operator t.[t'] described in Section 4.2 of "Typing Records,
+  Maps, and Structs" (Castagna et al., ICFP 2023).
+
+  ## Return Values
+
+  The function returns a tuple indicating the outcome and the resulting type union:
+
+  * `{:ok, type}`: Standard success. `type` is the resulting union of types
+    found for the matching keys. This covers two sub-cases:
+      * **Keys definitely exist:** If `disjoint?(type, not_set())` is true,
+        all keys matching `key_type` are guaranteed to exist.
+      * **Keys may exist:** If `type` includes `not_set()`, some keys
+        matching `key_type` might exist (contributing their types) while
+        others might be absent (contributing `not_set()`).
+
+  * `{:ok_absent, type}`: Success, but the resulting `type` is `none()` or a
+    subtype of `not_set()`. This indicates that no key matching `key_type`
+    can exist with a value other than `not_set()`. The caller may wish to
+    issue a warning, as this often implies selecting a field that is
+    effectively undefined.
+
+  # TODO: implement/decide if worth it (it's from the paper)
+  * `{:ok_spillover, type}`: Success, and `type` is the resulting union.
+    However, this indicates that the `key_type` included keys not explicitly
+    covered by the `map_type`'s fields or domain specifications. The
+    projection relied on the map's default behavior (e.g., the `term()`
+    value type for unspecified keys in an open map). The caller may wish to
+    issue a warning, as this could conceal issues like selecting keys
+    not intended by the map's definition.
+
+  * `:badmap`: The input `map_type` was invalid (e.g., not a map type or
+    a dynamic type wrapping a map type).
+
+  * `:badkeytype`: The input `key_type` was invalid (e.g., not a subtype
+    of the allowed key types like `atom()`, `integer()`, etc.).
+  """
+  # TODO: Figure out how to use this operation from Elixir
+  def map_get(:term, _key_descr), do: :badmap
+
+  def map_get(%{} = descr, key_descr) do
+    case :maps.take(:dynamic, descr) do
+      :error ->
+        if descr_key?(descr, :map) and map_only?(descr) do
+          {optional?, type_selected} = map_get_static(descr, key_descr) |> pop_optional_static()
+
+          cond do
+            empty?(type_selected) -> {:ok_absent, atom([nil])}
+            optional? -> {:ok, nil_or_type(type_selected)}
+            true -> {:ok_present, type_selected}
+          end
+        else
+          :badmap
+        end
+
+      {dynamic, static} ->
+        if descr_key?(dynamic, :map) and map_only?(static) do
+          {optional_dynamic?, dynamic_type} =
+            map_get_static(dynamic, key_descr) |> pop_optional_static()
+
+          {optional_static?, static_type} =
+            map_get_static(static, key_descr) |> pop_optional_static()
+
+          type_selected = union(dynamic(dynamic_type), static_type)
+
+          cond do
+            empty?(type_selected) -> {:ok_absent, atom([nil])}
+            optional_dynamic? or optional_static? -> {:ok, nil_or_type(type_selected)}
+            true -> {:ok_present, type_selected}
+          end
+        else
+          :badmap
+        end
+    end
+  end
+
+  # Returns the list of key types that are covered by the key_descr.
+  # E.g., for `{atom([:ok]), term} or integer()` it returns `[:tuple, :integer]`.
+  # We treat bitmap types as a separate key type.
+  defp covered_key_types(:term), do: @domain_key_types
+
+  defp covered_key_types(key_descr) do
+    for {type_kind, type} <- key_descr, reduce: [] do
+      acc ->
+        cond do
+          type_kind == :atom -> [{:atom, type} | acc]
+          type_kind == :bitmap -> bitmap_to_domain_keys(type) ++ acc
+          not empty?(%{type_kind => type}) -> [domain_key(type_kind) | acc]
+          true -> acc
+        end
+    end
+  end
+
+  defp bitmap_to_domain_keys(bitmap) do
+    [
+      if((bitmap &&& @bit_binary) != 0, do: domain_key(:binary)),
+      if((bitmap &&& @bit_empty_list) != 0, do: domain_key(:empty_list)),
+      if((bitmap &&& @bit_integer) != 0, do: domain_key(:integer)),
+      if((bitmap &&& @bit_float) != 0, do: domain_key(:float)),
+      if((bitmap &&& @bit_pid) != 0, do: domain_key(:pid)),
+      if((bitmap &&& @bit_port) != 0, do: domain_key(:port)),
+      if((bitmap &&& @bit_reference) != 0, do: domain_key(:reference))
+    ]
+    |> Enum.reject(&is_nil/1)
+  end
+
+  defp nil_or_type(type), do: union(type, atom([nil]))
+
+  defp unfold_domains(:closed), do: %{}
+
+  defp unfold_domains(:open),
+    do: Map.new(@domain_key_types, fn domain_key -> {domain_key, @term_or_optional} end)
+
+  defp unfold_domains(domains = %{}), do: domains
+
+  defp map_get_static(%{map: [{tag_or_domains, fields, []}]}, key_descr) do
+    # For each non-empty kind of type in the key_descr, we add the corresponding key domain in a union.
+    domains = unfold_domains(tag_or_domains)
+
+    key_descr
+    |> covered_key_types()
+    |> Enum.reduce(none(), fn
+      # Note: we could stop if we reach term_or_optional()
+      {:atom, atom_type}, acc -> map_get_atom([{domains, fields, []}], atom_type) |> union(acc)
+      key_type, acc -> Map.get(domains, key_type, not_set()) |> union(acc)
+    end)
+  end
+
+  defp map_get_static(%{map: dnf}, key_descr) do
+    key_descr
+    |> covered_key_types()
+    |> Enum.reduce(none(), fn
+      {:atom, atom_type}, acc ->
+        map_get_atom(dnf, atom_type) |> union(acc)
+
+      domain_key, acc ->
+        map_get_domain(dnf, domain_key) |> union(acc)
+    end)
+  end
+
+  defp map_get_static(%{}, _key), do: not_set()
+  defp map_get_static(:term, _key), do: term_or_optional()
+
+  # Given a map dnf return the union of types for a given atom type. Handles two cases:
+  # 1. A union of atoms (e.g., `{:union, atoms}`):
+  #    - Iterates through each atom in the union.
+  #    - Fetches the type for each atom and combines them into a union.
+  #
+  # 2. A negation of atoms (e.g., `{:negation, atoms}`):
+  #    - Fetches all possible keys in the map's DNF.
+  #    - Excludes the negated atoms from the considered keys.
+  #    - Includes the domain of all atoms in the map's DNF.
+  #
+  # Example:
+  #   Fetching a key of type `atom() and not (:a)` from a map of type
+  #   `%{a: atom(), b: float(), atom() => pid()}`
+  #   would return either `nil` or `float()` (key `:b`) or `pid()` (key `atom()`), but not `atom()` (key `:a`).
+  defp map_get_atom(dnf, atom_type) do
+    case atom_type do
+      {:union, atoms} ->
+        atoms
+        |> :sets.to_list()
+        |> Enum.reduce(none(), fn atom, acc ->
+          {static_optional?, type} = map_fetch_static(%{map: dnf}, atom)
+
+          if static_optional? do
+            union(type, acc) |> nil_or_type() |> if_set()
+          else
+            union(type, acc)
+          end
+        end)
+
+      {:negation, atoms} ->
+        # 1) Fetch all the possible keys in the dnf
+        # 2) Get them all, except the ones in neg_atoms
+        possible_keys = map_fetch_all_key_names(dnf)
+        considered_keys = :sets.subtract(possible_keys, atoms)
+
+        considered_keys
+        |> :sets.to_list()
+        |> Enum.reduce(none(), fn atom, acc ->
+          {static_optional?, type} = map_fetch_static(%{map: dnf}, atom)
+
+          if static_optional? do
+            union(type, acc) |> nil_or_type() |> if_set()
+          else
+            union(type, acc)
+          end
+        end)
+        |> union(map_get_domain(dnf, domain_key(:atom)))
+    end
+  end
+
+  # Fetch all present keys in a map dnf (including negated ones).
+  defp map_fetch_all_key_names(dnf) do
+    dnf
+    |> Enum.reduce(:sets.new(version: 2), fn {_tag, fields, negs}, acc ->
+      keys = :sets.from_list(Map.keys(fields))
+
+      # Add all the negative keys
+      # Example: %{...} and not %{a: not_set()} makes key :a present in the map
+      Enum.reduce(negs, keys, fn {_tag, neg_fields}, acc ->
+        :sets.from_list(Map.keys(neg_fields)) |> :sets.union(acc)
+      end)
+      |> :sets.union(acc)
+    end)
+  end
+
+  # Take a map dnf and return the union of types for the given key domain.
+  defp map_get_domain(dnf, domain_key(_) = domain_key) do
+    dnf
+    |> Enum.reduce(none(), fn
+      {tag, _fields, []}, acc when is_atom(tag) ->
+        map_key_tag_to_type(tag) |> union(acc)
+
+      # Optimization: if there are no negatives and domains exists, return its value
+      {%{^domain_key => value}, _fields, []}, acc ->
+        value |> union(acc)
+
+      # Optimization: if there are no negatives and the key does not exist, return the default type.
+      {domains = %{}, _fields, []}, acc ->
+        map_key_tag_to_type(domains) |> union(acc)
+
+      {tag_or_domains, fields, negs}, acc ->
+        {fst, snd} = map_pop_domain(tag_or_domains, fields, domain_key)
+
+        case map_split_negative_domain(negs, domain_key) do
+          :empty ->
+            acc
+
+          negative ->
+            negative
+            |> pair_make_disjoint()
+            |> pair_eliminate_negations_fst(fst, snd)
+            |> union(acc)
+        end
+    end)
+  end
+
   @doc """
   Removes a key from a map type and return its type.
 
@@ -2701,53 +3266,102 @@ defmodule Module.Types.Descr do
   defp map_empty?(:open, fs, [{:closed, _} | negs]), do: map_empty?(:open, fs, negs)
 
   defp map_empty?(tag, fields, [{neg_tag, neg_fields} | negs]) do
-    (Enum.all?(neg_fields, fn {neg_key, neg_type} ->
-       cond do
-         # Keys that are present in the negative map, but not in the positive one
-         is_map_key(fields, neg_key) ->
-           true
-
-         # The key is not shared between positive and negative maps,
-         # if the negative type is optional, then there may be a value in common
-         tag == :closed ->
-           is_optional_static(neg_type)
-
-         # There may be value in common
-         tag == :open ->
-           diff = difference(term_or_optional(), neg_type)
-           empty?(diff) or map_empty?(tag, Map.put(fields, neg_key, diff), negs)
-       end
-     end) and
-       Enum.all?(fields, fn {key, type} ->
-         case neg_fields do
-           %{^key => neg_type} ->
-             diff = difference(type, neg_type)
-             empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs)
-
-           %{} ->
-             cond do
-               neg_tag == :open ->
-                 true
-
-               neg_tag == :closed and not is_optional_static(type) ->
-                 false
-
-               true ->
-                 # an absent key in a open negative map can be ignored
-                 diff = difference(type, tag_to_type(neg_tag))
-                 empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs)
-             end
+    if map_check_domain_keys(tag, neg_tag) do
+      atom_default = map_key_tag_to_type(tag)
+      neg_atom_default = map_key_tag_to_type(neg_tag)
+
+      (Enum.all?(neg_fields, fn {neg_key, neg_type} ->
+         cond do
+           # Ignore keys present in both maps; will be handled below
+           is_map_key(fields, neg_key) ->
+             true
+
+           # The key is not shared between positive and negative maps,
+           # if the negative type is optional, then there may be a value in common
+           tag == :closed ->
+             is_optional_static(neg_type)
+
+           # There may be value in common
+           tag == :open ->
+             diff = difference(term_or_optional(), neg_type)
+             empty?(diff) or map_empty?(tag, Map.put(fields, neg_key, diff), negs)
+
+           true ->
+             diff = difference(atom_default, neg_type)
+             empty?(diff) or map_empty?(tag, Map.put(fields, neg_key, diff), negs)
          end
-       end)) or map_empty?(tag, fields, negs)
+       end) and
+         Enum.all?(fields, fn {key, type} ->
+           case neg_fields do
+             %{^key => neg_type} ->
+               diff = difference(type, neg_type)
+               empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs)
+
+             %{} ->
+               cond do
+                 neg_tag == :open ->
+                   true
+
+                 neg_tag == :closed and not is_optional_static(type) ->
+                   false
+
+                 true ->
+                   # an absent key in a open negative map can be ignored
+                   diff = difference(type, neg_atom_default)
+                   empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs)
+               end
+           end
+         end)) or map_empty?(tag, fields, negs)
+    else
+      map_empty?(tag, fields, negs)
+    end
+  end
+
+  # Verify the domain condition from equation (22) in paper ICFP'23 https://www.irif.fr/~gc/papers/icfp23.pdf
+  # which is that every domain key type in the positive map is a subtype
+  # of the corresponding domain key type in the negative map.
+  defp map_check_domain_keys(:closed, _), do: true
+  defp map_check_domain_keys(_, :open), do: true
+
+  # An open map is a subtype iff the negative domains are all present as term_or_optional()
+  defp map_check_domain_keys(:open, neg_domains) do
+    map_size(neg_domains) == length(@domain_key_types) and
+      Enum.all?(neg_domains, fn {domain_key(_), type} -> subtype?(term_or_optional(), type) end)
+  end
+
+  # A positive domains is smaller than a closed map iff all its keys are empty or optional
+  defp map_check_domain_keys(pos_domains, :closed) do
+    Enum.all?(pos_domains, fn {domain_key(_), type} -> empty_or_optional?(type) end)
+  end
+
+  # Component-wise comparison of domains
+  defp map_check_domain_keys(pos_domains, neg_domains) do
+    Enum.all?(pos_domains, fn {domain_key(_) = domain_key, type} ->
+      subtype?(type, Map.get(neg_domains, domain_key, not_set()))
+    end)
   end
 
   defp map_pop_key(tag, fields, key) do
     case :maps.take(key, fields) do
       {value, fields} -> {value, %{map: map_new(tag, fields)}}
-      :error -> {tag_to_type(tag), %{map: map_new(tag, fields)}}
+      :error -> {map_key_tag_to_type(tag), %{map: map_new(tag, fields)}}
+    end
+  end
+
+  # Pop a domain type, e.g. popping integers from %{integer() => binary()}
+  # returns {if_set(binary()), %{integer() => if_set(binary()}}
+  # If the domain is not present, use the tag to type as default.
+  defp map_pop_domain(domains = %{}, fields, domain_key) do
+    case :maps.take(domain_key, domains) do
+      {value, domains} -> {if_set(value), %{map: map_new(domains, fields)}}
+      :error -> {map_key_tag_to_type(domains), %{map: map_new(domains, fields)}}
     end
   end
 
+  # Atom case
+  defp map_pop_domain(tag, fields, _domain_key),
+    do: {map_key_tag_to_type(tag), %{map: map_new(tag, fields)}}
+
   defp map_split_negative(negs, key) do
     Enum.reduce_while(negs, [], fn
       # A negation with an open map means the whole thing is empty.
@@ -2756,6 +3370,13 @@ defmodule Module.Types.Descr do
     end)
   end
 
+  defp map_split_negative_domain(negs, domain_key) do
+    Enum.reduce_while(negs, [], fn
+      {:open, fields}, _acc when map_size(fields) == 0 -> {:halt, :empty}
+      {tag, fields}, neg_acc -> {:cont, [map_pop_domain(tag, fields, domain_key) | neg_acc]}
+    end)
+  end
+
   # Use heuristics to normalize a map dnf for pretty printing.
   defp map_normalize(dnfs) do
     for dnf <- dnfs, not map_empty?([dnf]) do
@@ -2874,11 +3495,26 @@ defmodule Module.Types.Descr do
     {:map, [], []}
   end
 
+  def map_literal_to_quoted({domains = %{}, fields}, _opts)
+      when map_size(domains) == 0 and map_size(fields) == 0 do
+    {:empty_map, [], []}
+  end
+
   def map_literal_to_quoted({:open, %{__struct__: @not_atom_or_optional} = fields}, _opts)
       when map_size(fields) == 1 do
     {:non_struct_map, [], []}
   end
 
+  def map_literal_to_quoted({domains = %{}, fields}, opts) do
+    domain_fields =
+      for {domain_key(domain_type), value_type} <- domains do
+        {{domain_type, [], []}, map_value_to_quoted(value_type, opts)}
+      end
+
+    regular_fields_quoted = map_fields_to_quoted(:closed, Enum.sort(fields), opts)
+    {:%{}, [], domain_fields ++ regular_fields_quoted}
+  end
+
   def map_literal_to_quoted({tag, fields}, opts) do
     case tag do
       :closed ->
@@ -2932,13 +3568,17 @@ defmodule Module.Types.Descr do
           literal_to_quoted(key)
         end
 
-      {optional?, type} = pop_optional_static(type)
+      {key, map_value_to_quoted(type, opts)}
+    end
+  end
+
+  defp map_value_to_quoted(type, opts) do
+    {optional?, type} = pop_optional_static(type)
 
-      cond do
-        not optional? -> {key, to_quoted(type, opts)}
-        empty?(type) -> {key, {:not_set, [], []}}
-        true -> {key, {:if_set, [], [to_quoted(type, opts)]}}
-      end
+    cond do
+      not optional? -> to_quoted(type, opts)
+      empty?(type) -> {:not_set, [], []}
+      true -> {:if_set, [], [to_quoted(type, opts)]}
     end
   end
 
@@ -2960,6 +3600,7 @@ defmodule Module.Types.Descr do
 
   defp tuple_descr(tag, fields) do
     case tuple_descr(fields, [], false) do
+      :empty -> %{}
       {fields, true} -> %{dynamic: %{tuple: tuple_new(tag, Enum.reverse(fields))}}
       {_, false} -> %{tuple: tuple_new(tag, fields)}
     end
@@ -2970,9 +3611,29 @@ defmodule Module.Types.Descr do
   end
 
   defp tuple_descr([value | rest], acc, dynamic?) do
-    case :maps.take(:dynamic, value) do
-      :error -> tuple_descr(rest, [value | acc], dynamic?)
-      {dynamic, _static} -> tuple_descr(rest, [dynamic | acc], true)
+    # Check if the static part is empty
+    static_empty? =
+      case value do
+        # Has dynamic component, check static separately
+        %{dynamic: _} -> false
+        _ -> empty?(value)
+      end
+
+    if static_empty? do
+      :empty
+    else
+      case :maps.take(:dynamic, value) do
+        :error ->
+          tuple_descr(rest, [value | acc], dynamic?)
+
+        {dynamic, _static} ->
+          # Check if dynamic component is empty
+          if empty?(dynamic) do
+            :empty
+          else
+            tuple_descr(rest, [dynamic | acc], true)
+          end
+      end
     end
   end
 
@@ -2980,16 +3641,16 @@ defmodule Module.Types.Descr do
     {acc, dynamic?}
   end
 
-  defp tuple_new(tag, elements), do: [{tag, elements, []}]
+  defp tuple_new(tag, elements), do: [{tag, elements}]
 
   defp tuple_intersection(dnf1, dnf2) do
-    for {tag1, elements1, negs1} <- dnf1,
-        {tag2, elements2, negs2} <- dnf2,
+    for {tag1, elements1} <- dnf1,
+        {tag2, elements2} <- dnf2,
         reduce: [] do
       acc ->
         case tuple_literal_intersection(tag1, elements1, tag2, elements2) do
           {tag, elements} ->
-            entry = {tag, elements, negs1 ++ negs2}
+            entry = {tag, elements}
 
             case :lists.member(entry, acc) do
               true -> acc
@@ -3000,10 +3661,6 @@ defmodule Module.Types.Descr do
             acc
         end
     end
-    |> case do
-      [] -> 0
-      acc -> acc
-    end
   end
 
   defp tuple_literal_intersection(tag1, elements1, tag2, elements2) do
@@ -3038,30 +3695,76 @@ defmodule Module.Types.Descr do
     zip_non_empty_intersection!(rest1, rest2, [non_empty_intersection!(type1, type2) | acc])
   end
 
-  defp tuple_difference(dnf1, dnf2) do
-    Enum.reduce(dnf2, dnf1, fn {tag2, elements2, negs2}, dnf1 ->
-      Enum.reduce(dnf1, [], fn {tag1, elements1, negs1}, acc ->
-        # Prune negations that have no values in common
-        acc =
-          case tuple_literal_intersection(tag1, elements1, tag2, elements2) do
-            :empty -> [{tag1, elements1, negs1}] ++ acc
-            _ -> [{tag1, elements1, [{tag2, elements2} | negs1]}] ++ acc
-          end
+  defp tuple_difference(_, dnf) when dnf == @tuple_top do
+    0
+  end
 
-        Enum.reduce(negs2, acc, fn {neg_tag2, neg_elements2}, inner_acc ->
-          case tuple_literal_intersection(tag1, elements1, neg_tag2, neg_elements2) do
-            :empty -> inner_acc
-            {tag, fields} -> [{tag, fields, negs1} | inner_acc]
-          end
-        end)
+  defp tuple_difference(dnf1, dnf2) do
+    Enum.reduce(dnf2, dnf1, fn {tag2, elements2}, dnf1 ->
+      Enum.reduce(dnf1, [], fn {tag1, elements1}, acc ->
+        tuple_eliminate_single_negation(tag1, elements1, {tag2, elements2})
+        |> tuple_union(acc)
       end)
     end)
-    |> case do
-      [] -> 0
-      acc -> acc
+  end
+
+  defp tuple_eliminate_single_negation(tag, elements, {neg_tag, neg_elements}) do
+    n = length(elements)
+    m = length(neg_elements)
+
+    # Scenarios where the difference is guaranteed to be empty:
+    # 1. When removing larger tuples from a fixed-size positive tuple
+    # 2. When removing smaller tuples from larger tuples
+    if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do
+      [{tag, elements}]
+    else
+      tuple_union(
+        tuple_elim_content([], tag, elements, neg_elements),
+        tuple_elim_size(n, m, tag, elements, neg_tag)
+      )
+    end
+  end
+
+  # Eliminates negations according to tuple content.
+  defp tuple_elim_content(_acc, _tag, _elements, []), do: []
+
+  # Subtracts each element of a negative tuple to build a new tuple with the difference.
+  # Example: {number(), atom()} and not {float(), :foo} contains types {integer(), :foo}
+  # as well as {float(), atom() and not :foo}
+  # Same process as tuple_elements_empty?
+  defp tuple_elim_content(acc, tag, elements, [neg_type | neg_elements]) do
+    {ty, elements} = List.pop_at(elements, 0, term())
+    diff = difference(ty, neg_type)
+
+    res = tuple_elim_content([ty | acc], tag, elements, neg_elements)
+
+    if empty?(diff) do
+      res
+    else
+      [{tag, Enum.reverse(acc, [diff | elements])} | res]
     end
   end
 
+  # Eliminates negations according to size
+  # Example: {integer(), ...} and not {term(), term(), ...} contains {integer()}
+  # The tuples to consider are all those of size n to m - 1, and if the negative tuple is
+  # closed, we also need to consider tuples of size greater than m + 1.
+  defp tuple_elim_size(_, _, :closed, _, _), do: []
+
+  defp tuple_elim_size(n, m, :open, elements, neg_tag) do
+    n..(m - 1)//1
+    |> Enum.reduce([], fn i, acc ->
+      [{:closed, tuple_fill(elements, i)} | acc]
+    end)
+    |> Kernel.++(
+      if neg_tag == :open do
+        []
+      else
+        [{:open, tuple_fill(elements, m + 1)}]
+      end
+    )
+  end
+
   defp tuple_union(dnf1, dnf2) do
     # Union is just concatenation, but we rely on some optimization strategies to
     # avoid the list to grow when possible
@@ -3077,14 +3780,14 @@ defmodule Module.Types.Descr do
     end
   end
 
-  defp maybe_optimize_tuple_union({tag1, pos1, []} = tuple1, {tag2, pos2, []} = tuple2) do
+  defp maybe_optimize_tuple_union({tag1, pos1} = tuple1, {tag2, pos2} = tuple2) do
     case tuple_union_optimization_strategy(tag1, pos1, tag2, pos2) do
       :all_equal ->
         tuple1
 
       {:one_index_difference, index, v1, v2} ->
         new_pos = List.replace_at(pos1, index, union(v1, v2))
-        {tag1, new_pos, []}
+        {tag1, new_pos}
 
       :left_subtype_of_right ->
         tuple2
@@ -3165,9 +3868,8 @@ defmodule Module.Types.Descr do
 
   defp tuple_to_quoted(dnf, opts) do
     dnf
-    |> tuple_simplify()
     |> tuple_fusion()
-    |> Enum.map(&tuple_each_to_quoted(&1, opts))
+    |> Enum.map(&tuple_literal_to_quoted(&1, opts))
   end
 
   # Given a dnf of tuples, fuses the tuple unions when possible,
@@ -3179,14 +3881,9 @@ 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
-    {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
+    dnf
+    |> Enum.group_by(fn {tag, elems} -> {tag, length(elems)} end)
+    |> Enum.flat_map(fn {_, tuples} -> tuple_non_negated_fuse(tuples) end)
   end
 
   defp tuple_non_negated_fuse(tuples) do
@@ -3206,20 +3903,6 @@ defmodule Module.Types.Descr do
     end
   end
 
-  defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}, opts) do
-    case negative_tuples do
-      [] ->
-        tuple_literal_to_quoted({tag, positive_tuple}, opts)
-
-      _ ->
-        negative_tuples
-        |> non_empty_map_or(&tuple_literal_to_quoted(&1, opts))
-        |> Kernel.then(
-          &{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}, opts), {:not, [], [&1]}]}
-        )
-    end
-  end
-
   defp tuple_literal_to_quoted({:closed, []}, _opts), do: {:{}, [], []}
 
   defp tuple_literal_to_quoted({tag, elements}, opts) do
@@ -3240,56 +3923,6 @@ defmodule Module.Types.Descr do
     end
   end
 
-  # Check if a tuple represented in DNF is empty
-  defp tuple_empty?(dnf) do
-    Enum.all?(dnf, fn {tag, pos, negs} -> tuple_empty?(tag, pos, negs) end)
-  end
-
-  # No negations, so not empty unless there's an empty type
-  defp tuple_empty?(_, pos, []), do: Enum.any?(pos, &empty?/1)
-  # Open empty negation makes it empty
-  defp tuple_empty?(_, _, [{:open, []} | _]), do: true
-  # Open positive can't be emptied by a single closed negative
-  defp tuple_empty?(:open, pos, [{:closed, _}]), do: Enum.any?(pos, &empty?/1)
-
-  defp tuple_empty?(tag, elements, [{neg_tag, neg_elements} | negs]) do
-    n = length(elements)
-    m = length(neg_elements)
-
-    # Scenarios where the difference is guaranteed to be empty:
-    # 1. When removing larger tuples from a fixed-size positive tuple
-    # 2. When removing smaller tuples from larger tuples
-    if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do
-      tuple_empty?(tag, elements, negs)
-    else
-      tuple_elements_empty?([], tag, elements, neg_elements, negs) and
-        tuple_compatibility(n, m, tag, elements, neg_tag, negs)
-    end
-  end
-
-  # Recursively check elements for emptiness
-  defp tuple_elements_empty?(_, _, _, [], _), do: true
-
-  defp tuple_elements_empty?(acc, tag, elements, [neg_type | neg_elements], negs) do
-    # Handles the case where {tag, elements} is an open tuple, like {:open, []}
-    {ty, elements} = List.pop_at(elements, 0, term())
-    diff = difference(ty, neg_type)
-
-    (empty?(diff) or tuple_empty?(tag, Enum.reverse(acc, [diff | elements]), negs)) and
-      tuple_elements_empty?([ty | acc], tag, elements, neg_elements, negs)
-  end
-
-  # Determines if the set difference is empty when:
-  # - Positive tuple: {tag, elements} of size n
-  # - Negative tuple: open or closed tuples of size m
-  defp tuple_compatibility(n, m, tag, elements, neg_tag, negs) do
-    # The tuples to consider are all those of size n to m - 1, and if the negative tuple is
-    # closed, we also need to consider tuples of size greater than m + 1.
-    tag == :closed or
-      (Enum.all?(n..(m - 1)//1, &tuple_empty?(:closed, tuple_fill(elements, &1), negs)) and
-         (neg_tag == :open or tuple_empty?(:open, tuple_fill(elements, m + 1), negs)))
-  end
-
   @doc """
   Fetches the type of the value returned by accessing `index` on `tuple`
   with the assumption that the descr is exclusively a tuple (or dynamic).
@@ -3368,43 +4001,23 @@ defmodule Module.Types.Descr do
 
   defp tuple_fetch_static(descr, index) when is_integer(index) do
     case descr do
-      :term ->
-        {true, term()}
-
-      %{tuple: tuple} ->
-        tuple_get(tuple, index)
-        |> pop_optional_static()
-
-      %{} ->
-        {false, none()}
+      :term -> {true, term()}
+      %{tuple: tuple} -> tuple_get(tuple, index) |> pop_optional_static()
+      %{} -> {false, none()}
     end
   end
 
   defp tuple_get(dnf, index) do
     Enum.reduce(dnf, none(), fn
-      # Optimization: if there are no negatives, just return the type at that index.
-      {tag, elements, []}, acc ->
-        Enum.at(elements, index, tag_to_type(tag)) |> union(acc)
-
-      {tag, elements, negs}, acc ->
-        {fst, snd} = tuple_pop_index(tag, elements, index)
-
-        case tuple_split_negative(negs, index) do
-          :empty ->
-            acc
-
-          negative ->
-            negative
-            |> pair_make_disjoint()
-            |> pair_eliminate_negations_fst(fst, snd)
-            |> union(acc)
-        end
+      {tag, elements}, acc -> Enum.at(elements, index, tuple_tag_to_type(tag)) |> union(acc)
     end)
   end
 
   @doc """
   Returns all of the values that are part of a tuple.
   """
+  def tuple_values(descr) when descr == %{}, do: :badtuple
+
   def tuple_values(descr) do
     case :maps.take(:dynamic, descr) do
       :error ->
@@ -3425,100 +4038,16 @@ defmodule Module.Types.Descr do
   end
 
   defp process_tuples_values(dnf) do
-    tuple_reduce(dnf, none(), &union/2, fn tag, elements ->
+    Enum.reduce(dnf, none(), fn {tag, elements}, acc ->
       cond do
         Enum.any?(elements, &empty?/1) -> none()
         tag == :open -> term()
         tag == :closed -> Enum.reduce(elements, none(), &union/2)
       end
+      |> union(acc)
     end)
   end
 
-  defp tuple_reduce(dnf, initial, join, transform) do
-    Enum.reduce(dnf, initial, fn {tag, elements, negs}, acc ->
-      join.(acc, tuple_reduce(tag, elements, negs, initial, join, transform))
-    end)
-  end
-
-  defp tuple_reduce(tag, elements, [], _init, _join, transform), do: transform.(tag, elements)
-  defp tuple_reduce(_tag, _elements, [{:open, []} | _], initial, _join, _transform), do: initial
-
-  defp tuple_reduce(tag, elements, [{neg_tag, neg_elements} | negs], initial, join, transform) do
-    n = length(elements)
-    m = length(neg_elements)
-
-    if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do
-      tuple_reduce(tag, elements, negs, initial, join, transform)
-    else
-      # Those two functions eliminate the negations, transforming into
-      # a union of tuples to compute their values.
-      elim_content([], tag, elements, neg_elements, negs, initial, join, transform)
-      |> join.(elim_size(n, m, tag, elements, neg_tag, negs, initial, join, transform))
-    end
-  end
-
-  # Eliminates negations according to tuple content.
-  # This means that there are no more neg_elements to subtract -- end the recursion.
-  defp elim_content(_acc, _tag, _elements, [], _, initial, _join, _transform), do: initial
-
-  # Subtracts each element of a negative tuple to build a new tuple with the difference.
-  # Example: {number(), atom()} and not {float(), :foo} contains types {integer(), :foo}
-  # as well as {float(), atom() and not :foo}
-  # Same process as tuple_elements_empty?
-  defp elim_content(acc, tag, elements, [neg_type | neg_elements], negs, init, join, transform) do
-    {ty, elements} = List.pop_at(elements, 0, term())
-    diff = difference(ty, neg_type)
-
-    if empty?(diff) do
-      init
-    else
-      tuple_reduce(tag, Enum.reverse(acc, [diff | elements]), negs, init, join, transform)
-    end
-    |> join.(elim_content([ty | acc], tag, elements, neg_elements, negs, init, join, transform))
-  end
-
-  # Eliminates negations according to size
-  # Example: {integer(), ...} and not {term(), term(), ...} contains {integer()}
-  defp elim_size(_, _, :closed, _, _, _, initial, _join, _transfo), do: initial
-
-  defp elim_size(n, m, tag, elements, neg_tag, negs, initial, join, transform) do
-    n..(m - 1)//1
-    |> Enum.reduce(initial, fn i, acc ->
-      tuple_reduce(:closed, tuple_fill(elements, i), negs, initial, join, transform)
-      |> join.(acc)
-    end)
-    |> join.(
-      if neg_tag == :open do
-        initial
-      else
-        tuple_reduce(tag, tuple_fill(elements, m + 1), negs, initial, join, transform)
-      end
-    )
-  end
-
-  defp tuple_pop_index(tag, elements, index) do
-    case List.pop_at(elements, index) do
-      {nil, _} -> {tag_to_type(tag), %{tuple: [{tag, elements, []}]}}
-      {type, rest} -> {type, %{tuple: [{tag, rest, []}]}}
-    end
-  end
-
-  defp tuple_split_negative(negs, index) do
-    Enum.reduce_while(negs, [], fn
-      {:open, []}, _acc -> {:halt, :empty}
-      {tag, elements}, acc -> {:cont, [tuple_pop_index(tag, elements, index) | acc]}
-    end)
-  end
-
-  # Use heuristics to simplify a tuple dnf for pretty printing.
-  defp tuple_simplify(dnf) do
-    for {tag, elements, negs} <- dnf,
-        not tuple_empty?([{tag, elements, negs}]) do
-      n = length(elements)
-      {tag, elements, Enum.reject(negs, &tuple_empty_negation?(tag, n, &1))}
-    end
-  end
-
   @doc """
   Delete an element from the tuple.
 
@@ -3569,25 +4098,7 @@ defmodule Module.Types.Descr do
 
   # Takes a static map type and removes an index from it.
   defp tuple_delete_static(%{tuple: dnf}, index) do
-    Enum.reduce(dnf, none(), fn
-      # Optimization: if there are no negatives, we can directly remove the element
-      {tag, elements, []}, acc ->
-        union(acc, %{tuple: tuple_new(tag, List.delete_at(elements, index))})
-
-      {tag, elements, negs}, acc ->
-        {fst, snd} = tuple_pop_index(tag, elements, index)
-
-        case tuple_split_negative(negs, index) do
-          :empty ->
-            acc
-
-          negative ->
-            negative
-            |> pair_make_disjoint()
-            |> pair_eliminate_negations_snd(fst, snd)
-            |> union(acc)
-        end
-    end)
+    %{tuple: Enum.map(dnf, fn {tag, elements} -> {tag, List.delete_at(elements, index)} end)}
   end
 
   # If there is no map part to this static type, there is nothing to delete.
@@ -3651,31 +4162,21 @@ defmodule Module.Types.Descr do
 
   defp tuple_insert_static(descr, index, type) do
     Map.update!(descr, :tuple, fn dnf ->
-      Enum.map(dnf, fn {tag, elements, negs} ->
-        {tag, List.insert_at(elements, index, type),
-         Enum.map(negs, fn {neg_tag, neg_elements} ->
-           {neg_tag, List.insert_at(neg_elements, index, type)}
-         end)}
+      Enum.map(dnf, fn {tag, elements} ->
+        {tag, List.insert_at(elements, index, type)}
       end)
     end)
   end
 
-  # Remove useless negations, which denote tuples of incompatible sizes.
-  defp tuple_empty_negation?(tag, n, {neg_tag, neg_elements}) do
-    m = length(neg_elements)
-    (tag == :closed and n < m) or (neg_tag == :closed and n > m)
-  end
-
   defp tuple_of_size_at_least(n) when is_integer(n) and n >= 0 do
-    tuple_descr(:open, List.duplicate(term(), n))
+    %{tuple: tuple_new(:open, List.duplicate(term(), n))}
   end
 
   defp tuple_of_size_at_least_static?(descr, index) do
     case descr do
       %{tuple: dnf} ->
         Enum.all?(dnf, fn
-          {_, elements, []} -> length(elements) >= index
-          entry -> subtype?(%{tuple: [entry]}, tuple_of_size_at_least(index))
+          {_, elements} -> length(elements) >= index
         end)
 
       %{} ->
@@ -3806,9 +4307,9 @@ defmodule Module.Types.Descr do
 
   ## Map helpers
 
+  # Erlang maps:merge_with/3 has to preserve the order in combiner.
+  # We don't care about the order, so we have a faster implementation.
   defp symmetrical_merge(left, right, fun) do
-    # Erlang maps:merge_with/3 has to preserve the order in combiner.
-    # We don't care about the order, so we have a faster implementation.
     if map_size(left) > map_size(right) do
       iterator_merge(:maps.next(:maps.iterator(right)), left, fun)
     else
@@ -3828,9 +4329,44 @@ defmodule Module.Types.Descr do
 
   defp iterator_merge(:none, map, _fun), do: map
 
+  # Perform a symmetrical merge with default values
+  defp symmetrical_merge(left, left_default, right, right_default, fun) do
+    iterator = :maps.next(:maps.iterator(left))
+    iterator_merge_left(iterator, left_default, right, right_default, %{}, fun)
+  end
+
+  defp iterator_merge_left({key, v1, iterator}, v1_default, map, v2_default, acc, fun) do
+    value =
+      case map do
+        %{^key => v2} -> fun.(key, v1, v2)
+        %{} -> fun.(key, v1, v2_default)
+      end
+
+    acc = Map.put(acc, key, value)
+    iterator_merge_left(:maps.next(iterator), v1_default, map, v2_default, acc, fun)
+  end
+
+  defp iterator_merge_left(:none, v1_default, map, _v2_default, acc, fun) do
+    iterator_merge_right(:maps.next(:maps.iterator(map)), v1_default, acc, fun)
+  end
+
+  defp iterator_merge_right({key, v2, iterator}, v1_default, acc, fun) do
+    acc =
+      case acc do
+        %{^key => _} -> acc
+        %{} -> Map.put(acc, key, fun.(key, v1_default, v2))
+      end
+
+    iterator_merge_right(:maps.next(iterator), v1_default, acc, fun)
+  end
+
+  defp iterator_merge_right(:none, _v1_default, acc, _fun) do
+    acc
+  end
+
+  # Erlang maps:intersect_with/3 has to preserve the order in combiner.
+  # We don't care about the order, so we have a faster implementation.
   defp symmetrical_intersection(left, right, fun) do
-    # Erlang maps:intersect_with/3 has to preserve the order in combiner.
-    # We don't care about the order, so we have a faster implementation.
     if map_size(left) > map_size(right) do
       iterator_intersection(:maps.next(:maps.iterator(right)), left, [], fun)
     else
diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex
index 2aafe9ea6ce..6893035530c 100644
--- a/lib/elixir/lib/module/types/expr.ex
+++ b/lib/elixir/lib/module/types/expr.ex
@@ -231,21 +231,36 @@ defmodule Module.Types.Expr do
   # %Struct{map | ...}
   # This syntax is deprecated, so we simply traverse.
   def of_expr(
-        {:%, _, [_, {:%{}, _, [{:|, _, [map, args]}]}]} = struct,
+        {:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]} = struct,
         _expected,
         expr,
         stack,
         context
       ) do
-    {_, context} = of_expr(map, term(), struct, stack, context)
+    {map_type, context} = of_expr(map, term(), struct, stack, context)
 
     context =
-      Enum.reduce(args, context, fn {key, value}, context when is_atom(key) ->
-        {_, context} = of_expr(value, term(), expr, stack, context)
+      if stack.mode == :traversal do
         context
-      end)
+      else
+        with {false, struct_key_type} <- map_fetch(map_type, :__struct__),
+             {:finite, [^module]} <- atom_fetch(struct_key_type) do
+          context
+        else
+          _ ->
+            error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
+        end
+      end
 
-    {dynamic(), context}
+    Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
+      # TODO: Once we support typed structs, we need to type check them here
+      {type, context} = of_expr(value, term(), expr, stack, context)
+
+      case map_fetch_and_put(acc, key, type) do
+        {_value, acc} -> {acc, context}
+        _ -> {acc, context}
+      end
+    end)
   end
 
   # %{...}
@@ -340,7 +355,7 @@ defmodule Module.Types.Expr do
             add_inferred(acc, args, body)
         end)
 
-      {fun_from_overlapping_clauses(acc), context}
+      {fun_from_inferred_clauses(acc), context}
     end
   end
 
@@ -461,7 +476,11 @@ defmodule Module.Types.Expr do
     {args_types, context} =
       Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
 
-    Apply.fun_apply(fun_type, args_types, call, stack, context)
+    if stack.mode == :traversal do
+      {dynamic(), context}
+    else
+      Apply.fun_apply(fun_type, args_types, call, stack, context)
+    end
   end
 
   def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context)
@@ -791,6 +810,57 @@ defmodule Module.Types.Expr do
 
   ## Warning formatting
 
+  def format_diagnostic({:badupdate, type, expr, context}) do
+    {:%, _, [module, {:%{}, _, [{:|, _, [map, _]}]}]} = expr
+    traces = collect_traces(map, context)
+
+    fix =
+      case map do
+        {var, meta, context} when is_atom(var) and is_atom(context) ->
+          if capture = meta[:capture] do
+            "instead of using &#{capture}, you must define an anonymous function, define a variable and pattern match on \"%#{inspect(module)}{}\""
+          else
+            "when defining the variable \"#{Macro.to_string(map)}\", you must also pattern match on \"%#{inspect(module)}{}\""
+          end
+
+        _ ->
+          "you must assign \"#{Macro.to_string(map)}\" to variable and pattern match on \"%#{inspect(module)}{}\""
+      end
+
+    %{
+      details: %{typing_traces: traces},
+      message:
+        IO.iodata_to_binary([
+          """
+          a struct for #{inspect(module)} is expected on struct update:
+
+              #{expr_to_string(expr, collapse_structs: false) |> indent(4)}
+
+          but got type:
+
+              #{to_quoted_string(type) |> indent(4)}
+          """,
+          format_traces(traces),
+          """
+
+          #{fix}.
+
+          #{hint()} given pattern matching is enough to catch typing errors, \
+          you may optionally convert the struct update into a map update. For \
+          example, instead of:
+
+              user = some_fun()
+              %User{user | name: "John Doe"}
+
+          it is enough to write:
+
+              %User{} = user = some_fun()
+              %{user | name: "John Doe"}
+          """
+        ])
+    }
+  end
+
   def format_diagnostic({:badmap, type, expr, context}) do
     traces = collect_traces(expr, context)
 
diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex
index b13a0e51e08..aea8c0b6074 100644
--- a/lib/elixir/lib/module/types/helpers.ex
+++ b/lib/elixir/lib/module/types/helpers.ex
@@ -141,7 +141,8 @@ defmodule Module.Types.Helpers do
     end)
   end
 
-  defp hint, do: :elixir_errors.prefix(:hint)
+  @doc "The hint prefix"
+  def hint, do: :elixir_errors.prefix(:hint)
 
   @doc """
   Collect traces from variables in expression.
@@ -270,6 +271,10 @@ defmodule Module.Types.Helpers do
   translating inlined Erlang calls back to Elixir.
 
   We also undo some macro expressions done by the Kernel module.
+
+  ## Options
+
+    * `:collapse_structs` - when false, show structs full representation
   """
   def expr_to_string(expr, opts \\ []) do
     string = prewalk_expr_to_string(expr, opts)
@@ -340,6 +345,13 @@ defmodule Module.Types.Helpers do
       {{:., _, [mod, fun]}, meta, args} ->
         erl_to_ex(mod, fun, args, meta)
 
+      {:fn, meta, [{:->, _, [_args, return]}]} = expr ->
+        if meta[:capture] do
+          {:&, meta, [return]}
+        else
+          expr
+        end
+
       {:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]} ->
         {mod, fun} =
           case :elixir_rewrite.erl_to_ex(mod, fun, arity) do
@@ -385,6 +397,13 @@ defmodule Module.Types.Helpers do
           case
         end
 
+      {var, meta, context} = expr when is_atom(var) and is_atom(context) ->
+        if is_integer(meta[:capture]) do
+          {:&, meta, [meta[:capture]]}
+        else
+          expr
+        end
+
       other ->
         other
     end)
diff --git a/lib/elixir/lib/option_parser.ex b/lib/elixir/lib/option_parser.ex
index c68619f6736..6c832cb3c63 100644
--- a/lib/elixir/lib/option_parser.ex
+++ b/lib/elixir/lib/option_parser.ex
@@ -129,6 +129,7 @@ defmodule OptionParser do
     * `:integer` - parses the value as an integer
     * `:float` - parses the value as a float
     * `:string` - parses the value as a string
+    * `:regex` - parses the value as a regular expression with Unicode support
 
   If a switch can't be parsed according to the given type, it is
   returned in the invalid options list.
@@ -282,11 +283,11 @@ defmodule OptionParser do
 
       iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
       ** (OptionParser.ParseError) 1 error found!
-      --limit : Expected type integer, got "xyz"
+      --limit : Expected type integer, got "xyz"...
 
       iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
       ** (OptionParser.ParseError) 1 error found!
-      --unknown : Unknown option
+      --unknown : Unknown option...
 
       iex> OptionParser.parse!(
       ...>   ["-l", "xyz", "-f", "bar"],
@@ -295,7 +296,7 @@ defmodule OptionParser do
       ...> )
       ** (OptionParser.ParseError) 2 errors found!
       -l : Expected type integer, got "xyz"
-      -f : Expected type integer, got "bar"
+      -f : Expected type integer, got "bar"...
 
   """
   @spec parse!(argv, options) :: {parsed, argv}
@@ -354,7 +355,7 @@ defmodule OptionParser do
       ...>   strict: [number: :integer]
       ...> )
       ** (OptionParser.ParseError) 1 error found!
-      --number : Expected type integer, got "lib"
+      --number : Expected type integer, got "lib"...
 
       iex> OptionParser.parse_head!(
       ...>   ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
@@ -362,7 +363,7 @@ defmodule OptionParser do
       ...> )
       ** (OptionParser.ParseError) 2 errors found!
       --verbose : Missing argument of type integer
-      --source : Expected type integer, got "lib"
+      --source : Expected type integer, got "lib"...
 
   """
   @spec parse_head!(argv, options) :: {parsed, argv}
@@ -664,7 +665,7 @@ defmodule OptionParser do
   end
 
   defp validate_switch({_name, type_or_type_and_modifiers}) do
-    valid = [:boolean, :count, :integer, :float, :string, :keep]
+    valid = [:boolean, :count, :integer, :float, :string, :regex, :keep]
     invalid = List.wrap(type_or_type_and_modifiers) -- valid
 
     if invalid != [] do
@@ -704,6 +705,12 @@ defmodule OptionParser do
             _ -> {true, value}
           end
 
+        :regex in kinds ->
+          case Regex.compile(value, "u") do
+            {:ok, regex} -> {false, regex}
+            {:error, _} -> {true, value}
+          end
+
         true ->
           {false, value}
       end
@@ -863,15 +870,20 @@ defmodule OptionParser do
     error_count = length(errors)
     error = if error_count == 1, do: "error", else: "errors"
 
-    "#{error_count} #{error} found!\n" <>
-      Enum.map_join(errors, "\n", &format_error(&1, opts, types))
+    slogan =
+      "#{error_count} #{error} found!\n" <>
+        Enum.map_join(errors, "\n", &format_error(&1, opts, types))
+
+    case format_available_options(opts, types) do
+      "" -> slogan
+      available_options -> slogan <> "\n\n#{available_options}"
+    end
   end
 
   defp format_error({option, nil}, opts, types) do
     if type = get_type(option, opts, types) do
       if String.contains?(option, "_") do
         msg = "#{option} : Unknown option"
-
         msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
       else
         "#{option} : Missing argument of type #{type}"
@@ -891,7 +903,13 @@ defmodule OptionParser do
 
   defp format_error({option, value}, opts, types) do
     type = get_type(option, opts, types)
-    "#{option} : Expected type #{type}, got #{inspect(value)}"
+
+    with :regex <- type,
+         {:error, {reason, position}} <- Regex.compile(value, "u") do
+      "#{option} : Invalid regular expression #{inspect(value)}: #{reason} at position #{position}"
+    else
+      _ -> "#{option} : Expected type #{type}, got #{inspect(value)}"
+    end
   end
 
   defp get_type(option, opts, types) do
@@ -917,4 +935,57 @@ defmodule OptionParser do
     option = String.replace(source, "_", "-")
     if score < current, do: best, else: {option, score}
   end
+
+  defp format_available_options(opts, switches) do
+    reverse_aliases =
+      opts
+      |> Keyword.get(:aliases, [])
+      |> Enum.reduce(%{}, fn {alias, target}, acc ->
+        Map.update(acc, target, [alias], &[alias | &1])
+      end)
+
+    formatted_options =
+      switches
+      |> Enum.sort()
+      |> Enum.map(fn {name, types} ->
+        types = List.wrap(types)
+
+        case types |> List.delete(:keep) |> List.first(:string) do
+          :boolean ->
+            base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
+            add_aliases(base, name, reverse_aliases)
+
+          type ->
+            base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
+            base = add_aliases(base, name, reverse_aliases)
+
+            if :keep in types do
+              base <> " (may be given more than once)"
+            else
+              base
+            end
+        end
+      end)
+
+    if formatted_options == [] do
+      ""
+    else
+      "Supported options:\n" <> Enum.map_join(formatted_options, "\n", &("  " <> &1))
+    end
+  end
+
+  defp add_aliases(base, name, reverse_aliases) do
+    case Map.get(reverse_aliases, name, []) do
+      [] ->
+        base
+
+      alias_list ->
+        alias_str =
+          alias_list
+          |> Enum.sort()
+          |> Enum.map_join(", ", &("-" <> Atom.to_string(&1)))
+
+        base <> " (alias: #{alias_str})"
+    end
+  end
 end
diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex
index cf7b3ae75ab..46461f116c2 100644
--- a/lib/elixir/lib/path.ex
+++ b/lib/elixir/lib/path.ex
@@ -22,6 +22,8 @@ defmodule Path do
   """
   @type t :: IO.chardata()
 
+  @type relative_to_opts :: [force: boolean()]
+
   @doc """
   Converts the given path to an absolute one.
 
@@ -401,7 +403,7 @@ defmodule Path do
       Path.relative_to("../foo", "/usr/local")     #=> "../foo"
 
   """
-  @spec relative_to(t, t, keyword) :: binary
+  @spec relative_to(t, t, relative_to_opts) :: binary
   def relative_to(path, cwd, opts \\ []) when is_list(opts) do
     os_type = major_os_type()
     split_path = split(path)
@@ -479,7 +481,7 @@ defmodule Path do
 
   Check `relative_to/3` for the supported options.
   """
-  @spec relative_to_cwd(t, keyword) :: binary
+  @spec relative_to_cwd(t, relative_to_opts) :: binary
   def relative_to_cwd(path, opts \\ []) when is_list(opts) do
     case :file.get_cwd() do
       {:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
@@ -801,7 +803,7 @@ defmodule Path do
       Path.wildcard("projects/*/ebin/**/*.{beam,app}")
 
   """
-  @spec wildcard(t, keyword) :: [binary]
+  @spec wildcard(t, match_dot: boolean()) :: [binary]
   def wildcard(glob, opts \\ []) when is_list(opts) do
     mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
 
diff --git a/lib/elixir/lib/port.ex b/lib/elixir/lib/port.ex
index 424790c765b..6d1df7196b1 100644
--- a/lib/elixir/lib/port.ex
+++ b/lib/elixir/lib/port.ex
@@ -78,7 +78,7 @@ defmodule Port do
 
   The port can be opened through four main mechanisms.
 
-  As a short summary, prefer to using the `:spawn` and `:spawn_executable`
+  As a short summary, prefer to use the `:spawn` and `:spawn_executable`
   options mentioned below. The other two options, `:spawn_driver` and `:fd`
   are for advanced usage within the VM. Also consider using `System.cmd/3`
   if all you want is to execute a program and retrieve its return value.
diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex
index ebedd282f36..ca9b6142d59 100644
--- a/lib/elixir/lib/protocol.ex
+++ b/lib/elixir/lib/protocol.ex
@@ -669,7 +669,7 @@ defmodule Protocol do
 
             {Descr.term(), clauses, clauses}
           else
-            {domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses}
+            {domain, [{[Descr.term()], Descr.atom()}], clauses}
           end
       end
 
diff --git a/lib/elixir/lib/record.ex b/lib/elixir/lib/record.ex
index 2b1ce2f3559..15d378e27b2 100644
--- a/lib/elixir/lib/record.ex
+++ b/lib/elixir/lib/record.ex
@@ -45,6 +45,13 @@ defmodule Record do
   a module by calling `Code.fetch_docs/1`.
   """
 
+  @type extract_opts :: [
+          from: binary(),
+          from_lib: binary(),
+          includes: [binary()],
+          macros: keyword()
+        ]
+
   @doc """
   Extracts record information from an Erlang file.
 
@@ -102,7 +109,7 @@ defmodule Record do
       ]
 
   """
-  @spec extract(name :: atom, keyword) :: keyword
+  @spec extract(name :: atom, extract_opts) :: keyword
   def extract(name, opts) when is_atom(name) and is_list(opts) do
     Record.Extractor.extract(name, opts)
   end
@@ -119,7 +126,7 @@ defmodule Record do
   Accepts the same options as listed for `Record.extract/2`.
 
   """
-  @spec extract_all(keyword) :: [{name :: atom, keyword}]
+  @spec extract_all(extract_opts) :: [{name :: atom, keyword}]
   def extract_all(opts) when is_list(opts) do
     Record.Extractor.extract_all(opts)
   end
diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex
index 2327f43ed99..8ca5a226d34 100644
--- a/lib/elixir/lib/regex.ex
+++ b/lib/elixir/lib/regex.ex
@@ -164,6 +164,11 @@ defmodule Regex do
 
   @type t :: %__MODULE__{re_pattern: term, source: binary, opts: [term]}
 
+  @type named_captures_opts :: [
+          return: :binary | :index,
+          offset: non_neg_integer()
+        ]
+
   defmodule CompileError do
     @moduledoc """
     An exception raised when a regular expression could not be compiled.
@@ -301,7 +306,7 @@ defmodule Regex do
     * `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
       to see the possible capture values.
     * `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
-      Defaults to zero.
+      Defaults to `0`.
 
   ## Examples
 
@@ -321,7 +326,7 @@ defmodule Regex do
       ["d", ""]
 
   """
-  @spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
+  @spec run(t, binary, capture_opts) :: nil | [binary] | [{integer, integer}]
   def run(regex, string, options \\ [])
 
   def run(%Regex{} = regex, string, options) when is_binary(string) do
@@ -343,6 +348,8 @@ defmodule Regex do
 
     * `:return` - when set to `:index`, returns byte index and match length.
       Defaults to `:binary`.
+    * `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
+      Defaults to `0`.
 
   ## Examples
 
@@ -363,7 +370,7 @@ defmodule Regex do
 
   You can then use `binary_part/3` to fetch the relevant part from the given string.
   """
-  @spec named_captures(t, String.t(), keyword) :: map | nil
+  @spec named_captures(t, String.t(), named_captures_opts) :: map | nil
   def named_captures(regex, string, options \\ []) when is_binary(string) do
     names = names(regex)
     options = Keyword.put(options, :capture, names)
@@ -516,7 +523,7 @@ defmodule Regex do
     * `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
       to see the possible capture values.
     * `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
-      Defaults to zero.
+      Defaults to `0`.
 
   ## Examples
 
@@ -545,7 +552,7 @@ defmodule Regex do
       [["cd"], ["ce"]]
 
   """
-  @spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
+  @spec scan(t(), String.t(), capture_opts) :: [[String.t()]] | [[{integer(), integer()}]]
   def scan(regex, string, options \\ [])
 
   def scan(%Regex{} = regex, string, options) when is_binary(string) do
@@ -573,6 +580,25 @@ defmodule Regex do
     end
   end
 
+  @typedoc """
+  Options for regex functions that capture matches.
+  """
+  @type capture_opts :: [
+          return: :binary | :index,
+          capture: :all | :first | :all_but_first | :none | :all_names | [binary() | atom()],
+          offset: non_neg_integer()
+        ]
+
+  @typedoc """
+  Options for `split/3`.
+  """
+  @type split_opts :: [
+          parts: pos_integer() | :infinity,
+          trim: boolean(),
+          on: :first | :all | :all_but_first | :none | :all_names | [atom() | integer()],
+          include_captures: boolean()
+        ]
+
   @doc """
   Splits the given target based on the given pattern and in the given number of
   parts.
@@ -626,7 +652,7 @@ defmodule Regex do
       ["a", "b", "c"]
 
   """
-  @spec split(t, String.t(), [term]) :: [String.t()]
+  @spec split(t, String.t(), split_opts) :: [String.t()]
   def split(regex, string, options \\ [])
 
   def split(%Regex{}, "", opts) do
diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex
index 17370964701..d35f82bc12a 100644
--- a/lib/elixir/lib/registry.ex
+++ b/lib/elixir/lib/registry.ex
@@ -242,6 +242,11 @@ defmodule Registry do
           {:register, registry, key, registry_partition :: pid, value}
           | {:unregister, registry, key, registry_partition :: pid}
 
+  @typedoc """
+  Options used for `dispatch/4`.
+  """
+  @type dispatch_opts :: [parallel: boolean()]
+
   ## Via callbacks
 
   @doc false
@@ -483,9 +488,15 @@ defmodule Registry do
 
   See the module documentation for examples of using the `dispatch/3`
   function for building custom dispatching or a pubsub system.
+
+  ## Options
+
+    * `:parallel` - if `true`, the dispatching is done in parallel
+      across all partitions. Defaults to `false`.
+
   """
   @doc since: "1.4.0"
-  @spec dispatch(registry, key, dispatcher, keyword) :: :ok
+  @spec dispatch(registry, key, dispatcher, dispatch_opts) :: :ok
         when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [term()]}
   def dispatch(registry, key, mfa_or_fun, opts \\ [])
       when is_atom(registry) and is_function(mfa_or_fun, 1)
diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex
index 626e7d259b3..73d59f31901 100644
--- a/lib/elixir/lib/string.ex
+++ b/lib/elixir/lib/string.ex
@@ -298,6 +298,15 @@ defmodule String do
           | [nonempty_binary]
           | (compiled_search_pattern :: :binary.cp())
 
+  @type split_opts :: [
+          parts: pos_integer() | :infinity,
+          trim: boolean()
+        ]
+
+  @type splitter_opts :: [trim: boolean()]
+
+  @type replace_opts :: [global: boolean()]
+
   @conditional_mappings [:greek, :turkic]
 
   @doc """
@@ -502,7 +511,8 @@ defmodule String do
       ["a", "b", " c  "]
 
   """
-  @spec split(t, pattern | Regex.t(), keyword) :: [t]
+  @spec split(t, pattern, split_opts()) :: [t]
+  @spec split(t, Regex.t(), Regex.split_opts()) :: [t]
   def split(string, pattern, options \\ [])
 
   def split(string, %Regex{} = pattern, options) when is_binary(string) and is_list(options) do
@@ -607,7 +617,7 @@ defmodule String do
       ["1", "2", "3", "4"]
 
   """
-  @spec splitter(t, pattern, keyword) :: Enumerable.t()
+  @spec splitter(t, pattern, splitter_opts) :: Enumerable.t()
   def splitter(string, pattern, options \\ [])
 
   def splitter(string, "", options) when is_binary(string) and is_list(options) do
@@ -1616,7 +1626,7 @@ defmodule String do
       "é"
 
   """
-  @spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), keyword) :: t
+  @spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), replace_opts) :: t
   def replace(subject, pattern, replacement, options \\ [])
       when is_binary(subject) and
              (is_binary(replacement) or is_function(replacement, 1)) and
diff --git a/lib/elixir/lib/string_io.ex b/lib/elixir/lib/string_io.ex
index 550633a7af8..d52ff1c238b 100644
--- a/lib/elixir/lib/string_io.ex
+++ b/lib/elixir/lib/string_io.ex
@@ -17,6 +17,11 @@ defmodule StringIO do
 
   """
 
+  @type open_opts :: [
+          capture_prompt: boolean(),
+          encoding: :unicode | :latin1
+        ]
+
   # We're implementing the GenServer behaviour instead of using the
   # `use GenServer` macro, because we don't want the `child_spec/1`
   # function as it doesn't make sense to be started under a supervisor.
@@ -59,7 +64,7 @@ defmodule StringIO do
 
   """
   @doc since: "1.7.0"
-  @spec open(binary, keyword, (pid -> res)) :: {:ok, res} when res: var
+  @spec open(binary, open_opts, (pid -> res)) :: {:ok, res} when res: var
   def open(string, options, function)
       when is_binary(string) and is_list(options) and is_function(function, 1) do
     {:ok, pid} = GenServer.start(__MODULE__, {self(), string, options}, [])
@@ -83,7 +88,8 @@ defmodule StringIO do
   If options are provided, the result will be `{:ok, pid}`, returning the
   IO device created. The option `:capture_prompt`, when set to `true`, causes
   prompts (which are specified as arguments to `IO.get*` functions) to be
-  included in the device's output.
+  included in the device's output. See `options/3` for the list of supported
+  options.
 
   If a function is provided, the device will be created and sent to the
   function. When the function returns, the device will be closed. The final
@@ -111,7 +117,7 @@ defmodule StringIO do
       {:ok, {"", "The input was foo"}}
 
   """
-  @spec open(binary, keyword) :: {:ok, pid}
+  @spec open(binary, open_opts) :: {:ok, pid}
   @spec open(binary, (pid -> res)) :: {:ok, res} when res: var
   def open(string, options_or_function \\ [])
 
diff --git a/lib/elixir/lib/supervisor.ex b/lib/elixir/lib/supervisor.ex
index 6829ee90c21..a3cb0318545 100644
--- a/lib/elixir/lib/supervisor.ex
+++ b/lib/elixir/lib/supervisor.ex
@@ -659,6 +659,19 @@ defmodule Supervisor do
   @typedoc since: "1.16.0"
   @type module_spec :: {module(), args :: term()} | module()
 
+  @typedoc """
+  Options for overriding child specification fields.
+  """
+  @type child_spec_overrides :: [
+          id: atom() | term(),
+          start: {module(), atom(), [term()]},
+          restart: restart(),
+          shutdown: shutdown(),
+          type: type(),
+          modules: [module()] | :dynamic,
+          significant: boolean()
+        ]
+
   @doc """
   Starts a supervisor with the given children.
 
@@ -896,7 +909,7 @@ defmodule Supervisor do
       #=>   start: {Agent, :start_link, [fn -> :ok end]}}
 
   """
-  @spec child_spec(child_spec() | module_spec(), keyword()) :: child_spec()
+  @spec child_spec(child_spec() | module_spec(), child_spec_overrides()) :: child_spec()
   def child_spec(module_or_map, overrides)
 
   def child_spec({_, _, _, _, _, _} = tuple, _overrides) do
diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex
index d2881e5b0b5..ab00c5a958f 100644
--- a/lib/elixir/lib/system.ex
+++ b/lib/elixir/lib/system.ex
@@ -113,6 +113,28 @@ defmodule System do
           | :sigusr1
           | :sigusr2
 
+  @type cmd_opts :: [
+          into: Collectable.t(),
+          lines: pos_integer(),
+          cd: Path.t(),
+          env: [{binary(), binary() | nil}],
+          arg0: binary(),
+          stderr_to_stdout: boolean(),
+          use_stdio: boolean(),
+          parallelism: boolean()
+        ]
+
+  @type shell_opts :: [
+          into: Collectable.t(),
+          lines: pos_integer(),
+          cd: Path.t(),
+          env: [{binary(), binary() | nil}],
+          stderr_to_stdout: boolean(),
+          use_stdio: boolean(),
+          parallelism: boolean(),
+          close_stdin: boolean()
+        ]
+
   @vm_signals [:sigquit, :sigterm, :sigusr1]
   @os_signals [:sighup, :sigabrt, :sigalrm, :sigusr2, :sigchld, :sigstop, :sigtstp]
   @signals @vm_signals ++ @os_signals
@@ -938,10 +960,10 @@ defmodule System do
 
     * `:close_stdin` (since v1.14.1) - if the stdin should be closed
       on Unix systems, forcing any command that waits on stdin to
-      immediately terminate. Defaults to false.
+      immediately terminate. Defaults to `false`.
   """
   @doc since: "1.12.0"
-  @spec shell(binary, keyword) :: {Collectable.t(), exit_status :: non_neg_integer}
+  @spec shell(binary, shell_opts) :: {Collectable.t(), exit_status :: non_neg_integer}
   def shell(command, opts \\ []) when is_binary(command) do
     command |> String.trim() |> do_shell(opts)
   end
@@ -1101,7 +1123,7 @@ defmodule System do
   If you desire to execute a trusted command inside a shell, with pipes,
   redirecting and so on, please check `shell/2`.
   """
-  @spec cmd(binary, [binary], keyword) :: {Collectable.t(), exit_status :: non_neg_integer}
+  @spec cmd(binary, [binary], cmd_opts) :: {Collectable.t(), exit_status :: non_neg_integer}
   def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
     assert_no_null_byte!(command, "System.cmd/3")
 
diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex
index ef7af6ebd7a..ec881c4450a 100644
--- a/lib/elixir/lib/task.ex
+++ b/lib/elixir/lib/task.ex
@@ -1444,6 +1444,13 @@ defmodule Task do
     end
   end
 
+  # exported only to avoid dialyzer opaqueness check in internal Task modules
+  @doc false
+  @spec __alias__(pid()) :: Task.ref()
+  def __alias__(pid) do
+    build_alias(pid)
+  end
+
   ## Optimizations
 
   defp build_monitor(pid) do
diff --git a/lib/elixir/lib/task/supervisor.ex b/lib/elixir/lib/task/supervisor.ex
index d8b3f5ac80a..a296e9bfe03 100644
--- a/lib/elixir/lib/task/supervisor.ex
+++ b/lib/elixir/lib/task/supervisor.ex
@@ -87,6 +87,18 @@ defmodule Task.Supervisor do
   @typedoc since: "1.17.0"
   @type async_stream_option :: Task.async_stream_option() | {:shutdown, Supervisor.shutdown()}
 
+  @typedoc """
+  Options for `async/3`, `async/5`, `async_nolink/3`, and `async_nolink/5` functions.
+  """
+  @type async_opts :: [
+          shutdown: :brutal_kill | timeout()
+        ]
+
+  @type start_child_opts :: [
+          restart: :temporary | :transient | :permanent,
+          shutdown: :brutal_kill | timeout()
+        ]
+
   @doc false
   def child_spec(opts) when is_list(opts) do
     id =
@@ -175,7 +187,7 @@ defmodule Task.Supervisor do
       The tasks must trap exits for the timeout to have an effect.
 
   """
-  @spec async(Supervisor.supervisor(), (-> any), Keyword.t()) :: Task.t()
+  @spec async(Supervisor.supervisor(), (-> any), async_opts) :: Task.t()
   def async(supervisor, fun, options \\ []) do
     async(supervisor, :erlang, :apply, [fun, []], options)
   end
@@ -197,7 +209,7 @@ defmodule Task.Supervisor do
       The tasks must trap exits for the timeout to have an effect.
 
   """
-  @spec async(Supervisor.supervisor(), module, atom, [term], Keyword.t()) :: Task.t()
+  @spec async(Supervisor.supervisor(), module, atom, [term], async_opts) :: Task.t()
   def async(supervisor, module, fun, args, options \\ []) do
     async(supervisor, :link, module, fun, args, options)
   end
@@ -284,7 +296,7 @@ defmodule Task.Supervisor do
       end
 
   """
-  @spec async_nolink(Supervisor.supervisor(), (-> any), Keyword.t()) :: Task.t()
+  @spec async_nolink(Supervisor.supervisor(), (-> any), async_opts) :: Task.t()
   def async_nolink(supervisor, fun, options \\ []) do
     async_nolink(supervisor, :erlang, :apply, [fun, []], options)
   end
@@ -303,7 +315,7 @@ defmodule Task.Supervisor do
   as the `:restart` option (the default), as `async_nolink/5` keeps a
   direct reference to the task which is lost if the task is restarted.
   """
-  @spec async_nolink(Supervisor.supervisor(), module, atom, [term], Keyword.t()) :: Task.t()
+  @spec async_nolink(Supervisor.supervisor(), module, atom, [term], async_opts) :: Task.t()
   def async_nolink(supervisor, module, fun, args, options \\ []) do
     async(supervisor, :nolink, module, fun, args, options)
   end
@@ -523,7 +535,7 @@ defmodule Task.Supervisor do
       The task must trap exits for the timeout to have an effect.
 
   """
-  @spec start_child(Supervisor.supervisor(), (-> any), keyword) ::
+  @spec start_child(Supervisor.supervisor(), (-> any), start_child_opts) ::
           DynamicSupervisor.on_start_child()
   def start_child(supervisor, fun, options \\ []) do
     restart = options[:restart]
@@ -538,7 +550,7 @@ defmodule Task.Supervisor do
   Similar to `start_child/3` except the task is specified
   by the given `module`, `fun` and `args`.
   """
-  @spec start_child(Supervisor.supervisor(), module, atom, [term], keyword) ::
+  @spec start_child(Supervisor.supervisor(), module, atom, [term], start_child_opts) ::
           DynamicSupervisor.on_start_child()
   def start_child(supervisor, module, fun, args, options \\ [])
       when is_atom(fun) and is_list(args) do
@@ -602,7 +614,7 @@ defmodule Task.Supervisor do
     case start_child_with_spec(supervisor, [get_owner(owner), :monitor], :temporary, shutdown) do
       {:ok, pid} ->
         if link_type == :link, do: Process.link(pid)
-        alias = :erlang.monitor(:process, pid, alias: :demonitor)
+        alias = Task.__alias__(pid)
         send(pid, {owner, alias, alias, get_callers(owner), {module, fun, args}})
         %Task{pid: pid, ref: alias, owner: owner, mfa: {module, fun, length(args)}}
 
diff --git a/lib/elixir/lib/version.ex b/lib/elixir/lib/version.ex
index 8d98380cee1..36ce18d30dc 100644
--- a/lib/elixir/lib/version.ex
+++ b/lib/elixir/lib/version.ex
@@ -118,6 +118,8 @@ defmodule Version do
   @type build :: String.t() | nil
   @type t :: %__MODULE__{major: major, minor: minor, patch: patch, pre: pre, build: build}
 
+  @type match_opts :: [allow_pre: boolean()]
+
   defmodule Requirement do
     @moduledoc """
     A struct that holds version requirement information.
@@ -296,7 +298,7 @@ defmodule Version do
       ** (Version.InvalidRequirementError) invalid requirement: "== == 1.0.0"
 
   """
-  @spec match?(version, requirement, keyword) :: boolean
+  @spec match?(version, requirement, match_opts) :: boolean
   def match?(version, requirement, opts \\ [])
 
   def match?(version, requirement, opts) when is_binary(requirement) do
@@ -440,7 +442,7 @@ defmodule Version do
 
   If `string` is an invalid requirement, a `Version.InvalidRequirementError` is raised.
 
-  # Examples
+  ## Examples
 
       iex> Version.parse_requirement!("== 2.0.1")
       Version.parse_requirement!("== 2.0.1")
@@ -483,7 +485,7 @@ defmodule Version do
   @doc """
   Converts the given version to a string.
 
-  ### Examples
+  ## Examples
 
       iex> Version.to_string(%Version{major: 1, minor: 2, patch: 3})
       "1.2.3"
diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md
index 921717d946c..d4bfebc0fed 100644
--- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md
+++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md
@@ -144,7 +144,7 @@ end
 
 #### Problem
 
-An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself. However, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1_048_576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation.
+An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself. However, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1 048 576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation.
 
 For these reasons, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behavior caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible.
 
diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md
index 27b204d7d3f..5282a67f5bc 100644
--- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md
+++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md
@@ -292,7 +292,7 @@ For convenience, the markup notation to generate the admonition block above is t
 
 #### Problem
 
-This anti-pattern is the opposite of ["Compile-time dependencies"](#compile-time-dependencies) and it happens when a compile-time dependency is accidentally bypassed, making the Elixir compiler is to track dependencies and recompile files correctly. This happens when building aliases (in other words, module names) dynamically, either within a module or within a macro.
+This anti-pattern is the opposite of ["Compile-time dependencies"](#compile-time-dependencies) and it happens when a compile-time dependency is accidentally bypassed, making the Elixir compiler unable to track dependencies and recompile files correctly. This happens when building aliases (in other words, module names) dynamically, either within a module or within a macro.
 
 #### Example
 
diff --git a/lib/elixir/pages/getting-started/alias-require-and-import.md b/lib/elixir/pages/getting-started/alias-require-and-import.md
index f77b78efae7..31d1564960e 100644
--- a/lib/elixir/pages/getting-started/alias-require-and-import.md
+++ b/lib/elixir/pages/getting-started/alias-require-and-import.md
@@ -156,83 +156,6 @@ end
 
 Since `use` allows any code to run, we can't really know the side-effects of using a module without reading its documentation. Therefore use this function with care and only if strictly required. Don't use `use` where an `import` or `alias` would do.
 
-## Understanding Aliases
-
-At this point, you may be wondering: what exactly is an Elixir alias and how is it represented?
-
-An alias in Elixir is a capitalized identifier (like `String`, `Keyword`, etc) which is converted to an atom during compilation. For instance, the `String` alias translates by default to the atom `:"Elixir.String"`:
-
-```elixir
-iex> is_atom(String)
-true
-iex> to_string(String)
-"Elixir.String"
-iex> :"Elixir.String" == String
-true
-```
-
-By using the `alias/2` directive, we are changing the atom the alias expands to.
-
-Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms:
-
-```elixir
-iex> List.flatten([1, [2], 3])
-[1, 2, 3]
-iex> :"Elixir.List".flatten([1, [2], 3])
-[1, 2, 3]
-```
-
-That's the mechanism we use to call Erlang modules:
-
-```elixir
-iex> :lists.flatten([1, [2], 3])
-[1, 2, 3]
-```
-
-## Module nesting
-
-Now that we have talked about aliases, we can talk about nesting and how it works in Elixir. Consider the following example:
-
-```elixir
-defmodule Foo do
-  defmodule Bar do
-  end
-end
-```
-
-The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
-
-If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above.
-
-**Note**: in Elixir, you don't have to define the `Foo` module before being able to define the `Foo.Bar` module, as they are effectively independent. The above could also be written as:
-
-```elixir
-defmodule Foo.Bar do
-end
-
-defmodule Foo do
-  alias Foo.Bar
-  # Can still access it as `Bar`
-end
-```
-
-Aliasing a nested module does not bring parent modules into scope. Consider the following example:
-
-```elixir
-defmodule Foo do
-  defmodule Bar do
-    defmodule Baz do
-    end
-  end
-end
-
-alias Foo.Bar.Baz
-# The module `Foo.Bar.Baz` is now available as `Baz`
-# However, the module `Foo.Bar` is *not* available as `Bar`
-```
-
-As we will see in later chapters, aliases also play a crucial role in macros, to guarantee they are hygienic.
-
 ## Multi alias/import/require/use
 
 It is possible to `alias`, `import`, `require`, or `use` multiple modules at once. This is particularly useful once we start nesting modules, which is very common when building Elixir applications. For example, imagine you have an application where all modules are nested under `MyApp`, you can alias the modules `MyApp.Foo`, `MyApp.Bar` and `MyApp.Baz` at once as follows:
@@ -241,4 +164,4 @@ It is possible to `alias`, `import`, `require`, or `use` multiple modules at onc
 alias MyApp.{Foo, Bar, Baz}
 ```
 
-With this, we have finished our tour of Elixir modules. The next topic to cover is module attributes.
+With this, we have finished our tour of Elixir modules.
diff --git a/lib/elixir/pages/getting-started/anonymous-functions.md b/lib/elixir/pages/getting-started/anonymous-functions.md
index bec912d000e..253ba3e650e 100644
--- a/lib/elixir/pages/getting-started/anonymous-functions.md
+++ b/lib/elixir/pages/getting-started/anonymous-functions.md
@@ -9,7 +9,7 @@ Anonymous functions allow us to store and pass executable code around as if it w
 
 ## Identifying functions and documentation
 
-Before we move on to discuss anonymous functions, let's talk about how Elixir identifies named functions.
+Before we move on to discuss anonymous functions, let's talk about how Elixir identifies named functions – the functions defined in [modules](modules-and-functions.md).
 
 Functions in Elixir are identified by both their name and their arity. The arity of a function describes the number of arguments that the function takes. From this point on we will use both the function name and its arity to describe functions throughout the documentation. `trunc/1` identifies the function which is named `trunc` and takes `1` argument, whereas `trunc/2` identifies a different (nonexistent) function with the same name but with an arity of `2`.
 
diff --git a/lib/elixir/pages/getting-started/case-cond-and-if.md b/lib/elixir/pages/getting-started/case-cond-and-if.md
index 2da7c568afd..9c354105e5c 100644
--- a/lib/elixir/pages/getting-started/case-cond-and-if.md
+++ b/lib/elixir/pages/getting-started/case-cond-and-if.md
@@ -100,7 +100,11 @@ iex> if nil do
 "This will"
 ```
 
-This is also a good opportunity to talk about variable scoping in Elixir. If any variable is declared or changed inside [`if`](`if/2`), [`case`](`case/2`), and similar constructs, the declaration and change will only be visible inside the construct. For example:
+### Expressions
+
+Some programming languages make a distinction about expressions (code that returns a value) and statements (code that returns no value). In Elixir, there are only expressions, no statements. Everything you write in Elixir language returns some value.
+
+This property allows variables to be scoped to individual blocks of code such as [`if`](`if/2`), [`case`](`case/2`), where declarations or changes are only visible inside the block. A change can't leak to outer blocks, which makes code easier to follow and understand. For example:
 
 ```elixir
 iex> x = 1
@@ -113,19 +117,22 @@ iex> x
 1
 ```
 
-In said cases, if you want to change a value, you must return the value from the [`if`](`if/2`):
+You see the return value of the [`if`](`if/2`) expression as the resulting `2` here. To retain changes made within the [`if`](`if/2`) expression on the outer block you need to assign the returned value to a variable in the outer block.
 
 ```elixir
 iex> x = 1
 1
-iex> x = if true do
-...>   x + 1
-...> else
-...>   x
-...> end
+iex> x =
+...>   if true do
+...>     x + 1
+...>   else
+...>     x
+...>   end
 2
 ```
 
+With all expressions returning a value there's also no need for alternative constructs, such as ternary operators posing as an alternative to [`if`](`if/2`). Elixir does include an inline notation for [`if`](`if/2`) and, as we will [learn later](keywords-and-maps.md#do-blocks-and-keywords), it is a syntactic variation on `if`'s arguments.
+
 > #### `if` is a macro {: .info}
 >
 > An interesting note regarding [`if`](`if/2`) is that it is implemented as a macro in the language: it isn't a special language construct as it would be in many languages. You can check the documentation and its source for more information.
diff --git a/lib/elixir/pages/getting-started/debugging.md b/lib/elixir/pages/getting-started/debugging.md
index b9fe6ea2475..a370f70f1d3 100644
--- a/lib/elixir/pages/getting-started/debugging.md
+++ b/lib/elixir/pages/getting-started/debugging.md
@@ -77,7 +77,7 @@ dbg(Map.put(feature, :in_version, "1.14.0"))
 
 The code above prints this:
 
-```shell
+```text
 [my_file.exs:2: (file)]
 feature #=> %{inspiration: "Rust", name: :dbg}
 [my_file.exs:3: (file)]
@@ -97,7 +97,7 @@ __ENV__.file
 
 This code prints:
 
-```shell
+```text
 [dbg_pipes.exs:5: (file)]
 __ENV__.file #=> "/home/myuser/dbg_pipes.exs"
 |> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"]
diff --git a/lib/elixir/pages/getting-started/erlang-libraries.md b/lib/elixir/pages/getting-started/erlang-libraries.md
index 667b390e11f..e0bdbf4ad1e 100644
--- a/lib/elixir/pages/getting-started/erlang-libraries.md
+++ b/lib/elixir/pages/getting-started/erlang-libraries.md
@@ -85,7 +85,7 @@ iex> :digraph.get_short_path(digraph, v0, v2)
 Note that the functions in `:digraph` alter the graph structure in-place, this
 is possible because they are implemented as ETS tables, explained next.
 
-## Erlang Term Storage
+## Erlang Term Storage (ETS)
 
 The modules [`:ets`](`:ets`) and [`:dets`](`:dets`) handle storage of large data structures in memory or on disk respectively.
 
diff --git a/lib/elixir/pages/getting-started/introduction.md b/lib/elixir/pages/getting-started/introduction.md
index 3c31ea7a250..c642addcdce 100644
--- a/lib/elixir/pages/getting-started/introduction.md
+++ b/lib/elixir/pages/getting-started/introduction.md
@@ -57,4 +57,4 @@ $ elixir simple.exs
 Hello world from Elixir
 ```
 
-Later on we will learn [how to compile Elixir code](modules-and-functions.md) and how to create and work within Elixir projects using the Mix build tool. For now, let's move on to learn the basic data types in the language.
+`iex` and `elixir` are all we need to learn the main language concepts. There is a separate guide named ["Mix and OTP guide"](../mix-and-otp/introduction-to-mix.md) that explores how to actually create, manage, and test full-blown Elixir projects. For now, let's move on to learn the basic data types in the language.
diff --git a/lib/elixir/pages/getting-started/keywords-and-maps.md b/lib/elixir/pages/getting-started/keywords-and-maps.md
index 040a47bedc8..656e84c96ca 100644
--- a/lib/elixir/pages/getting-started/keywords-and-maps.md
+++ b/lib/elixir/pages/getting-started/keywords-and-maps.md
@@ -131,7 +131,7 @@ iex> if true do
 In the example above, the `do` and `else` blocks make up a keyword list. They are nothing more than a syntax convenience on top of keyword lists. We can rewrite the above to:
 
 ```elixir
-iex> if true, do: "This will be seen", else: "This won't"
+iex> if(true, do: "This will be seen", else: "This won't")
 "This will be seen"
 ```
 
@@ -225,6 +225,8 @@ These operations have one large benefit in that they raise if the key does not e
 
 Elixir developers typically prefer to use the `map.key` syntax and pattern matching instead of the functions in the `Map` module when working with maps because they lead to an assertive style of programming. [This blog post by José Valim](https://dashbit.co/blog/writing-assertive-code-with-elixir) provides insight and examples on how you get more concise and faster software by writing assertive code in Elixir.
 
+In a further chapter you'll learn about ["Structs"](structs.md), which further enforce the idea of a map with predefined keys.
+
 ## Nested data structures
 
 Often we will have maps inside maps, or even keywords lists inside maps, and so forth. Elixir provides conveniences for manipulating nested data structures via the `get_in/1`, `put_in/2`, `update_in/2`, and other macros giving the same conveniences you would find in imperative languages while keeping the immutable properties of the language.
diff --git a/lib/elixir/pages/getting-started/module-attributes.md b/lib/elixir/pages/getting-started/module-attributes.md
index 5c556aced43..e1d824b8231 100644
--- a/lib/elixir/pages/getting-started/module-attributes.md
+++ b/lib/elixir/pages/getting-started/module-attributes.md
@@ -32,7 +32,7 @@ In the example above, we are defining the module documentation by using the modu
 
 `@moduledoc` and `@doc` are by far the most used attributes, and we expect you to use them a lot. Elixir treats documentation as first-class and provides many functions to access documentation. We will cover them [in their own chapter](writing-documentation.md).
 
-Let's go back to the `Math` module defined in the previous chapters, add some documentation and save it to the `math.ex` file:
+Documentation is only accessible from compiled modules. So in order to give it a try, let's once again define the `Math` module, but this time within a file named `math.ex`:
 
 ```elixir
 defmodule Math do
@@ -53,23 +53,29 @@ defmodule Math do
 end
 ```
 
-Elixir promotes the use of Markdown with heredocs to write readable documentation. Heredocs are multi-line strings, they start and end with triple double-quotes, keeping the formatting of the inner text. We can access the documentation of any compiled module directly from IEx:
+Elixir promotes the use of Markdown with heredocs to write readable documentation. Heredocs are multi-line strings, they start and end with triple double-quotes, keeping the formatting of the inner text.
 
-```console
-$ elixirc math.ex
-$ iex
+Now let's compile it. Start `iex` and then invoke [the `c/2` helper](`IEx.Helpers.c/2`):
+
+```elixir
+iex> c("math.ex", ".")
+[Math]
 ```
 
+And now we can access them:
+
 ```elixir
-iex> h Math # Access the docs for the module Math
+iex> h Math # Docs for module Math
 ...
-iex> h Math.sum # Access the docs for the sum function
+iex> h Math.sum # Docs for the sum function
 ...
 ```
 
-We also provide a tool called [ExDoc](https://github.com/elixir-lang/ex_doc) which is used to generate HTML pages from the documentation.
+When we compiled the module, you may have noticed Elixir created a `Elixir.Math.beam` file. That's the bytecode for the module and that's where the documentation is stored.
+
+In our day to day, Elixir developers use the `Mix` build tool to compile code and projects like [ExDoc](https://github.com/elixir-lang/ex_doc) to generate HTML and EPUB pages from the documentation.
 
-You can take a look at the docs for `Module` for a complete list of supported attributes. Elixir also uses attributes to annotate our code with [typespecs](../references/typespecs.md).
+Take a look at the docs for `Module` for a complete list of supported attributes.
 
 ## As temporary storage
 
diff --git a/lib/elixir/pages/getting-started/modules-and-functions.md b/lib/elixir/pages/getting-started/modules-and-functions.md
index 25263746508..888b48c90f8 100644
--- a/lib/elixir/pages/getting-started/modules-and-functions.md
+++ b/lib/elixir/pages/getting-started/modules-and-functions.md
@@ -12,7 +12,7 @@ iex> String.length("hello")
 5
 ```
 
-In order to create our own modules in Elixir, we use the [`defmodule`](`defmodule/2`) macro. The first letter of the module must be in uppercase. We use the [`def`](`def/2`) macro to define functions in that module. The first letter of every function must be in lowercase (or underscore):
+In order to create our own modules in Elixir, we use the [`defmodule`](`defmodule/2`) macro. The first letter of an module name (an alias, as described further down) must be in uppercase. We use the [`def`](`def/2`) macro to define functions in that module. The first letter of every function must be in lowercase (or underscore):
 
 ```elixir
 iex> defmodule Math do
@@ -25,38 +25,13 @@ iex> Math.sum(1, 2)
 3
 ```
 
-In this chapter we will define our own modules, with different levels of complexity. As our examples get longer in size, it can be tricky to type them all in the shell. It's about time for us to learn how to compile Elixir code and also how to run Elixir scripts.
+In this chapter we will define our own modules, with different levels of complexity. As our examples get longer in size, it can be tricky to type them all in the shell, so we will resort more frequently to scripting.
 
-## Compilation
+## Scripting
 
-Most of the time it is convenient to write modules into files so they can be compiled and reused. Let's assume we have a file named `math.ex` with the following contents:
+Elixir has two file extensions `.ex` (Elixir) and `.exs` (Elixir scripts). Elixir treats both files exactly the same way, the only difference is in intention. `.ex` files are meant to be compiled while `.exs` files are used for scripting.
 
-```elixir
-defmodule Math do
-  def sum(a, b) do
-    a + b
-  end
-end
-```
-
-This file can be compiled using `elixirc`:
-
-```console
-$ elixirc math.ex
-```
-
-This will generate a file named `Elixir.Math.beam` containing the bytecode for the defined module. If we start `iex` again, our module definition will be available (provided that `iex` is started in the same directory the bytecode file is in):
-
-```elixir
-iex> Math.sum(1, 2)
-3
-```
-
-## Scripting mode
-
-In addition to the Elixir file extension `.ex`, Elixir also supports `.exs` files for scripting. Elixir treats both files exactly the same way, the only difference is in intention. `.ex` files are meant to be compiled while `.exs` files are used for scripting. This convention is followed by projects like `mix`.
-
-For instance, we can create a file called `math.exs`:
+Let's create a file named `math.exs`:
 
 ```elixir
 defmodule Math do
@@ -74,15 +49,13 @@ And execute it as:
 $ elixir math.exs
 ```
 
-Because we used `elixir` instead of `elixirc`, the module was compiled and loaded into memory, but no `.beam` file was written to disk.
-
-Elixir projects are usually organized into three directories:
+You can also load the file within `iex` by running:
 
-  * `_build` - contains compilation artifacts
-  * `lib` - contains Elixir code (usually `.ex` files)
-  * `test` - contains tests (usually `.exs` files)
+```console
+$ iex math.exs
+```
 
-When working on actual projects, the build tool called `mix` will be responsible for compiling and setting up the proper paths for you. For learning and convenience purposes, we recommend you to write the following code into script files and execute them as shown above.
+And then have direct access to the `Math` module.
 
 ## Function definition
 
@@ -194,4 +167,78 @@ IO.puts(Concat.join("Hello", "world", "_")) #=> Hello_world
 
 When a variable is not used by a function or a clause, we add a leading underscore (`_`) to its name to signal this intent. This rule is also covered in our [Naming Conventions](../references/naming-conventions.md#underscore-_foo) document.
 
-This finishes our short introduction to modules. In the next chapters, we will learn how to use function definitions for recursion and later on explore more functionality related to modules.
+## Understanding Aliases
+
+An alias in Elixir is a capitalized identifier (like `String`, `Keyword`, etc) which is converted to an atom during compilation. For instance, the `String` alias translates by default to the atom `:"Elixir.String"`:
+
+```elixir
+iex> is_atom(String)
+true
+iex> to_string(String)
+"Elixir.String"
+iex> :"Elixir.String" == String
+true
+```
+
+By using the `alias/2` directive, we are changing the atom the alias expands to.
+
+Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms. By namespacing
+those atoms elixir modules avoid conflicting with existing erlang modules.
+
+```elixir
+iex> List.flatten([1, [2], 3])
+[1, 2, 3]
+iex> :"Elixir.List".flatten([1, [2], 3])
+[1, 2, 3]
+```
+
+That's the mechanism we use to call Erlang modules:
+
+```elixir
+iex> :lists.flatten([1, [2], 3])
+[1, 2, 3]
+```
+
+## Module nesting
+
+Now that we have talked about aliases, we can talk about nesting and how it works in Elixir. Consider the following example:
+
+```elixir
+defmodule Foo do
+  defmodule Bar do
+  end
+end
+```
+
+The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
+
+If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above.
+
+**Note**: in Elixir, you don't have to define the `Foo` module before being able to define the `Foo.Bar` module, as they are effectively independent. The above could also be written as:
+
+```elixir
+defmodule Foo.Bar do
+end
+
+defmodule Foo do
+  alias Foo.Bar
+  # Can still access it as `Bar`
+end
+```
+
+Aliasing a nested module does not bring parent modules into scope. Consider the following example:
+
+```elixir
+defmodule Foo do
+  defmodule Bar do
+    defmodule Baz do
+    end
+  end
+end
+
+alias Foo.Bar.Baz
+# The module `Foo.Bar.Baz` is now available as `Baz`
+# However, the module `Foo.Bar` is *not* available as `Bar`
+```
+
+As we will see in later chapters, aliases also play a crucial role in macros, to guarantee they are hygienic.
diff --git a/lib/elixir/pages/getting-started/pattern-matching.md b/lib/elixir/pages/getting-started/pattern-matching.md
index 8b114791d53..78dde7c0ef2 100644
--- a/lib/elixir/pages/getting-started/pattern-matching.md
+++ b/lib/elixir/pages/getting-started/pattern-matching.md
@@ -113,6 +113,38 @@ iex> [0 | list]
 [0, 1, 2, 3]
 ```
 
+In some cases, you don't care about a particular value in a pattern. It is a common practice to bind those values to the underscore, `_`. For example, if only the head of the list matters to us, we can assign the tail to underscore:
+
+```elixir
+iex> [head | _] = [1, 2, 3]
+[1, 2, 3]
+iex> head
+1
+```
+
+The variable `_` is special in that it can never be read from. Trying to read from it gives a compile error:
+
+```elixir
+iex> _
+** (CompileError) iex:1: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions
+```
+
+If a variable is mentioned more than once in a pattern, all references must bind to the same value:
+
+```elixir
+iex> {x, x} = {1, 1}
+{1, 1}
+iex> {x, x} = {1, 2}
+** (MatchError) no match of right hand side value: {1, 2}
+```
+
+Although pattern matching allows us to build powerful constructs, its usage is limited. For instance, you cannot make function calls on the left side of a match. The following example is invalid:
+
+```elixir
+iex> length([1, [2], 3]) = 3
+** (CompileError) iex:1: cannot invoke remote function :erlang.length/1 inside match
+```
+
 Pattern matching allows developers to easily destructure data types such as tuples and lists. As we will see in the following chapters, it is one of the foundations of recursion in Elixir and applies to other types as well, like maps and binaries.
 
 ## The pin operator
@@ -168,36 +200,4 @@ iex> {y, 1} = {2, 2}
 ** (MatchError) no match of right hand side value: {2, 2}
 ```
 
-If a variable is mentioned more than once in a pattern, all references must bind to the same value:
-
-```elixir
-iex> {x, x} = {1, 1}
-{1, 1}
-iex> {x, x} = {1, 2}
-** (MatchError) no match of right hand side value: {1, 2}
-```
-
-In some cases, you don't care about a particular value in a pattern. It is a common practice to bind those values to the underscore, `_`. For example, if only the head of the list matters to us, we can assign the tail to underscore:
-
-```elixir
-iex> [head | _] = [1, 2, 3]
-[1, 2, 3]
-iex> head
-1
-```
-
-The variable `_` is special in that it can never be read from. Trying to read from it gives a compile error:
-
-```elixir
-iex> _
-** (CompileError) iex:1: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions
-```
-
-Although pattern matching allows us to build powerful constructs, its usage is limited. For instance, you cannot make function calls on the left side of a match. The following example is invalid:
-
-```elixir
-iex> length([1, [2], 3]) = 3
-** (CompileError) iex:1: cannot invoke remote function :erlang.length/1 inside match
-```
-
 This finishes our introduction to pattern matching. As we will see in the next chapter, pattern matching is very common in many language constructs and they can be further augmented with guards.
diff --git a/lib/elixir/pages/getting-started/structs.md b/lib/elixir/pages/getting-started/structs.md
index 73fb42e9fe1..ff817819b0d 100644
--- a/lib/elixir/pages/getting-started/structs.md
+++ b/lib/elixir/pages/getting-started/structs.md
@@ -76,6 +76,28 @@ iex> %User{} = %{}
 
 For more details on creating, updating, and pattern matching structs, see the documentation for `%/2`.
 
+## Dynamic struct updates
+
+When you need to update structs with data from keyword lists or maps, use `Kernel.struct!/2`:
+
+```elixir
+iex> john = %User{name: "John", age: 27}
+%User{age: 27, name: "John"}
+iex> updates = [name: "Jane", age: 30]
+[name: "Jane", age: 30]
+iex> struct!(john, updates)
+%User{age: 27, name: "Jane"}
+```
+
+`struct!/2` will raise an error if you try to set invalid fields:
+
+```elixir
+iex> struct!(john, invalid: "field")
+** (KeyError) key :invalid not found in: %User{age: 27, name: "John"}
+```
+
+Use the map update syntax (`%{john | name: "Jane"}`) when you know the exact fields at compile time. Always use `struct!/2` instead of `Map` functions to preserve struct integrity.
+
 ## Structs are bare maps underneath
 
 Structs are simply maps with a "special" field named `__struct__` that holds the name of the struct:
@@ -113,7 +135,7 @@ iex> %Product{}
 %Product{name: nil}
 ```
 
-You can define a structure combining both fields with explicit default values, and implicit `nil` values. In this case you must first specify the fields which implicitly default to nil:
+You can define a structure combining both fields with explicit default values, and implicit `nil` values. In this case you must first specify the fields which implicitly default to `nil`:
 
 ```elixir
 iex> defmodule User do
diff --git a/lib/elixir/pages/mix-and-otp/agents.md b/lib/elixir/pages/mix-and-otp/agents.md
index 4df693b71ae..86e4252c8c8 100644
--- a/lib/elixir/pages/mix-and-otp/agents.md
+++ b/lib/elixir/pages/mix-and-otp/agents.md
@@ -3,7 +3,7 @@
   SPDX-FileCopyrightText: 2021 The Elixir Team
 -->
 
-# Simple state management with agents
+# Simple state with agents
 
 In this chapter, we will learn how to keep and share state between multiple entities. If you have previous programming experience, you may think of globally shared variables, but the model we will learn here is quite different. The next chapters will generalize the concepts introduced here.
 
@@ -11,19 +11,14 @@ If you have skipped the *Getting Started* guide or read it long ago, be sure to
 
 ## The trouble with (mutable) state
 
-Elixir is an immutable language where nothing is shared by default. If we want to share information, which can be read and modified from multiple places, we have two main options in Elixir:
+Elixir is an immutable language where nothing is shared by default. If we want to share information, this is typically done by sending messages between processes.
 
-  * Using processes and message passing
-  * [ETS (Erlang Term Storage)](`:ets`)
-
-We covered processes in the *Getting Started* guide. ETS (Erlang Term Storage) is a new topic that we will explore in later chapters. When it comes to processes though, we rarely hand-roll our own, instead we use the abstractions available in Elixir and OTP:
+When it comes to processes though, we rarely hand-roll our own, instead we use the abstractions available in Elixir and OTP:
 
   * `Agent` — Simple wrappers around state.
   * `GenServer` — "Generic servers" (processes) that encapsulate state, provide sync and async calls, support code reloading, and more.
   * `Task` — Asynchronous units of computation that allow spawning a process and potentially retrieving its result at a later time.
 
-We will explore these abstractions as we move forward. Keep in mind that they are all implemented on top of processes using the basic features provided by the VM, like `send/2`, `receive/1`, `spawn/1` and `Process.link/1`.
-
 Here, we will use agents, and create a module named `KV.Bucket`, responsible for storing our key-value entries in a way that allows them to be read and modified by other processes.
 
 ## Agents 101
@@ -47,7 +42,7 @@ iex> Agent.stop(agent)
 :ok
 ```
 
-We started an agent with an initial state of an empty list. We updated the agent's state, adding our new item to the head of the list. The second argument of `Agent.update/3` is a function that takes the agent's current state as input and returns its desired new state. Finally, we retrieved the whole list. The second argument of `Agent.get/3` is a function that takes the state as input and returns the value that `Agent.get/3` itself will return. Once we are done with the agent, we can call `Agent.stop/3` to terminate the agent process.
+We started an agent with an initial state of an empty list. The `start_link/1` function returned the `:ok` tuple with a process identifier (PID) of the agent. We will use this PID for all further interactions. We then updated the agent's state, adding our new item to the head of the list. The second argument of `Agent.update/3` is a function that takes the agent's current state as input and returns its desired new state. Finally, we retrieved the whole list. The second argument of `Agent.get/3` is a function that takes the state as input and returns the value that `Agent.get/3` itself will return. Once we are done with the agent, we can call `Agent.stop/3` to terminate the agent process.
 
 The `Agent.update/3` function accepts as a second argument any function that receives one argument and returns a value:
 
@@ -93,7 +88,9 @@ Also note the `async: true` option passed to `ExUnit.Case`. This option makes th
 Async or not, our new test should obviously fail, as none of the functionality is implemented in the module being tested:
 
 ```text
-** (UndefinedFunctionError) function KV.Bucket.start_link/1 is undefined (module KV.Bucket is not available)
+1) test stores values by key (KV.BucketTest)
+   test/kv/bucket_test.exs:4
+   ** (UndefinedFunctionError) function KV.Bucket.start_link/1 is undefined (module KV.Bucket is not available)
 ```
 
 In order to fix the failing test, let's create a file at `lib/kv/bucket.ex` with the contents below. Feel free to give a try at implementing the `KV.Bucket` module yourself using agents before peeking at the implementation below.
@@ -104,9 +101,11 @@ defmodule KV.Bucket do
 
   @doc """
   Starts a new bucket.
+
+  All options are forwarded to `Agent.start_link/2`.
   """
-  def start_link(_opts) do
-    Agent.start_link(fn -> %{} end)
+  def start_link(opts) do
+    Agent.start_link(fn -> %{} end, opts)
   end
 
   @doc """
@@ -125,49 +124,43 @@ defmodule KV.Bucket do
 end
 ```
 
-The first step in our implementation is to call `use Agent`. Most of the functionality we will learn, such as `GenServer` and `Supervisor`, follow this pattern. For all of them, calling `use` generates a `child_spec/1` function with default configuration, which will be handy when we start supervising processes in chapter 4.
+The first step in our implementation is to call `use Agent`. This is a pattern we will see throughout the guides and understand in depth in the next chapter.
 
-Then we define a `start_link/1` function, which will effectively start the agent. It is a convention to define a `start_link/1` function that always accepts a list of options. We don't plan on using any options right now, but we might later on. We then proceed to call `Agent.start_link/1`, which receives an anonymous function that returns the Agent's initial state.
+Then we define a `start_link/1` function, which will effectively start the agent. It is a convention to define a `start_link/1` function that always accepts a list of options. We then call `Agent.start_link/2` passing an anonymous function that returns the Agent's initial state and the same list of options we received.
 
 We are keeping a map inside the agent to store our keys and values. Getting and putting values on the map is done with the Agent API and the capture operator `&`, introduced in [the Getting Started guide](../getting-started/anonymous-functions.md#the-capture-operator). The agent passes its state to the anonymous function via the `&1` argument when `Agent.get/2` and `Agent.update/2` are called.
 
 Now that the `KV.Bucket` module has been defined, our test should pass! You can try it yourself by running: `mix test`.
 
-## Test setup with ExUnit callbacks
+## Naming processes
 
-Before moving on and adding more features to `KV.Bucket`, let's talk about ExUnit callbacks. As you may expect, all `KV.Bucket` tests will require a bucket agent to be up and running. Luckily, ExUnit supports callbacks that allow us to skip such repetitive tasks.
+When starting `KV.Bucket`, we pass a list of options which we forward to `Agent.start_link/2`. One of the options accepted by `Agent.start_link/2` is a name option which allows us to name a process, so we can interact with it using its name instead of its PID.
 
-Let's rewrite the test case to use callbacks:
+Let's write a test as an example. Back on `KV.BucketTest`, add this:
 
 ```elixir
-defmodule KV.BucketTest do
-  use ExUnit.Case, async: true
+  test "stores values by key on a named process" do
+    {:ok, _} = KV.Bucket.start_link(name: :shopping_list)
+    assert KV.Bucket.get(:shopping_list, "milk") == nil
 
-  setup do
-    {:ok, bucket} = KV.Bucket.start_link([])
-    %{bucket: bucket}
+    KV.Bucket.put(:shopping_list, "milk", 3)
+    assert KV.Bucket.get(:shopping_list, "milk") == 3
   end
-
-  test "stores values by key", %{bucket: bucket} do
-    assert KV.Bucket.get(bucket, "milk") == nil
-
-    KV.Bucket.put(bucket, "milk", 3)
-    assert KV.Bucket.get(bucket, "milk") == 3
-  end
-end
 ```
 
-We have first defined a setup callback with the help of the `setup/1` macro. The `setup/1` macro defines a callback that is run before every test, in the same process as the test itself.
-
-Note that we need a mechanism to pass the `bucket` PID from the callback to the test. We do so by using the *test context*. When we return `%{bucket: bucket}` from the callback, ExUnit will merge this map into the test context. Since the test context is a map itself, we can pattern match the bucket out of it, providing access to the bucket inside the test:
+However, keep in mind that names are shared in the current node. If two tests attempt to create two processes named `:shopping_list` at the same time, one would succeed and the other would fail. For this reason, it is a common practice in Elixir to name processes started during tests after the test itself, like this:
 
 ```elixir
-test "stores values by key", %{bucket: bucket} do
-  # `bucket` is now the bucket from the setup block
-end
+  test "stores values by key on a named process", config do
+    {:ok, _} = KV.Bucket.start_link(name: config.test)
+    assert KV.Bucket.get(config.test, "milk") == nil
+
+    KV.Bucket.put(config.test, "milk", 3)
+    assert KV.Bucket.get(config.test, "milk") == 3
+  end
 ```
 
-You can read more about ExUnit cases in the [`ExUnit.Case` module documentation](`ExUnit.Case`) and more about callbacks in `ExUnit.Callbacks`.
+The `config` argument, passed after the test name, is the *test context* and it includes configuration and metadata about the current test, which is useful in scenarios like these.
 
 ## Other agent actions
 
@@ -214,4 +207,4 @@ end
 
 When a long action is performed on the server, all other requests to that particular server will wait until the action is done, which may cause some clients to timeout.
 
-In the next chapter, we will explore GenServers, where the segregation between clients and servers is made more apparent.
+Some APIs, such as GenServers, make a clearer distiction between client and server, and we will explore them in future chapters. Next let's talk about naming things, applications, and supervisors.
diff --git a/lib/elixir/pages/mix-and-otp/config-and-distribution.md b/lib/elixir/pages/mix-and-otp/config-and-distribution.md
new file mode 100644
index 00000000000..6ed27bfb771
--- /dev/null
+++ b/lib/elixir/pages/mix-and-otp/config-and-distribution.md
@@ -0,0 +1,305 @@
+<!--
+  SPDX-License-Identifier: Apache-2.0
+  SPDX-FileCopyrightText: 2021 The Elixir Team
+-->
+
+# Configuration and distribution
+
+So far we have hardcoded our applications to run a web server on port 4040. This has been somewhat problematic since we can't, for example, run our development server and tests at the same time. In this chapter, we will learn how to use the application environment for configuration, paving the way for us to enable distribution by running multiple development servers on the same machine (on different ports).
+
+In this last guide, we will make the routing table for our distributed key-value store configurable, and then finally package the software for production.
+
+Let's do this.
+
+## Application environment
+
+In the chapter [Registries, applications, and supervisors](supervisor-and-application.md), we have learned that our project is backed by an application, which bundles our modules and specifies how your supervision tree starts and shuts down. Each application can also have its own configuration, which in Erlang/OTP (and therefore Elixir) is called "application environment".
+
+We can use the application environment to configure our own application, as well as others. Let's see the application environment in practice. Create a file `config/runtime.exs` with the following:
+
+```elixir
+import Config
+
+port =
+  cond do
+    port_env = System.get_env("PORT") ->
+      String.to_integer(port_env)
+
+    config_env() == :test ->
+      4040
+
+    true ->
+      4050
+  end
+
+config :kv, :port, port
+```
+
+The above is attempting to read the "PORT" environment variable and use it as the port if defined. Otherwise, we default to port `4040` for tests and port `4050` for other environments, eliminating the conflict between environments we have seen in the past. Then we store its value under the `:port` key of our `:kv` application.
+
+Now we just need to read this configuration. Open up `lib/kv.ex` and the `start/2` function to the following:
+
+```elixir
+  def start(_type, _args) do
+    port = Application.fetch_env!(:kv, :port)
+
+    children = [
+      {Registry, name: KV, keys: :unique},
+      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
+      {Task.Supervisor, name: KV.ServerSupervisor},
+      Supervisor.child_spec({Task, fn -> KV.Server.accept(port) end}, restart: :permanent)
+    ]
+
+    Supervisor.start_link(children, strategy: :one_for_one)
+  end
+```
+
+Run `iex -S mix` and you will see the following message printed:
+
+```text
+[info] Accepting connections on port 4050
+```
+
+Run tests, without killing the development server, and you will see it running on port 4040.
+
+Our change was straight-forward. We used `Application.fetch_env!/2` to read the entry for `port` in `:kv`'s environment. We explicitly used `fetch_env!/2` (instead of `get_env/2` or `fetch_env`) because it will raise if the port was not configured (preventing the app from booting).
+
+## Compile vs runtime configuration
+
+Configuration files provide a mechanism for us to configure the environment of any application. Elixir provides two configuration entry points:
+
+  * `config/config.exs` — this file is read at build time, before we compile our application and before we even load our dependencies. This means we can't access the code in our application nor in our dependencies. However, it means we can control how they are compiled
+
+  * `config/runtime.exs` — this file is read after our application and dependencies are compiled and therefore it can configure how our application works at runtime. If you want to read system environment variables (via `System.get_env/1`) or access external configuration, this is the appropriate place to do so
+
+You can learn more about configuration in the `Config` and `Config.Provider` modules.
+
+Generally speaking, we use `Application.fetch_env!/2` (and friends) to read runtime configuration. `Application.compile_env/2` is available for reading compile-time configuration. This allows Elixir to track which modules to recompile when the compilation environment changes.
+
+Now that we can start multiple servers, let's explore distribution.
+
+## Our first distributed code
+
+Elixir ships with facilities to connect nodes and exchange information between them. In fact, we use the same concepts of processes, message passing and receiving messages when working in a distributed environment because Elixir processes are *location transparent*. This means that when sending a message, it doesn't matter if the recipient process is on the same node or on another node, the VM will be able to deliver the message in both cases.
+
+In order to run distributed code, we need to start the VM with a name. The name can be short (when in the same network) or long (requires the full computer address). Let's start a new IEx session:
+
+```console
+$ iex --sname foo
+```
+
+You can see now the prompt is slightly different and shows the node name followed by the computer name:
+
+    Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help)
+    iex(foo@jv)1>
+
+My computer is named `jv`, so I see `foo@jv` in the example above, but you will get a different result. We will use `foo@computer-name` in the following examples and you should update them accordingly when trying out the code.
+
+Let's define a module named `Hello` in this shell:
+
+```elixir
+iex> defmodule Hello do
+...>   def world, do: IO.puts("hello world")
+...> end
+```
+
+If you have another computer on the same network with both Erlang and Elixir installed, you can start another shell on it. If you don't, you can start another IEx session in another terminal. In either case, give it the short name of `bar`:
+
+```console
+$ iex --sname bar
+```
+
+Note that inside this new IEx session, we cannot access `Hello.world/0`:
+
+```elixir
+iex> Hello.world
+** (UndefinedFunctionError) function Hello.world/0 is undefined (module Hello is not available)
+    Hello.world()
+```
+
+However, we can spawn a new process on `foo@computer-name` from `bar@computer-name`! Let's give it a try (where `@computer-name` is the one you see locally):
+
+```elixir
+iex> Node.spawn_link(:"foo@computer-name", fn -> Hello.world() end)
+#PID<9014.59.0>
+hello world
+```
+
+Elixir spawned a process on another node and returned its PID. You can see the PID number no longer starts with zero, showing it belongs to another node. The code then executed on the other node where the `Hello.world/0` function exists and invoked that function. Note that the result of "hello world" was printed on the current node `bar` and not on `foo`. In other words, the message to be printed was sent back from `foo` to `bar`. This happens because the process spawned on the other node (`foo`) knows all the output should be sent back to the original node!
+
+We can send and receive messages from the PID returned by `Node.spawn_link/2` as usual. Let's try a quick ping-pong example:
+
+```elixir
+iex> pid = Node.spawn_link(:"foo@computer-name", fn ->
+...>   receive do
+...>     {:ping, client} -> send(client, :pong)
+...>   end
+...> end)
+#PID<9014.59.0>
+iex> send(pid, {:ping, self()})
+{:ping, #PID<0.73.0>}
+iex> flush()
+:pong
+:ok
+```
+
+In other words, we can spawn processes in other nodes, hold onto their PIDs, and then send messages to them as if they were running on the same machine. That's the *location transparency* principle. And because everything we have built so far was built on top of messaging passing, we should be able to adjust our key-value store to become a distributed one with little work.
+
+## Distributed naming registry with `:global`
+
+First, let's check that our code is not currently distributed. Start a new node like this:
+
+```console
+$ PORT=4100 iex --sname foo -S mix
+```
+
+And the other like this:
+
+```console
+$ PORT=4101 iex --sname bar -S mix
+```
+
+Now, within `foo@computer-name`, do this:
+
+```elixir
+iex> :erpc.call(:"bar@computer-name", KV, :create_bucket, ["shopping"])
+{:ok, #PID<22121.164.0>}
+```
+
+Instead of using `Node.spawn_link/2`, we used [Erlang's builtin RPC module](`:erpc`) to call the function `create_bucket` in the `KV` module passing a one element list with the string "shopping" as the argument list. We could have used `Node.spawn_link/2`, but `:erpc.call/4` conveniently returns the result of the invocation.
+
+Still in `foo@computer-name`, let's try to access the bucket:
+
+```elixir
+iex> KV.lookup_bucket("shopping")
+nil
+```
+
+It returns `nil`. However, if you run `KV.lookup_bucket("shopping")` in `bar@computer-name`, it will return the proper bucket. In other words, the nodes can communicate with each other, but buckets spawned in one node are not visible to the other.
+
+This is because we are using [Elixir's Registry](`Registry`) to name our buckets, which is a **local** process registry. In other words, it is designed for processes running on a single node and not for distribution.
+
+Luckily, Erlang ships with a distributed registry called [`:global`](`:global`), which is directly supported by the `:name` option by passing a `{:global, name}` tuple. All we need to do is update the `via/1` function in `lib/kv.ex` from this:
+
+```elixir
+  defp via(name), do: {:via, Registry, {KV, name}}
+```
+
+to this:
+
+```elixir
+  defp via(name), do: {:global, name}
+```
+
+Do the change above and restart both `foo@computer-name` and `bar@computer-name`. Now, back on `foo@computer-name`, let's give it another try:
+
+```elixir
+iex> :erpc.call(:"bar@computer-name", KV, :create_bucket, ["shopping"])
+{:ok, #PID<21821.179.0>}
+iex> KV.lookup_bucket("shopping")
+#PID<21821.179.0>
+```
+
+And there you go! By simply changing which naming registry we used, we now have a distributed key value store. You can even try using `telnet` to connect to the servers on different ports and validate that changes in one session are visible in the other one. Exciting!
+
+## Node discovery and dependencies
+
+There is one essential ingredient to wrap up our distributed key-value store. In order for the `:global` registry to work, we need to make sure the nodes are connected to each other. When we run `:erpc` call passing the node name:
+
+```elixir
+:erpc.call(:"bar@computer-name", KV, :create_bucket, ["shopping"])
+```
+
+Elixir automatically connected the nodes together. This is easy to do in an IEx session when both instances are running on the same machine but it requires more work in a production environment, where instances are on different machines which may be started at any time and running on different IP addresses.
+
+Luckily for us, this is also a well-solved problem. For example, if you are using [the Phoenix web framework](https://phoenixframework.org) in production, it ships with [the `dns_cluster` package](https://github.com/phoenixframework/dns_cluster), which automatically runs DNS queries to find new nodes and connect them. If you are using Kubernetes or cloud providers, [packages like `libcluster`](https://github.com/bitwalker/libcluster) ship with different strategies to discover and connect nodes.
+
+Installing dependencies in Elixir is simple. Most commonly, we use the [Hex Package Manager](https://hex.pm), by listing the dependency inside the deps function in our `mix.exs` file:
+
+```elixir
+def deps do
+  [{:dns_cluster, "~> 0.2"}]
+end
+```
+
+This dependency refers to the latest version of `dns_cluster` in the 0.x version series that has been pushed to Hex. This is indicated by the `~>` preceding the version number. For more information on specifying version requirements, see the documentation for the `Version` module.
+
+Typically, stable releases are pushed to Hex. If you want to depend on an external dependency still in development, Mix is able to manage Git dependencies too:
+
+```elixir
+def deps do
+  [{:dns_cluster, git: "https://github.com/phoenixframework/dns_cluster.git"}]
+end
+```
+
+You will notice that when you add a dependency to your project, Mix generates a `mix.lock` file that guarantees *repeatable builds*. The lock file must be checked in to your version control system, to guarantee that everyone who uses the project will use the same dependency versions as you.
+
+Mix provides many tasks for working with dependencies, which can be seen in `mix help`:
+
+```console
+$ mix help
+mix deps              # Lists dependencies and their status
+mix deps.clean        # Deletes the given dependencies' files
+mix deps.compile      # Compiles dependencies
+mix deps.get          # Gets all out of date dependencies
+mix deps.tree         # Prints the dependency tree
+mix deps.unlock       # Unlocks the given dependencies
+mix deps.update       # Updates the given dependencies
+```
+
+The most common tasks are `mix deps.get` and `mix deps.update`. Once fetched, dependencies are automatically compiled for you. You can read more about deps by running `mix help deps`.
+
+To wrap up this chapter, we will build a very simple node discovery mechanism, where the name of the nodes we should connect to are given on boot, using the lessons we learned in this chapter.
+
+## `Node.connect/1`
+
+We will change our application to support a "NODES" environment variable with the name of all nodes each instance should connect to.
+
+Open up `config/runtime.exs` and add this to the bottom:
+
+```elixir
+nodes =
+  System.get_env("NODES", "")
+  |> String.split(",", trim: true)
+  |> Enum.map(&String.to_atom/1)
+
+config :kv, :nodes, nodes
+```
+
+We fetch the environment variable, split it on "," while discarding all empty strings, and then convert each entry to an atom, as node names are atoms.
+
+Now, in your `start/2` callback, we will add this to of the `start/2` function:
+
+```elixir
+  def start(_type, _args) do
+    for node <- Application.fetch_env!(:kv, :nodes) do
+      Node.connect(node)
+    end
+```
+
+Now we can start our nodes as:
+
+```console
+$ NODES="foo@computer-name,bar@computer-name" PORT=4040 iex --sname foo -S mix
+$ NODES="foo@computer-name,bar@computer-name" PORT=4041 iex --sname bar -S mix
+```
+
+And they should connect to each other. Give it a try!
+
+In an actual production system, there is some additional care we must take. For example, we often use `--name` instead of `--sname` and give fully qualified node names.
+
+Furthermore, when connecting two instances, we must guarantee they have the same cookie, which is a secret Erlang uses to authorize the connection. When they run on the same machine, they share the same cookie by default, but it must be either explicitly set or shared in other ways when deploying in a cluster.
+
+We will revisit these topics in the last chapter when we talk about releases.
+
+## Distributed system trade-offs
+
+In this chapter, we made our key-value store distributed by using the `:global` naming registry. However, it is important to keep in mind that every distributed system, be it a library or a full-blown database, is designed with a series of trade-offs in mind.
+
+In particular, `:global` requires consistency across all known nodes whenever a new bucket is created. For example, if your cluster has three nodes, creating a new bucket will require all three nodes to agree on its name. This means if one node is unresponsive, perhaps due to a [network partition](https://en.wikipedia.org/wiki/Network_partition), the node will have to either reconnect or be kicked out before registration succeeds. This also means that, as your cluster grows in size, registration becomes more expensive, although lookups are always cheap and immediate. Within the ecosystem, there are other named registries, which explore different trade-offs, such as [Syn](https://github.com/ostinelli/syn).
+
+Further complications arise when we consider storage. Today, when our nodes terminate, we lose all data stored in the buckets. In our current design, since we allow each node to store their own buckets, it means we would need to backup each node. And, if we don't want data losses, we would also need to replicate the data.
+
+For those reasons, it is still very common to use a database (or any storage system) when writing production applications in Elixir, and use Elixir to implement the realtime and collaborative aspects of your applications that extend beyond storage. For example, we can use Elixir to track which clients are connected to the cluster at any given moment or implement a feed where users are notified in realtime whenever items are added or removed from a bucket.
+
+In fact, that's exactly what we will build in the next chapter. Allowing us to wrap up everything we have learned so far and also talk about one of the essential building blocks in Elixir software: GenServers.
diff --git a/lib/elixir/pages/mix-and-otp/config-and-releases.md b/lib/elixir/pages/mix-and-otp/config-and-releases.md
deleted file mode 100644
index 0eb7b83ac9c..00000000000
--- a/lib/elixir/pages/mix-and-otp/config-and-releases.md
+++ /dev/null
@@ -1,429 +0,0 @@
-<!--
-  SPDX-License-Identifier: Apache-2.0
-  SPDX-FileCopyrightText: 2021 The Elixir Team
--->
-
-# Configuration and releases
-
-In this last guide, we will make the routing table for our distributed key-value store configurable, and then finally package the software for production.
-
-Let's do this.
-
-## Application environment
-
-So far we have hard-coded the routing table into the `KV.Router` module. However, we would like to make the table dynamic. This allows us not only to configure development/test/production, but also to allow different nodes to run with different entries in the routing table. There is a feature of OTP that does exactly that: the application environment.
-
-Each application has an environment that stores the application's specific configuration by key. For example, we could store the routing table in the `:kv` application environment, giving it a default value and allowing other applications to change the table as needed.
-
-Open up `apps/kv/mix.exs` and change the `application/0` function to return the following:
-
-```elixir
-def application do
-  [
-    extra_applications: [:logger],
-    env: [routing_table: []],
-    mod: {KV, []}
-  ]
-end
-```
-
-We have added a new `:env` key to the application. It returns the application default environment, which has an entry of key `:routing_table` and value of an empty list. It makes sense for the application environment to ship with an empty table, as the specific routing table depends on the testing/deployment structure.
-
-In order to use the application environment in our code, we need to replace `KV.Router.table/0` with the definition below:
-
-```elixir
-@doc """
-The routing table.
-"""
-def table do
-  Application.fetch_env!(:kv, :routing_table)
-end
-```
-
-We use `Application.fetch_env!/2` to read the entry for `:routing_table` in `:kv`'s environment. You can find more information and other functions to manipulate the app environment in the `Application` module.
-
-Since our routing table is now empty, our distributed tests should fail. Restart the apps and re-run tests to see the failure:
-
-```console
-$ iex --sname bar -S mix
-$ elixir --sname foo -S mix test --only distributed
-```
-
-We need a way to configure the application environment. That's when we use configuration files.
-
-## Configuration
-
-Configuration files provide a mechanism for us to configure the environment of any application. Elixir provides two configuration entry points:
-
-  * `config/config.exs` — this file is read at build time, before we compile our application and before we even load our dependencies. This means we can't access the code in our application nor in our dependencies. However, it means we can control how they are compiled
-
-  * `config/runtime.exs` — this file is read after our application and dependencies are compiled and therefore it can configure how our application works at runtime. If you want to read system environment variables (via `System.get_env/1`) or access external configuration, this is the appropriate place to do so
-
-You can learn more about configuration in the `Config` and `Config.Provider` modules. For now, let's see an example.
-
-We can configure IEx default prompt to another value by creating a `config/runtime.exs` file with the following content:
-
-```elixir
-import Config
-config :iex, default_prompt: ">>>"
-```
-
-Start IEx with `iex -S mix` and you can see that the IEx prompt has changed.
-
-This means we can also configure our `:routing_table` directly in the `config/runtime.exs` file. However, which configuration value should we use?
-
-Currently we have two tests tagged with `@tag :distributed`. The "server interaction" test in `KVServerTest`, and the "route requests across nodes" in `KV.RouterTest`. Both tests are failing since they require a routing table, which is currently empty.
-
-For simplicity, we will define a routing table that always points to the current node. That's the table we will use for development and most of our tests. Back in `config/runtime.exs`, add this line:
-
-```elixir
-config :kv, :routing_table, [{?a..?z, node()}]
-```
-
-With such a simple table available, we can now remove `@tag :distributed` from the test in `test/kv_server_test.exs`. If you run the complete suite, the test should now pass.
-
-However, for the tests in `KV.RouterTest`, we effectively need two nodes in our routing table. To do so, we will write a setup block that runs before all tests in that file. The setup block will change the application environment and revert it back once we are done, like this:
-
-```elixir
-defmodule KV.RouterTest do
-  use ExUnit.Case
-
-  setup_all do
-    current = Application.get_env(:kv, :routing_table)
-
-    Application.put_env(:kv, :routing_table, [
-      {?a..?m, :"foo@computer-name"},
-      {?n..?z, :"bar@computer-name"}
-    ])
-
-    on_exit fn -> Application.put_env(:kv, :routing_table, current) end
-  end
-
-  @tag :distributed
-  test "route requests across nodes" do
-```
-
-Note we removed `async: true` from `use ExUnit.Case`. Since the application environment is a global storage, tests that modify it cannot run concurrently. With all changes in place, all tests should pass, including the distributed one.
-
-## Releases
-
-Now that our application runs distributed, you may be wondering how we can package our application to run in production. After all, all of our code so far depends on Erlang and Elixir versions that are installed in your current system. To achieve this goal, Elixir provides releases.
-
-A release is a self-contained directory that consists of your application code, all of its dependencies, plus the whole Erlang Virtual Machine (VM) and runtime. Once a release is assembled, it can be packaged and deployed to a target as long as the target runs on the same operating system (OS) distribution and version as the machine that assembled the release.
-
-In a regular project, we can assemble a release by simply running `mix release`. However, we have an umbrella project, and in such cases Elixir requires some extra input from us. Let's see what is necessary:
-
-```shell
-$ MIX_ENV=prod mix release
-** (Mix) Umbrella projects require releases to be explicitly defined with a non-empty applications key that chooses which umbrella children should be part of the releases:
-
-releases: [
-  foo: [
-    applications: [child_app_foo: :permanent]
-  ],
-  bar: [
-    applications: [child_app_bar: :permanent]
-  ]
-]
-
-Alternatively you can perform the release from the children applications
-```
-
-That's because an umbrella project gives us plenty of options when deploying the software. We can:
-
-  * deploy all applications in the umbrella to a node that will work as both TCP server and key-value storage
-
-  * deploy the `:kv_server` application to work only as a TCP server as long as the routing table points only to other nodes
-
-  * deploy only the `:kv` application when we want a node to work only as storage (no TCP access)
-
-As a starting point, let's define a release that includes both `:kv_server` and `:kv` applications. We will also add a version to it. Open up the `mix.exs` in the umbrella root and add inside `def project`:
-
-```elixir
-releases: [
-  foo: [
-    version: "0.0.1",
-    applications: [kv_server: :permanent, kv: :permanent]
-  ]
-]
-```
-
-That defines a release named `foo` with both `kv_server` and `kv` applications. Their mode is set to `:permanent`, which means that, if those applications crash, the whole node terminates. That's reasonable since those applications are essential to our system.
-
-Before we assemble the release, let's also define our routing table for production. Given we expect to have two nodes, we need to update `config/runtime.exs` to look like this:
-
-```elixir
-import Config
-
-config :kv, :routing_table, [{?a..?z, node()}]
-
-if config_env() == :prod do
-  config :kv, :routing_table, [
-    {?a..?m, :"foo@computer-name"},
-    {?n..?z, :"bar@computer-name"}
-  ]
-end
-```
-
-We have hard-coded the table and node names, which is good enough for our example, but you would likely move it to an external configuration system in an actual production setup. We have also wrapped it in a `config_env() == :prod` check, so this configuration does not apply to other environments.
-
-With the configuration in place, let's give assembling the release another try:
-
-    $ MIX_ENV=prod mix release foo
-    * assembling foo-0.0.1 on MIX_ENV=prod
-    * skipping runtime configuration (config/runtime.exs not found)
-
-    Release created at _build/prod/rel/foo!
-
-        # To start your system
-        _build/prod/rel/foo/bin/foo start
-
-    Once the release is running:
-
-        # To connect to it remotely
-        _build/prod/rel/foo/bin/foo remote
-
-        # To stop it gracefully (you may also send SIGINT/SIGTERM)
-        _build/prod/rel/foo/bin/foo stop
-
-    To list all commands:
-
-        _build/prod/rel/foo/bin/foo
-
-Excellent! A release was assembled in `_build/prod/rel/foo`. Inside the release, there will be a `bin/foo` file which is the entry point to your system. It supports multiple commands, such as:
-
-  * `bin/foo start`, `bin/foo start_iex`, `bin/foo restart`, and `bin/foo stop` — for general management of the release
-
-  * `bin/foo rpc COMMAND` and `bin/foo remote` — for running commands on the running system or to connect to the running system
-
-  * `bin/foo eval COMMAND` — to start a fresh system that runs a single command and then shuts down
-
-  * `bin/foo daemon` and `bin/foo daemon_iex` — to start the system as a daemon on Unix-like systems
-
-  * `bin/foo install` — to install the system as a service on Windows machines
-
-If you run `bin/foo start`, it will start the system using a short name (`--sname`) equal to the release name, which in this case is `foo`. The next step is to start a system named `bar`, so we can connect `foo` and `bar` together, like we did in the previous chapter. But before we achieve this, let's talk a bit about the benefits of releases.
-
-## Why releases?
-
-Releases allow developers to precompile and package all of their code and the runtime into a single unit. The benefits of releases are:
-
-  * Code preloading. The VM has two mechanisms for loading code: interactive and embedded. By default, it runs in the interactive mode which dynamically loads modules when they are used for the first time. The first time your application calls `Enum.map/2`, the VM will find the `Enum` module and load it. There's a downside. When you start a new server in production, it may need to load many other modules, causing the first requests to have an unusual spike in response time. Releases run in embedded mode, which loads all available modules upfront, guaranteeing your system is ready to handle requests after booting.
-
-  * Configuration and customization. Releases give developers fine grained control over system configuration and the VM flags used to start the system.
-
-  * Self-contained. A release does not require the source code to be included in your production artifacts. All of the code is precompiled and packaged. Releases do not even require Erlang or Elixir on your servers, as they include the Erlang VM and its runtime by default. Furthermore, both Erlang and Elixir standard libraries are stripped to bring only the parts you are actually using.
-
-  * Multiple releases. You can assemble different releases with different configuration per application or even with different applications altogether.
-
-We have written extensive documentation on releases, so [please check the official documentation for more information](`mix release`). For now, we will continue exploring some of the features outlined above.
-
-## Assembling multiple releases
-
-So far, we have assembled a release named `foo`, but our routing table contains information for both `foo` and `bar`. Let's start `foo`:
-
-    $ _build/prod/rel/foo/bin/foo start
-    16:58:58.508 [info]  Accepting connections on port 4040
-
-And let's connect to it and issue a request in another terminal:
-
-    $ telnet 127.0.0.1 4040
-    Trying 127.0.0.1...
-    Connected to localhost.
-    Escape character is '^]'.
-    CREATE bitsandpieces
-    OK
-    PUT bitsandpieces sword 1
-    OK
-    GET bitsandpieces sword
-    1
-    OK
-    GET shopping foo
-    Connection closed by foreign host.
-
-Our application works already when we operate on the bucket named "bitsandpieces". But since the "shopping" bucket would be stored on `bar`, the request fails as `bar` is not available. If you go back to the terminal running `foo`, you will see:
-
-    17:16:19.555 [error] Task #PID<0.622.0> started from #PID<0.620.0> terminating
-    ** (stop) exited in: GenServer.call({KV.RouterTasks, :"bar@computer-name"}, {:start_task, [{:"foo@josemac-2", #PID<0.622.0>, #PID<0.622.0>}, [#PID<0.622.0>, #PID<0.620.0>, #PID<0.618.0>], :monitor, {KV.Router, :route, ["shopping", KV.Registry, :lookup, [KV.Registry, "shopping"]]}], :temporary, nil}, :infinity)
-        ** (EXIT) no connection to bar@computer-name
-        (elixir) lib/gen_server.ex:1010: GenServer.call/3
-        (elixir) lib/task/supervisor.ex:454: Task.Supervisor.async/6
-        (kv) lib/kv/router.ex:21: KV.Router.route/4
-        (kv_server) lib/kv_server/command.ex:74: KVServer.Command.lookup/2
-        (kv_server) lib/kv_server.ex:29: KVServer.serve/1
-        (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
-        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
-    Function: #Function<0.128611034/0 in KVServer.loop_acceptor/1>
-        Args: []
-
-Let's now define a release for `:bar`. One first step could be to define a release exactly like `foo` inside `mix.exs`. Additionally we will set the `cookie` option on both releases to `weknoweachother` in order for them to allow connections from each other. See the [Distributed Erlang Documentation](http://www.erlang.org/doc/reference_manual/distributed.html) for further information on this topic:
-
-```elixir
-releases: [
-  foo: [
-    version: "0.0.1",
-    applications: [kv_server: :permanent, kv: :permanent],
-    cookie: "weknoweachother"
-  ],
-  bar: [
-    version: "0.0.1",
-    applications: [kv_server: :permanent, kv: :permanent],
-    cookie: "weknoweachother"
-  ]
-]
-```
-
-And now let's assemble both releases:
-
-```shell
-$ MIX_ENV=prod mix release foo
-$ MIX_ENV=prod mix release bar
-```
-
-Stop `foo` if it's still running and re-start it to load the `cookie`:
-
-```shell
-$ _build/prod/rel/foo/bin/foo start
-```
-
-And start `bar` in another terminal:
-
-```shell
-$ _build/prod/rel/bar/bin/bar start
-```
-
-You should see an error like the error below happen 5 times, before the application finally shuts down:
-
-```text
-    17:21:57.567 [error] Task #PID<0.620.0> started from KVServer.Supervisor terminating
-    ** (MatchError) no match of right hand side value: {:error, :eaddrinuse}
-        (kv_server) lib/kv_server.ex:12: KVServer.accept/1
-        (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
-        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
-    Function: #Function<0.98032413/0 in KVServer.Application.start/2>
-        Args: []
-```
-
-That's happening because the release `foo` is already listening on port `4040` and `bar` is trying to do the same! One option could be to move the `:port` configuration to the application environment, like we did for the routing table, and setup different ports per node.
-
-But let's try something else. Let's make it so the `bar` release contains only the `:kv` application. So it works as a storage but it won't have a front-end. Change the `:bar` information to this:
-
-```elixir
-releases: [
-  foo: [
-    version: "0.0.1",
-    applications: [kv_server: :permanent, kv: :permanent],
-    cookie: "weknoweachother"
-  ],
-  bar: [
-    version: "0.0.1",
-    applications: [kv: :permanent],
-    cookie: "weknoweachother"
-  ]
-]
-```
-
-And now let's assemble `bar` once more:
-
-    $ MIX_ENV=prod mix release bar
-
-And finally successfully boot it:
-
-    $ _build/prod/rel/bar/bin/bar start
-
-If you connect to localhost once again and perform another request, now everything should work, as long as the routing table contains the correct node names. Outstanding!
-
-With releases, we were able to "cut different slices" of our project and prepared them to run in production, all packaged into a single directory.
-
-## Configuring releases
-
-Releases also provide built-in hooks for configuring almost every need of the production system:
-
-  * `config/config.exs` — provides build-time application configuration, which is executed before our application compiles. This file often imports configuration files based on the environment, such as `config/dev.exs` and `config/prod.exs`.
-
-  * `config/runtime.exs` — provides runtime application configuration. It is executed every time the release boots and is further extensible via config providers.
-
-  * `rel/env.sh.eex` and `rel/env.bat.eex` — template files that are copied into every release and executed on every command to set up environment variables, including ones specific to the VM, and the general environment.
-
-  * `rel/vm.args.eex` — a template file that is copied into every release and provides static configuration of the Erlang Virtual Machine and other runtime flags.
-
-As we have seen, `config/config.exs` and `config/runtime.exs` are loaded during releases and regular Mix commands. On the other hand, `rel/env.sh.eex` and `rel/vm.args.eex` are specific to releases. Let's take a look.
-
-### Operating System environment configuration
-
-Every release contains an environment file, named `env.sh` on Unix-like systems and `env.bat` on Windows machines, that executes before the Elixir system starts. In this file, you can execute any OS-level code, such as invoke other applications, set environment variables and so on. Some of those environment variables can even configure how the release itself runs.
-
-For instance, releases run using short-names (`--sname`). However, if you want to actually run a distributed key-value store in production, you will need multiple nodes and start the release with the `--name` option. We can achieve this by setting the `RELEASE_DISTRIBUTION` environment variable inside the `env.sh` and `env.bat` files. Mix already has a template for said files which we can customize, so let's ask Mix to copy them to our application:
-
-    $ mix release.init
-    * creating rel/vm.args.eex
-    * creating rel/remote.vm.args.eex
-    * creating rel/env.sh.eex
-    * creating rel/env.bat.eex
-
-If you open up `rel/env.sh.eex`, you will see:
-
-```shell
-#!/bin/sh
-
-# # Sets and enables heart (recommended only in daemon mode)
-# case $RELEASE_COMMAND in
-#   daemon*)
-#     HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND"
-#     export HEART_COMMAND
-#     export ELIXIR_ERL_OPTIONS="-heart"
-#     ;;
-#   *)
-#     ;;
-# esac
-
-# # Set the release to load code on demand (interactive) instead of preloading (embedded).
-# export RELEASE_MODE=interactive
-
-# # Set the release to work across nodes.
-# # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
-# export RELEASE_DISTRIBUTION=name
-# export RELEASE_NODE=<%= @release.name %>
-```
-
-The steps necessary to work across nodes is already commented out as an example. You can enable full distribution by uncommenting the last two lines by removing the leading `# `.
-
-If you are on Windows, you will have to open up `rel/env.bat.eex`, where you will find this:
-
-```bat
-@echo off
-rem Set the release to load code on demand (interactive) instead of preloading (embedded).
-rem set RELEASE_MODE=interactive
-
-rem Set the release to work across nodes.
-rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
-rem set RELEASE_DISTRIBUTION=name
-rem set RELEASE_NODE=<%= @release.name %>
-```
-
-Once again, uncomment the last two lines by removing the leading `rem ` to enable full distribution. And that's all!
-
-### VM arguments
-
-The `rel/vm.args.eex` allows you to specify low-level flags that control how the Erlang VM and its runtime operate. You specify entries as if you were specifying arguments in the command line with code comments also supported. Here is the default generated file:
-
-    ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
-    ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
-
-    ## Increase number of concurrent ports/sockets
-    ##+Q 65536
-
-    ## Tweak GC to run more often
-    ##-env ERL_FULLSWEEP_AFTER 10
-
-You can see [a complete list of VM arguments and flags in the Erlang documentation](http://www.erlang.org/doc/man/erl.html).
-
-## Summing up
-
-Throughout the guide, we have built a very simple distributed key-value store as an opportunity to explore many constructs like generic servers, supervisors, tasks, agents, applications and more. Not only that, we have written tests for the whole application, got familiar with ExUnit, and learned how to use the Mix build tool to accomplish a wide range of tasks.
-
-If you are looking for a distributed key-value store to use in production, you should definitely look into [Riak](http://riak.com/products/riak-kv/), which also runs in the Erlang VM. In Riak, the buckets are replicated, to avoid data loss, and instead of a router, they use [consistent hashing](https://en.wikipedia.org/wiki/Consistent_hashing) to map a bucket to a node. A consistent hashing algorithm helps reduce the amount of data that needs to be migrated when new storage nodes are added to your live system.
-
-Of course, Elixir can be used for much more than distributed key-value stores. Embedded systems, data-processing and data-ingestion, web applications, audio/video streaming systems, and others are many of the different domains Elixir excels at. We hope this guide has prepared you to explore any of those domains or any future domain you may desire to bring Elixir into.
-
-Happy coding!
diff --git a/lib/elixir/pages/mix-and-otp/dependencies-and-umbrella-projects.md b/lib/elixir/pages/mix-and-otp/dependencies-and-umbrella-projects.md
deleted file mode 100644
index cecc976323f..00000000000
--- a/lib/elixir/pages/mix-and-otp/dependencies-and-umbrella-projects.md
+++ /dev/null
@@ -1,305 +0,0 @@
-<!--
-  SPDX-License-Identifier: Apache-2.0
-  SPDX-FileCopyrightText: 2021 The Elixir Team
--->
-
-# Dependencies and umbrella projects
-
-In this chapter, we will discuss how to manage dependencies in Mix.
-
-Our `kv` application is complete, so it's time to implement the server that will handle the requests we defined in the first chapter:
-
-```text
-CREATE shopping
-OK
-
-PUT shopping milk 1
-OK
-
-PUT shopping eggs 3
-OK
-
-GET shopping milk
-1
-OK
-
-DELETE shopping eggs
-OK
-```
-
-However, instead of adding more code to the `kv` application, we are going to build the TCP server as another application that is a client of the `kv` application. Since the whole runtime and Elixir ecosystem are geared towards applications, it makes sense to break our projects into smaller applications that work together rather than building a big, monolithic app.
-
-Before creating our new application, we must discuss how Mix handles dependencies. In practice, there are two kinds of dependencies we usually work with: internal and external dependencies. Mix supports mechanisms to work with both.
-
-## External dependencies
-
-External dependencies are the ones not tied to your business domain. For example, if you need an HTTP API for your distributed KV application, you can use the [Plug](https://github.com/elixir-lang/plug) project as an external dependency.
-
-Installing external dependencies is simple. Most commonly, we use the [Hex Package Manager](https://hex.pm), by listing the dependency inside the deps function in our `mix.exs` file:
-
-```elixir
-def deps do
-  [{:plug, "~> 1.0"}]
-end
-```
-
-This dependency refers to the latest version of Plug in the 1.x.x version series that has been pushed to Hex. This is indicated by the `~>` preceding the version number. For more information on specifying version requirements, see the documentation for the `Version` module.
-
-Typically, stable releases are pushed to Hex. If you want to depend on an external dependency still in development, Mix is able to manage Git dependencies too:
-
-```elixir
-def deps do
-  [{:plug, git: "https://github.com/elixir-lang/plug.git"}]
-end
-```
-
-You will notice that when you add a dependency to your project, Mix generates a `mix.lock` file that guarantees *repeatable builds*. The lock file must be checked in to your version control system, to guarantee that everyone who uses the project will use the same dependency versions as you.
-
-Mix provides many tasks for working with dependencies, which can be seen in `mix help`:
-
-```console
-$ mix help
-mix deps              # Lists dependencies and their status
-mix deps.clean        # Deletes the given dependencies' files
-mix deps.compile      # Compiles dependencies
-mix deps.get          # Gets all out of date dependencies
-mix deps.tree         # Prints the dependency tree
-mix deps.unlock       # Unlocks the given dependencies
-mix deps.update       # Updates the given dependencies
-```
-
-The most common tasks are `mix deps.get` and `mix deps.update`. Once fetched, dependencies are automatically compiled for you. You can read more about deps by typing `mix help deps`, and in the documentation for the `Mix.Tasks.Deps` module.
-
-## Internal dependencies
-
-Internal dependencies are the ones that are specific to your project. They usually don't make sense outside the scope of your project/company/organization. Most of the time, you want to keep them private, whether due to technical, economic or business reasons.
-
-If you have an internal dependency, Mix supports two methods to work with them: Git repositories or umbrella projects.
-
-For example, if you push the `kv` project to a Git repository, you'll need to list it in your deps code in order to use it:
-
-```elixir
-def deps do
-  [{:kv, git: "https://github.com/YOUR_ACCOUNT/kv.git"}]
-end
-```
-
-If the repository is private though, you may need to specify the private URL `git@github.com:YOUR_ACCOUNT/kv.git`. In any case, Mix will be able to fetch it for you as long as you have the proper credentials.
-
-Using Git repositories for internal dependencies is somewhat discouraged in Elixir. Remember that the runtime and the Elixir ecosystem already provide the concept of applications. As such, we expect you to frequently break your code into applications that can be organized logically, even within a single project.
-
-However, if you push every application as a separate project to a Git repository, your projects may become very hard to maintain as you will spend a lot of time managing those Git repositories rather than writing your code.
-
-For this reason, Mix supports "umbrella projects". Umbrella projects are used to build applications that run together in a single repository. That is exactly the style we are going to explore in the next sections.
-
-Let's create a new Mix project. We are going to creatively name it `kv_umbrella`, and this new project will have both the existing `kv` application and the new `kv_server` application inside. The directory structure will look like this:
-
-    + kv_umbrella
-      + apps
-        + kv
-        + kv_server
-
-The interesting thing about this approach is that Mix has many conveniences for working with such projects, such as the ability to compile and test all applications inside `apps` with a single command. However, even though they are all listed together inside `apps`, they are still decoupled from each other, so you can build, test and deploy each application in isolation if you want to.
-
-So let's get started!
-
-## Umbrella projects
-
-Let's start a new project using `mix new`. This new project will be named `kv_umbrella` and we need to pass the `--umbrella` option when creating it. Do not create this new project inside the existing `kv` project!
-
-```console
-$ mix new kv_umbrella --umbrella
-* creating README.md
-* creating .formatter.exs
-* creating .gitignore
-* creating mix.exs
-* creating apps
-* creating config
-* creating config/config.exs
-```
-
-From the printed information, we can see far fewer files are generated. The generated `mix.exs` file is different too. Let's take a look (comments have been removed):
-
-```elixir
-defmodule KvUmbrella.MixProject do
-  use Mix.Project
-
-  def project do
-    [
-      apps_path: "apps",
-      start_permanent: Mix.env() == :prod,
-      deps: deps()
-    ]
-  end
-
-  defp deps do
-    []
-  end
-end
-```
-
-What makes this project different from the previous one is the `apps_path: "apps"` entry in the project definition. This means this project will act as an umbrella. Such projects do not have source files nor tests, although they can have their own dependencies. Each child application must be defined inside the `apps` directory.
-
-Let's move inside the apps directory and start building `kv_server`. This time, we are going to pass the `--sup` flag, which will tell Mix to generate a supervision tree automatically for us, instead of building one manually as we did in previous chapters:
-
-```console
-$ cd kv_umbrella/apps
-$ mix new kv_server --module KVServer --sup
-```
-
-The generated files are similar to the ones we first generated for `kv`, with a few differences. Let's open up `mix.exs`:
-
-```elixir
-defmodule KVServer.MixProject do
-  use Mix.Project
-
-  def project do
-    [
-      app: :kv_server,
-      version: "0.1.0",
-      build_path: "../../_build",
-      config_path: "../../config/config.exs",
-      deps_path: "../../deps",
-      lockfile: "../../mix.lock",
-      elixir: "~> 1.14",
-      start_permanent: Mix.env() == :prod,
-      deps: deps()
-    ]
-  end
-
-  # Run "mix help compile.app" to learn about applications
-  def application do
-    [
-      extra_applications: [:logger],
-      mod: {KVServer.Application, []}
-    ]
-  end
-
-  # Run "mix help deps" to learn about dependencies
-  defp deps do
-    [
-      # {:dep_from_hexpm, "~> 0.3.0"},
-      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
-      # {:sibling_app_in_umbrella, in_umbrella: true},
-    ]
-  end
-end
-```
-
-First of all, since we generated this project inside `kv_umbrella/apps`, Mix automatically detected the umbrella structure and added four lines to the project definition:
-
-```elixir
-build_path: "../../_build",
-config_path: "../../config/config.exs",
-deps_path: "../../deps",
-lockfile: "../../mix.lock",
-```
-
-Those options mean all dependencies will be checked out to `kv_umbrella/deps`, and they will share the same build, config, and lock files. We haven't talked about configuration yet, but from here we can build the intuition that all configuration and dependencies are shared across all projects in an umbrella, and it is not per application.
-
-The second change is in the `application` function inside `mix.exs`:
-
-```elixir
-def application do
-  [
-    extra_applications: [:logger],
-    mod: {KVServer.Application, []}
-  ]
-end
-```
-
-Because we passed the `--sup` flag, Mix automatically added `mod: {KVServer.Application, []}`, specifying that `KVServer.Application` is our application callback module. `KVServer.Application` will start our application supervision tree.
-
-In fact, let's open up `lib/kv_server/application.ex`:
-
-```elixir
-defmodule KVServer.Application do
-  # See https://hexdocs.pm/elixir/Application.html
-  # for more information on OTP Applications
-  @moduledoc false
-
-  use Application
-
-  @impl true
-  def start(_type, _args) do
-    # List all child processes to be supervised
-    children = [
-      # Starts a worker by calling: KVServer.Worker.start_link(arg)
-      # {KVServer.Worker, arg},
-    ]
-
-    # See https://hexdocs.pm/elixir/Supervisor.html
-    # for other strategies and supported options
-    opts = [strategy: :one_for_one, name: KVServer.Supervisor]
-    Supervisor.start_link(children, opts)
-  end
-end
-```
-
-Notice that it defines the application callback function, `start/2`, and instead of defining a supervisor named `KVServer.Supervisor` that uses the `Supervisor` module, it conveniently defined the supervisor inline! You can read more about such supervisors by reading the `Supervisor` module documentation.
-
-We can already try out our first umbrella child. We could run tests inside the `apps/kv_server` directory, but that wouldn't be much fun. Instead, go to the root of the umbrella project and run `mix test`:
-
-```console
-$ mix test
-```
-
-And it works!
-
-Since we want `kv_server` to eventually use the functionality we defined in `kv`, we need to add `kv` as a dependency to our application.
-
-## Dependencies within an umbrella project
-
-Dependencies between applications in an umbrella project must still be explicitly defined and Mix makes it easy to do so. Open up `apps/kv_server/mix.exs` and change the `deps/0` function to the following:
-
-```elixir
-defp deps do
-  [{:kv, in_umbrella: true}]
-end
-```
-
-The line above makes `:kv` available as a dependency inside `:kv_server` and automatically starts the `:kv` application before the server starts.
-
-Finally, copy the `kv` application we have built so far to the `apps` directory in our new umbrella project. The final directory structure should match the structure we mentioned earlier:
-
-    + kv_umbrella
-      + apps
-        + kv
-        + kv_server
-
-We now need to modify `apps/kv/mix.exs` to contain the umbrella entries we have seen in `apps/kv_server/mix.exs`. Open up `apps/kv/mix.exs` and add to the `project/0` function:
-
-```elixir
-build_path: "../../_build",
-config_path: "../../config/config.exs",
-deps_path: "../../deps",
-lockfile: "../../mix.lock",
-```
-
-Now you can run tests for both projects from the umbrella root with `mix test`. Sweet!
-
-## Don't drink the kool aid
-
-Umbrella projects are a convenience to help you organize and manage multiple applications. While it provides a degree of separation between applications, those applications are not fully decoupled, as they share the same configuration and the same dependencies.
-
-The pattern of keeping multiple applications in the same repository is known as "mono-repo". Umbrella projects maximize this pattern by providing conveniences to compile, test and run multiple applications at once.
-
-If you find yourself in a position where you want to use different configurations in each application for the same dependency or use different dependency versions, then it is likely your codebase has grown beyond what umbrellas can provide.
-
-The good news is that breaking an umbrella apart is quite straightforward, as you simply need to move applications outside of the umbrella project's `apps/` directory and update the project's mix.exs file to no longer set the `build_path`, `config_path`, `deps_path`, and `lockfile` configuration. You can depend on private projects outside of the umbrella in multiple ways:
-
-  1. Move it to a separate folder within the same repository and point to it using a path dependency (the mono-repo pattern)
-  2. Move the repository to a separate Git repository and depend on it
-  3. Publish the project to a private [Hex.pm](https://hex.pm/) organization
-
-## Summing up
-
-In this chapter, we have learned more about Mix dependencies and umbrella projects. While we may run `kv` without a server, our `kv_server` depends directly on `kv`. By breaking them into separate applications, we gain more control in how they are developed and tested.
-
-When using umbrella applications, it is important to have a clear boundary between them. Our upcoming `kv_server` must only access public APIs defined in `kv`. Think of your umbrella apps as any other dependency or even Elixir itself: you can only access what is public and documented. Reaching into private functionality in your dependencies is a poor practice that will eventually cause your code to break when a new version is up.
-
-Umbrella applications can also be used as a stepping stone for eventually extracting an application from your codebase. For example, imagine a web application that has to send "push notifications" to its users. The whole "push notifications system" can be developed as a separate application in the umbrella, with its own supervision tree and APIs. If you ever run into a situation where another project needs the push notifications system, the system can be moved to a private repository or [a Hex package](https://hex.pm/).
-
-Finally, keep in mind that applications in an umbrella project all share the same configurations and dependencies. If two applications in your umbrella need to configure the same dependency in drastically different ways or even use different versions, you have probably outgrown the benefits brought by umbrellas. Remember you can break the umbrella and still leverage the benefits behind "mono-repos".
-
-With our umbrella project up and running, it is time to start writing our server.
diff --git a/lib/elixir/pages/mix-and-otp/distributed-tasks.md b/lib/elixir/pages/mix-and-otp/distributed-tasks.md
deleted file mode 100644
index 681bde4b70c..00000000000
--- a/lib/elixir/pages/mix-and-otp/distributed-tasks.md
+++ /dev/null
@@ -1,359 +0,0 @@
-<!--
-  SPDX-License-Identifier: Apache-2.0
-  SPDX-FileCopyrightText: 2021 The Elixir Team
--->
-
-# Distributed tasks and tags
-
-In this chapter, we will go back to the `:kv` application and add a routing layer that will allow us to distribute requests between nodes based on the bucket name.
-
-The routing layer will receive a routing table of the following format:
-
-```elixir
-[
-  {?a..?m, :"foo@computer-name"},
-  {?n..?z, :"bar@computer-name"}
-]
-```
-
-The router will check the first byte of the bucket name against the table and dispatch to the appropriate node based on that. For example, a bucket starting with the letter "a" (`?a` represents the Unicode codepoint of the letter "a") will be dispatched to node `foo@computer-name`.
-
-If the matching entry points to the node evaluating the request, then we've finished routing, and this node will perform the requested operation. If the matching entry points to a different node, we'll pass the request to said node, which will look at its own routing table (which may be different from the one in the first node) and act accordingly. If no entry matches, an error will be raised.
-
-> Note: we will be using two nodes in the same machine throughout this chapter. You are free to use two (or more) different machines on the same network but you need to do some prep work. First of all, you need to ensure all machines have a `~/.erlang.cookie` file with exactly the same value. Then you need to guarantee [epmd](https://www.erlang.org/doc/apps/erts/epmd_cmd) is running on a port that is not blocked (you can run `epmd -d` for debug info).
-
-## Our first distributed code
-
-Elixir ships with facilities to connect nodes and exchange information between them. In fact, we use the same concepts of processes, message passing and receiving messages when working in a distributed environment because Elixir processes are *location transparent*. This means that when sending a message, it doesn't matter if the recipient process is on the same node or on another node, the VM will be able to deliver the message in both cases.
-
-In order to run distributed code, we need to start the VM with a name. The name can be short (when in the same network) or long (requires the full computer address). Let's start a new IEx session:
-
-```console
-$ iex --sname foo
-```
-
-You can see now the prompt is slightly different and shows the node name followed by the computer name:
-
-    Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help)
-    iex(foo@jv)1>
-
-My computer is named `jv`, so I see `foo@jv` in the example above, but you will get a different result. We will use `foo@computer-name` in the following examples and you should update them accordingly when trying out the code.
-
-Let's define a module named `Hello` in this shell:
-
-```elixir
-iex> defmodule Hello do
-...>   def world, do: IO.puts("hello world")
-...> end
-```
-
-If you have another computer on the same network with both Erlang and Elixir installed, you can start another shell on it. If you don't, you can start another IEx session in another terminal. In either case, give it the short name of `bar`:
-
-```console
-$ iex --sname bar
-```
-
-Note that inside this new IEx session, we cannot access `Hello.world/0`:
-
-```elixir
-iex> Hello.world
-** (UndefinedFunctionError) function Hello.world/0 is undefined (module Hello is not available)
-    Hello.world()
-```
-
-However, we can spawn a new process on `foo@computer-name` from `bar@computer-name`! Let's give it a try (where `@computer-name` is the one you see locally):
-
-```elixir
-iex> Node.spawn_link(:"foo@computer-name", fn -> Hello.world() end)
-#PID<9014.59.0>
-hello world
-```
-
-Elixir spawned a process on another node and returned its PID. The code then executed on the other node where the `Hello.world/0` function exists and invoked that function. Note that the result of "hello world" was printed on the current node `bar` and not on `foo`. In other words, the message to be printed was sent back from `foo` to `bar`. This happens because the process spawned on the other node (`foo`) knows all the output should be sent back to the original node!
-
-We can send and receive messages from the PID returned by `Node.spawn_link/2` as usual. Let's try a quick ping-pong example:
-
-```elixir
-iex> pid = Node.spawn_link(:"foo@computer-name", fn ->
-...>   receive do
-...>     {:ping, client} -> send(client, :pong)
-...>   end
-...> end)
-#PID<9014.59.0>
-iex> send(pid, {:ping, self()})
-{:ping, #PID<0.73.0>}
-iex> flush()
-:pong
-:ok
-```
-
-From our quick exploration, we could conclude that we should use `Node.spawn_link/2` to spawn processes on a remote node every time we need to do a distributed computation. However, we have learned throughout this guide that spawning processes outside of supervision trees should be avoided if possible, so we need to look for other options.
-
-There are three better alternatives to `Node.spawn_link/2` that we could use in our implementation:
-
-1. We could use Erlang's [`:erpc`](`:erpc`) module to execute functions on a remote node. Inside the `bar@computer-name` shell above, you can call `:erpc.call(:"foo@computer-name", Hello, :world, [])` and it will print "hello world"
-
-2. We could have a server running on the other node and send requests to that node via the `GenServer` API. For example, you can call a server on a remote node by using `GenServer.call({name, node}, arg)` or passing the remote process PID as the first argument
-
-3. We could use [tasks](`Task`), which we have learned about in [a previous chapter](task-and-gen-tcp.md), as they can be spawned on both local and remote nodes
-
-The options above have different properties. The GenServer would serialize your requests on a single server, while tasks are effectively running asynchronously on the remote node, with the only serialization point being the spawning done by the supervisor.
-
-For our routing layer, we are going to use tasks, but feel free to explore the other alternatives too.
-
-## async/await
-
-So far we have explored tasks that are started and run in isolation, without regard to their return value. However, sometimes it is useful to run a task to compute a value and read its result later on. For this, tasks also provide the `async/await` pattern:
-
-```elixir
-task = Task.async(fn -> compute_something_expensive() end)
-res = compute_something_else()
-res + Task.await(task)
-```
-
-`async/await` provides a very simple mechanism to compute values concurrently. Not only that, `async/await` can also be used with the same `Task.Supervisor` we have used in previous chapters. We just need to call `Task.Supervisor.async/2` instead of `Task.Supervisor.start_child/2` and use `Task.await/2` to read the result later on.
-
-## Distributed tasks
-
-Distributed tasks are exactly the same as supervised tasks. The only difference is that we pass the node name when spawning the task on the supervisor. Open up `lib/kv/supervisor.ex` from the `:kv` application. Let's add a task supervisor as the last child of the tree:
-
-```elixir
-{Task.Supervisor, name: KV.RouterTasks},
-```
-
-Now, let's start two named nodes again, but inside the `:kv` application:
-
-```console
-$ iex --sname foo -S mix
-$ iex --sname bar -S mix
-```
-
-From inside `bar@computer-name`, we can now spawn a task directly on the other node via the supervisor:
-
-```elixir
-iex> task = Task.Supervisor.async({KV.RouterTasks, :"foo@computer-name"}, fn ->
-...>   {:ok, node()}
-...> end)
-%Task{
-  mfa: {:erlang, :apply, 2},
-  owner: #PID<0.122.0>,
-  pid: #PID<12467.88.0>,
-  ref: #Reference<0.0.0.400>
-}
-iex> Task.await(task)
-{:ok, :"foo@computer-name"}
-```
-
-Our first distributed task retrieves the name of the node the task is running on. Notice we have given an anonymous function to `Task.Supervisor.async/2` but, in distributed cases, it is preferable to give the module, function, and arguments explicitly:
-
-```elixir
-iex> task = Task.Supervisor.async({KV.RouterTasks, :"foo@computer-name"}, Kernel, :node, [])
-%Task{
-  mfa: {Kernel, :node, 0},
-  owner: #PID<0.122.0>,
-  pid: #PID<12467.89.0>,
-  ref: #Reference<0.0.0.404>
-}
-iex> Task.await(task)
-:"foo@computer-name"
-```
-
-The difference is that anonymous functions require the target node to have exactly the same code version as the caller. Using module, function, and arguments is more robust because you only need to find a function with matching arity in the given module.
-
-With this knowledge in hand, let's finally write the routing code.
-
-## Routing layer
-
-Create a file at `lib/kv/router.ex` with the following contents:
-
-```elixir
-defmodule KV.Router do
-  @doc """
-  Dispatch the given `mod`, `fun`, `args` request
-  to the appropriate node based on the `bucket`.
-  """
-  def route(bucket, mod, fun, args) do
-    # Get the first byte of the binary
-    first = :binary.first(bucket)
-
-    # Try to find an entry in the table() or raise
-    entry =
-      Enum.find(table(), fn {enum, _node} ->
-        first in enum
-      end) || no_entry_error(bucket)
-
-    # If the entry node is the current node
-    if elem(entry, 1) == node() do
-      apply(mod, fun, args)
-    else
-      {KV.RouterTasks, elem(entry, 1)}
-      |> Task.Supervisor.async(KV.Router, :route, [bucket, mod, fun, args])
-      |> Task.await()
-    end
-  end
-
-  defp no_entry_error(bucket) do
-    raise "could not find entry for #{inspect bucket} in table #{inspect table()}"
-  end
-
-  @doc """
-  The routing table.
-  """
-  def table do
-    # Replace computer-name with your local machine name
-    [{?a..?m, :"foo@computer-name"}, {?n..?z, :"bar@computer-name"}]
-  end
-end
-```
-
-Let's write a test to verify our router works. Create a file named `test/kv/router_test.exs` containing:
-
-```elixir
-defmodule KV.RouterTest do
-  use ExUnit.Case, async: true
-
-  test "route requests across nodes" do
-    assert KV.Router.route("hello", Kernel, :node, []) ==
-             :"foo@computer-name"
-    assert KV.Router.route("world", Kernel, :node, []) ==
-             :"bar@computer-name"
-  end
-
-  test "raises on unknown entries" do
-    assert_raise RuntimeError, ~r/could not find entry/, fn ->
-      KV.Router.route(<<0>>, Kernel, :node, [])
-    end
-  end
-end
-```
-
-The first test invokes `Kernel.node/0`, which returns the name of the current node, based on the bucket names "hello" and "world". According to our routing table so far, we should get `foo@computer-name` and `bar@computer-name` as responses, respectively.
-
-The second test checks that the code raises for unknown entries.
-
-In order to run the first test, we need to have two nodes running. Move into `apps/kv` and let's restart the node named `bar` which is going to be used by tests.
-
-```console
-$ iex --sname bar -S mix
-```
-
-And now run tests with:
-
-```console
-$ elixir --sname foo -S mix test
-```
-
-The test should pass.
-
-## Test filters and tags
-
-Although our tests pass, our testing structure is getting more complex. In particular, running tests with only `mix test` causes failures in our suite, since our test requires a connection to another node.
-
-Luckily, ExUnit ships with a facility to tag tests, allowing us to run specific callbacks or even filter tests altogether based on those tags. We have already used the `:capture_log` tag in the previous chapter, which has its semantics specified by ExUnit itself.
-
-This time let's add a `:distributed` tag to `test/kv/router_test.exs`:
-
-```elixir
-@tag :distributed
-test "route requests across nodes" do
-```
-
-Writing `@tag :distributed` is equivalent to writing `@tag distributed: true`.
-
-With the test properly tagged, we can now check if the node is alive on the network and, if not, we can exclude all distributed tests. Open up `test/test_helper.exs` inside the `:kv` application and add the following:
-
-```elixir
-exclude =
-  if Node.alive?(), do: [], else: [distributed: true]
-
-ExUnit.start(exclude: exclude)
-```
-
-Now run tests with `mix test`:
-
-```console
-$ mix test
-Excluding tags: [distributed: true]
-
-.......
-
-Finished in 0.05 seconds
-9 tests, 0 failures, 1 excluded
-```
-
-This time all tests passed and ExUnit warned us that distributed tests were being excluded. If you run tests with `$ elixir --sname foo -S mix test`, one extra test should run and successfully pass as long as the `bar@computer-name` node is available.
-
-The `mix test` command also allows us to dynamically include and exclude tags. For example, we can run `$ mix test --include distributed` to run distributed tests regardless of the value set in `test/test_helper.exs`. We could also pass `--exclude` to exclude a particular tag from the command line. Finally, `--only` can be used to run only tests with a particular tag:
-
-```console
-$ elixir --sname foo -S mix test --only distributed
-```
-
-You can read more about filters, tags, and the default tags in the `ExUnit.Case` module documentation.
-
-## Wiring it all up
-
-Now with our routing system in place, let's change `KVServer` to use the router. Replace the `lookup/2` function in `KVServer.Command` from this:
-
-```elixir
-defp lookup(bucket, callback) do
-  case KV.Registry.lookup(KV.Registry, bucket) do
-    {:ok, pid} -> callback.(pid)
-    :error -> {:error, :not_found}
-  end
-end
-```
-
-by this:
-
-```elixir
-defp lookup(bucket, callback) do
-  case KV.Router.route(bucket, KV.Registry, :lookup, [KV.Registry, bucket]) do
-    {:ok, pid} -> callback.(pid)
-    :error -> {:error, :not_found}
-  end
-end
-```
-
-Instead of directly looking up the registry, we are using the router instead to match a specific node. Then we get a `pid` that can be from any process in our cluster. From now on, `GET`, `PUT` and `DELETE` requests are all routed to the appropriate node.
-
-Let's also make sure that when a new bucket is created it ends up on the correct node. Replace the `run/1` function in `KVServer.Command`, the one that matches the `:create` command, with the following:
-
-```elixir
-def run({:create, bucket}) do
-  case KV.Router.route(bucket, KV.Registry, :create, [KV.Registry, bucket]) do
-    pid when is_pid(pid) -> {:ok, "OK\r\n"}
-    _ -> {:error, "FAILED TO CREATE BUCKET"}
-  end
-end
-```
-
-Now if you run the tests, you will see that an existing test that checks the server interaction will fail, as it will attempt to use the routing table. To address this failure, change the `test_helper.exs` for `:kv_server` application as we did for `:kv` and add `@tag :distributed` to this test too:
-
-```elixir
-@tag :distributed
-test "server interaction", %{socket: socket} do
-```
-
-However, keep in mind that by making the test distributed, we will likely run it less frequently, since we may not do the distributed setup on every test run. We will learn how to address this in the next chapter, by effectively learning how to make the routing table configurable.
-
-## Summing up
-
-We have only scratched the surface of what is possible when it comes to distribution.
-
-In all of our examples, we relied on Erlang's ability to automatically connect nodes whenever there is a request. For example, when we invoked `Node.spawn_link(:"foo@computer-name", fn -> Hello.world() end)`, Erlang automatically connected to said node and started a new process. However, you may also want to take a more explicit approach to connections, by using `Node.connect/1` and `Node.disconnect/1`.
-
-By default, Erlang establishes a fully meshed network, which means all nodes are connected to each other. Under this topology, the Erlang distribution is known to scale to several dozens of nodes in the same cluster. Erlang also has the concept of hidden nodes, which can allow developers to assemble custom topologies as seen in projects such as [Partisan](https://github.com/lasp-lang/partisan).
-
-In production, you may have nodes connecting and disconnecting at any time. In such scenarios, you need to provide *node discoverability*. Libraries such as [libcluster](https://github.com/bitwalker/libcluster/) and [dns_cluster](https://github.com/phoenixframework/dns_cluster) provide several strategies for node discoverability using DNS, Kubernetes, etc.
-
-Distributed key-value stores, used in real-life, need to consider the fact nodes may go up and down at any time and also migrate the bucket across nodes. Even further, buckets often need to be duplicated between nodes, so a failure in a node does not lead to the whole bucket being lost. This process is called *replication*. Our implementation won't attempt to tackle such problems. Instead, we assume there is a fixed number of nodes and therefore use a fixed routing table.
-
-These topics can be daunting at first but remember that most Elixir frameworks abstract those concerns for you. For example, when using [the Phoenix web framework](https://phoenixframework.org), its plug-and-play abstractions take care of sending messages and tracking how users join and leave a cluster. However, if you are interested in distributed systems after all, there is much to explore. Here are some additional references:
-
-  * [The excellent Distribunomicon chapter from Learn You Some Erlang](http://learnyousomeerlang.com/distribunomicon)
-  * Erlang's [`:global` module](`:global`), which can provide global names and global locks, allowing unique names and unique locks in a whole cluster of machines
-  * Erlang's [`:pg` module](`:pg`), which allows process to join different groups shared across the whole cluster
-  * [Phoenix PubSub project](https://github.com/phoenixframework/phoenix_pubsub), which provides a distributed messaging system and a distributed presence system for tracking users and processes in a cluster
-
-You will also find many libraries for building distributed systems within the overall Erlang ecosystem. For now, it is time to go back to our simple distributed key-value store and learn how to configure and package it for production.
diff --git a/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md b/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md
index e29949dded9..eb9d0c62412 100644
--- a/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md
+++ b/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md
@@ -25,7 +25,7 @@ DELETE shopping eggs
 OK
 ```
 
-After the parsing is done, we will update our server to dispatch the parsed commands to the `:kv` application we built previously.
+After the parsing is done, we will update our server to dispatch the parsed commands to the relevant buckets.
 
 ## Doctests
 
@@ -33,16 +33,16 @@ On the language homepage, we mention that Elixir makes documentation a first-cla
 
 In this section, we will implement the parsing functionality, document it and make sure our documentation is up to date with doctests. This helps us provide documentation with accurate code samples.
 
-Let's create our command parser at `lib/kv_server/command.ex` and start with the doctest:
+Let's create our command parser at `lib/kv/command.ex` and start with the doctest:
 
 ```elixir
-defmodule KVServer.Command do
+defmodule KV.Command do
   @doc ~S"""
   Parses the given `line` into a command.
 
   ## Examples
 
-      iex> KVServer.Command.parse("CREATE shopping\r\n")
+      iex> KV.Command.parse("CREATE shopping\r\n")
       {:ok, {:create, "shopping"}}
 
   """
@@ -56,29 +56,29 @@ Doctests are specified by an indentation of four spaces followed by the `iex>` p
 
 Also, note that we started the documentation string using `@doc ~S"""`. The `~S` prevents the `\r\n` characters from being converted to a carriage return and line feed until they are evaluated in the test.
 
-To run our doctests, we'll create a file at `test/kv_server/command_test.exs` and call `doctest KVServer.Command` in the test case:
+To run our doctests, we'll create a file at `test/kv/command_test.exs` and call `doctest KV.Command` in the test case:
 
 ```elixir
-defmodule KVServer.CommandTest do
+defmodule KV.CommandTest do
   use ExUnit.Case, async: true
-  doctest KVServer.Command
+  doctest KV.Command
 end
 ```
 
 Run the test suite and the doctest should fail:
 
 ```text
-  1) doctest KVServer.Command.parse/1 (1) (KVServer.CommandTest)
-     test/kv_server/command_test.exs:3
+  1) doctest KV.Command.parse/1 (1) (KV.CommandTest)
+     test/kv/command_test.exs:3
      Doctest failed
      doctest:
-       iex> KVServer.Command.parse("CREATE shopping\r\n")
+       iex> KV.Command.parse("CREATE shopping\r\n")
        {:ok, {:create, "shopping"}}
-     code: KVServer.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
+     code: KV.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
      left:  :not_implemented
      right: {:ok, {:create, "shopping"}}
      stacktrace:
-       lib/kv_server/command.ex:7: KVServer.Command (module)
+       lib/kv/command.ex:7: KV.Command (module)
 ```
 
 Excellent!
@@ -96,50 +96,50 @@ end
 Our implementation splits the line on whitespace and then matches the command against a list. Using `String.split/1` means our commands will be whitespace-insensitive. Leading and trailing whitespace won't matter, nor will consecutive spaces between words. Let's add some new doctests to test this behavior along with the other commands:
 
 ```elixir
-@doc ~S"""
-Parses the given `line` into a command.
+  @doc ~S"""
+  Parses the given `line` into a command.
 
-## Examples
+  ## Examples
 
-    iex> KVServer.Command.parse "CREATE shopping\r\n"
-    {:ok, {:create, "shopping"}}
+      iex> KV.Command.parse "CREATE shopping\r\n"
+      {:ok, {:create, "shopping"}}
 
-    iex> KVServer.Command.parse "CREATE  shopping  \r\n"
-    {:ok, {:create, "shopping"}}
+      iex> KV.Command.parse "CREATE  shopping  \r\n"
+      {:ok, {:create, "shopping"}}
 
-    iex> KVServer.Command.parse "PUT shopping milk 1\r\n"
-    {:ok, {:put, "shopping", "milk", "1"}}
+      iex> KV.Command.parse "PUT shopping milk 1\r\n"
+      {:ok, {:put, "shopping", "milk", "1"}}
 
-    iex> KVServer.Command.parse "GET shopping milk\r\n"
-    {:ok, {:get, "shopping", "milk"}}
+      iex> KV.Command.parse "GET shopping milk\r\n"
+      {:ok, {:get, "shopping", "milk"}}
 
-    iex> KVServer.Command.parse "DELETE shopping eggs\r\n"
-    {:ok, {:delete, "shopping", "eggs"}}
+      iex> KV.Command.parse "DELETE shopping eggs\r\n"
+      {:ok, {:delete, "shopping", "eggs"}}
 
-Unknown commands or commands with the wrong number of
-arguments return an error:
+  Unknown commands or commands with the wrong number of
+  arguments return an error:
 
-    iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
-    {:error, :unknown_command}
+      iex> KV.Command.parse "UNKNOWN shopping eggs\r\n"
+      {:error, :unknown_command}
 
-    iex> KVServer.Command.parse "GET shopping\r\n"
-    {:error, :unknown_command}
+      iex> KV.Command.parse "GET shopping\r\n"
+      {:error, :unknown_command}
 
-"""
+  """
 ```
 
 With doctests at hand, it is your turn to make tests pass! Once you're ready, you can compare your work with our solution below:
 
 ```elixir
-def parse(line) do
-  case String.split(line) do
-    ["CREATE", bucket] -> {:ok, {:create, bucket}}
-    ["GET", bucket, key] -> {:ok, {:get, bucket, key}}
-    ["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
-    ["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
-    _ -> {:error, :unknown_command}
+  def parse(line) do
+    case String.split(line) do
+      ["CREATE", bucket] -> {:ok, {:create, bucket}}
+      ["GET", bucket, key] -> {:ok, {:get, bucket, key}}
+      ["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
+      ["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
+      _ -> {:error, :unknown_command}
+    end
   end
-end
 ```
 
 Notice how we were able to elegantly parse the commands without adding a bunch of `if/else` clauses that check the command name and number of arguments!
@@ -147,104 +147,107 @@ Notice how we were able to elegantly parse the commands without adding a bunch o
 Finally, you may have observed that each doctest corresponds to a different test in our suite, which now reports a total of 7 doctests. That is because ExUnit considers the following to define two different doctests:
 
 ```elixir
-iex> KVServer.Command.parse("UNKNOWN shopping eggs\r\n")
+iex> KV.Command.parse("UNKNOWN shopping eggs\r\n")
 {:error, :unknown_command}
 
-iex> KVServer.Command.parse("GET shopping\r\n")
+iex> KV.Command.parse("GET shopping\r\n")
 {:error, :unknown_command}
 ```
 
 Without new lines, as seen below, ExUnit compiles it into a single doctest:
 
 ```elixir
-iex> KVServer.Command.parse("UNKNOWN shopping eggs\r\n")
+iex> KV.Command.parse("UNKNOWN shopping eggs\r\n")
 {:error, :unknown_command}
-iex> KVServer.Command.parse("GET shopping\r\n")
+iex> KV.Command.parse("GET shopping\r\n")
 {:error, :unknown_command}
 ```
 
 As the name says, doctest is documentation first and a test later. Their goal is not to replace tests but to provide up-to-date documentation. You can read more about doctests in the `ExUnit.DocTest` documentation.
 
-## `with`
+## Using `with`
 
 As we are now able to parse commands, we can finally start implementing the logic that runs the commands. Let's add a stub definition for this function for now:
 
 ```elixir
-defmodule KVServer.Command do
+defmodule KV.Command do
   @doc """
   Runs the given command.
   """
-  def run(command) do
-    {:ok, "OK\r\n"}
+  def run(command, socket) do
+    :gen_tcp.send(socket, "OK\r\n")
+    :ok
   end
 end
 ```
 
-Before we implement this function, let's change our server to start using our new `parse/1` and `run/1` functions. Remember, our `read_line/1` function was also crashing when the client closed the socket, so let's take the opportunity to fix it, too. Open up `lib/kv_server.ex` and replace the existing server definition:
+Before we implement this function, let's change our server to start using our new `parse/1` and `run/1` functions. Remember, our `read_line/1` function was also crashing when the client closed the socket, so let's take the opportunity to fix it, too. Open up `lib/kv/server.ex` and replace the existing server definition:
 
 ```elixir
-defp serve(socket) do
-  socket
-  |> read_line()
-  |> write_line(socket)
+  defp serve(socket) do
+    socket
+    |> read_line()
+    |> write_line(socket)
 
-  serve(socket)
-end
+    serve(socket)
+  end
 
-defp read_line(socket) do
-  {:ok, data} = :gen_tcp.recv(socket, 0)
-  data
-end
+  defp read_line(socket) do
+    {:ok, data} = :gen_tcp.recv(socket, 0)
+    data
+  end
 
-defp write_line(line, socket) do
-  :gen_tcp.send(socket, line)
-end
+  defp write_line(line, socket) do
+    :gen_tcp.send(socket, line)
+  end
 ```
 
 by the following:
 
 ```elixir
-defp serve(socket) do
-  msg =
-    case read_line(socket) do
-      {:ok, data} ->
-        case KVServer.Command.parse(data) do
-          {:ok, command} ->
-            KVServer.Command.run(command)
-          {:error, _} = err ->
-            err
-        end
-      {:error, _} = err ->
-        err
-    end
-
-  write_line(socket, msg)
-  serve(socket)
-end
+  defp serve(socket) do
+    msg =
+      case read_line(socket) do
+        {:ok, data} ->
+          case KV.Command.parse(data) do
+            {:ok, command} ->
+              KV.Command.run(command, socket)
+
+            {:error, _} = err ->
+              err
+          end
+
+        {:error, _} = err ->
+          err
+      end
+
+    write_line(socket, msg)
+    serve(socket)
+  end
 
-defp read_line(socket) do
-  :gen_tcp.recv(socket, 0)
-end
+  defp read_line(socket) do
+    :gen_tcp.recv(socket, 0)
+  end
 
-defp write_line(socket, {:ok, text}) do
-  :gen_tcp.send(socket, text)
-end
+  defp write_line(_socket, :ok) do
+    :ok
+  end
 
-defp write_line(socket, {:error, :unknown_command}) do
-  # Known error; write to the client
-  :gen_tcp.send(socket, "UNKNOWN COMMAND\r\n")
-end
+  defp write_line(socket, {:error, :unknown_command}) do
+    # Known error; write to the client
+    :gen_tcp.send(socket, "UNKNOWN COMMAND\r\n")
+  end
 
-defp write_line(_socket, {:error, :closed}) do
-  # The connection was closed, exit politely
-  exit(:shutdown)
-end
+  defp write_line(_socket, {:error, :closed}) do
+    # The connection was closed, exit politely
+    exit(:shutdown)
+  end
 
-defp write_line(socket, {:error, error}) do
-  # Unknown error; write to the client and exit
-  :gen_tcp.send(socket, "ERROR\r\n")
-  exit(error)
-end
+  defp write_line(socket, {:error, error}) do
+    # Unknown error; write to the client and exit
+    :gen_tcp.send(socket, "ERROR\r\n")
+    exit(error)
+  end
 ```
 
 If we start our server, we can now send commands to it. For now, we will get two different responses: "OK" when the command is known and "UNKNOWN COMMAND" otherwise:
@@ -264,18 +267,18 @@ This means our implementation is going in the correct direction, but it doesn't
 
 The previous implementation used pipelines which made the logic straightforward to follow. However, now that we need to handle different error codes along the way, our server logic is nested inside many `case` calls.
 
-Thankfully, Elixir v1.2 introduced the `with` construct, which allows you to simplify code like the above, replacing nested `case` calls with a chain of matching clauses. Let's rewrite the `serve/1` function to use `with`:
+Thankfully, Elixir has the `with` construct, which allows you to simplify code like the above, replacing nested `case` calls with a chain of matching clauses. Let's rewrite the `serve/1` function to use `with`:
 
 ```elixir
-defp serve(socket) do
-  msg =
-    with {:ok, data} <- read_line(socket),
-         {:ok, command} <- KVServer.Command.parse(data),
-         do: KVServer.Command.run(command)
-
-  write_line(socket, msg)
-  serve(socket)
-end
+  defp serve(socket) do
+    msg =
+      with {:ok, data} <- read_line(socket),
+          {:ok, command} <- KV.Command.parse(data),
+          do: KV.Command.run(command, socket)
+
+    write_line(socket, msg)
+    serve(socket)
+  end
 ```
 
 Much better! `with` will retrieve the value returned by the right-side of `<-` and match it against the pattern on the left side. If the value matches the pattern, `with` moves on to the next expression. In case there is no match, the non-matching value is returned.
@@ -286,55 +289,60 @@ You can read more about `with/1` in our documentation.
 
 ## Running commands
 
-The last step is to implement `KVServer.Command.run/1`, to run the parsed commands against the `:kv` application. Its implementation is shown below:
+The last step is to implement `KV.Command.run/1` to run the parsed commands on top of buckets. Its implementation is shown below:
 
 ```elixir
-@doc """
-Runs the given command.
-"""
-def run(command)
-
-def run({:create, bucket}) do
-  KV.Registry.create(KV.Registry, bucket)
-  {:ok, "OK\r\n"}
-end
+  @doc """
+  Runs the given command.
+  """
+  def run(command, socket)
 
-def run({:get, bucket, key}) do
-  lookup(bucket, fn pid ->
-    value = KV.Bucket.get(pid, key)
-    {:ok, "#{value}\r\nOK\r\n"}
-  end)
-end
+  def run({:create, bucket}, socket) do
+    KV.create_bucket(bucket)
+    :gen_tcp.send(socket, "OK\r\n")
+    :ok
+  end
 
-def run({:put, bucket, key, value}) do
-  lookup(bucket, fn pid ->
-    KV.Bucket.put(pid, key, value)
-    {:ok, "OK\r\n"}
-  end)
-end
+  def run({:get, bucket, key}, socket) do
+    lookup(bucket, fn pid ->
+      value = KV.Bucket.get(pid, key)
+      :gen_tcp.send(socket, "#{value}\r\nOK\r\n")
+      :ok
+    end)
+  end
 
-def run({:delete, bucket, key}) do
-  lookup(bucket, fn pid ->
-    KV.Bucket.delete(pid, key)
-    {:ok, "OK\r\n"}
-  end)
-end
+  def run({:put, bucket, key, value}, socket) do
+    lookup(bucket, fn pid ->
+      KV.Bucket.put(pid, key, value)
+      :gen_tcp.send(socket, "OK\r\n")
+      :ok
+    end)
+  end
 
-defp lookup(bucket, callback) do
-  case KV.Registry.lookup(KV.Registry, bucket) do
-    {:ok, pid} -> callback.(pid)
-    :error -> {:error, :not_found}
+  def run({:delete, bucket, key}, socket) do
+    lookup(bucket, fn pid ->
+      KV.Bucket.delete(pid, key)
+      :gen_tcp.send(socket, "OK\r\n")
+      :ok
+    end)
+  end
+
+  defp lookup(bucket, callback) do
+    if bucket = KV.lookup_bucket(bucket) do
+      callback.(bucket)
+    else
+      {:error, :not_found}
+    end
   end
-end
 ```
 
-Every function clause dispatches the appropriate command to the `KV.Registry` server that we registered during the `:kv` application startup. Since our `:kv_server` depends on the `:kv` application, it is completely fine to depend on the services it provides.
+Each function clause dispatches the appropriate command to the appropriate bucket.
 
-You might have noticed we have a function head, `def run(command)`, without a body. In the [Modules and Functions](../getting-started/modules-and-functions.md#default-arguments) chapter, we learned that a bodiless function can be used to declare default arguments for a multi-clause function. Here is another use case where we use a function without a body to document what the arguments are.
+You might have noticed we have a function head, `def run(command, socket)`, without a body. In the [Modules and Functions](../getting-started/modules-and-functions.md#default-arguments) chapter, we learned that a bodiless function can be used to declare default arguments for a multi-clause function. Here is another use case where we use a function without a body to document what the arguments are.
 
-Note that we have also defined a private function named `lookup/2` to help with the common functionality of looking up a bucket and returning its `pid` if it exists, `{:error, :not_found}` otherwise.
+We have also defined a private function named `lookup/2` to help with the common functionality of looking up a bucket and returning its `pid` if it exists, `{:error, :not_found}` otherwise.
 
-By the way, since we are now returning `{:error, :not_found}`, we should amend the `write_line/2` function in `KVServer` to print such error as well:
+By the way, since we are now returning `{:error, :not_found}`, we should amend the `write_line/2` function in `KV.Server` to print such error as well:
 
 ```elixir
 defp write_line(socket, {:error, :not_found}) do
@@ -342,67 +350,57 @@ defp write_line(socket, {:error, :not_found}) do
 end
 ```
 
-Our server functionality is almost complete. Only tests are missing. This time, we have left tests for last because there are some important considerations to be made.
-
-`KVServer.Command.run/1`'s implementation is sending commands directly to the server named `KV.Registry`, which is registered by the `:kv` application. This means this server is global and if we have two tests sending messages to it at the same time, our tests will conflict with each other (and likely fail). We need to decide between having unit tests that are isolated and can run asynchronously, or writing integration tests that work on top of the global state, but exercise our application's full stack as it is meant to be exercised in production.
-
-So far we have only written unit tests, typically testing a single module directly. However, in order to make `KVServer.Command.run/1` testable as a unit we would need to change its implementation to not send commands directly to the `KV.Registry` process but instead pass a server as an argument. For example, we would need to change `run`'s signature to `def run(command, pid)` and then change all clauses accordingly:
+Our server functionality is almost complete. Only tests are missing.
 
-```elixir
-def run({:create, bucket}, pid) do
-  KV.Registry.create(pid, bucket)
-  {:ok, "OK\r\n"}
-end
-
-# ... other run clauses ...
-```
+## Integration tests
 
-Feel free to go ahead and do the changes above and write some unit tests. The idea is that your tests will start an instance of the `KV.Registry` and pass it as an argument to `run/2` instead of relying on the global `KV.Registry`. This has the advantage of keeping our tests asynchronous as there is no shared state.
+`KV.Command.run/1`'s implementation is sending commands directly to the `KV` module, which is using a local registry to name processes. This means if we have two tests sending messages to the same bucket, our tests will conflict with each other (and likely fail). One might think this would be a reason to use mocks and other strategies to keep our tests isolated, but such techniques often make our testing environment too distant from how our code actually runs in production, and you may end-up with bugs lurking.
 
-But let's also try something different. Let's write integration tests that rely on the global server names to exercise the whole stack from the TCP server to the bucket. Our integration tests will rely on global state and must be synchronous. With integration tests, we get coverage on how the components in our application work together at the cost of test performance. They are typically used to test the main flows in your application. For example, we should avoid using integration tests to test an edge case in our command parsing implementation.
+Luckily, there is a technique that we have been using throughout this guide that would be equally applicable here: it is ok to rely on the local registry as long as each test uses unique names. Using a combination of the test module and test name is more than enough to guarantee that.
 
-Our integration test will use a TCP client that sends commands to our server and assert we are getting the desired responses.
+So let's write integration tests that rely on unique names to exercise the whole stack from the TCP server to the bucket.
 
-Let's implement the integration test in `test/kv_server_test.exs` as shown below:
+Create a new file at `test/kv/server_test.exs` as shown below:
 
 ```elixir
-defmodule KVServerTest do
-  use ExUnit.Case
+defmodule KV.ServerTest do
+  use ExUnit.Case, async: true
 
-  setup do
-    Application.stop(:kv)
-    :ok = Application.start(:kv)
-  end
+  @socket_options [:binary, packet: :line, active: false]
 
-  setup do
-    opts = [:binary, packet: :line, active: false]
-    {:ok, socket} = :gen_tcp.connect(~c"localhost", 4040, opts)
-    %{socket: socket}
+  setup config do
+    {:ok, socket} = :gen_tcp.connect(~c"localhost", 4040, @socket_options)
+    test_name = config.test |> Atom.to_string() |> String.replace(" ", "-")
+    %{socket: socket, name: "#{config.module}-#{test_name}"}
   end
 
-  test "server interaction", %{socket: socket} do
-    assert send_and_recv(socket, "UNKNOWN shopping\r\n") ==
-           "UNKNOWN COMMAND\r\n"
+  test "server interaction", %{socket: socket, name: name} do
+    # CREATE
+    assert send_and_recv(socket, "CREATE #{name}\r\n") == "OK\r\n"
 
-    assert send_and_recv(socket, "GET shopping eggs\r\n") ==
-           "NOT FOUND\r\n"
+    # PUT
+    assert send_and_recv(socket, "PUT #{name} eggs 3\r\n") == "OK\r\n"
 
-    assert send_and_recv(socket, "CREATE shopping\r\n") ==
-           "OK\r\n"
+    # GET
+    assert send_and_recv(socket, "GET #{name} eggs\r\n") == "3\r\n"
+    assert send_and_recv(socket, "") == "OK\r\n"
 
-    assert send_and_recv(socket, "PUT shopping eggs 3\r\n") ==
-           "OK\r\n"
+    # DELETE
+    assert send_and_recv(socket, "DELETE #{name} eggs\r\n") == "OK\r\n"
 
-    # GET returns two lines
-    assert send_and_recv(socket, "GET shopping eggs\r\n") == "3\r\n"
+    # GET
+    assert send_and_recv(socket, "GET #{name} eggs\r\n") == "\r\n"
     assert send_and_recv(socket, "") == "OK\r\n"
+  end
 
-    assert send_and_recv(socket, "DELETE shopping eggs\r\n") ==
-           "OK\r\n"
+  test "unknown command", %{socket: socket} do
+    assert send_and_recv(socket, "WHATEVER\r\n") ==
+             "UNKNOWN COMMAND\r\n"
+  end
 
-    # GET returns two lines
-    assert send_and_recv(socket, "GET shopping eggs\r\n") == "\r\n"
-    assert send_and_recv(socket, "") == "OK\r\n"
+  test "unknown bucket", %{socket: socket} do
+    assert send_and_recv(socket, "GET whatever eggs\r\n") ==
+             "NOT FOUND\r\n"
   end
 
   defp send_and_recv(socket, command) do
@@ -413,38 +411,16 @@ defmodule KVServerTest do
 end
 ```
 
-Our integration test checks all server interaction, including unknown commands and not found errors. It is worth noting that, as with ETS tables and linked processes, there is no need to close the socket. Once the test process exits, the socket is automatically closed.
-
-This time, since our test relies on global data, we have not given `async: true` to `use ExUnit.Case`. Furthermore, in order to guarantee our test is always in a clean state, we stop and start the `:kv` application before each test. In fact, stopping the `:kv` application even prints a warning on the terminal:
-
-```text
-18:12:10.698 [info] Application kv exited: :stopped
-```
+Run `mix test` and the tests should all pass. However, make sure to terminate any `iex -S mix` session you may have running, as currently tests and development environment are running on the same port (4040). We will address it in the next chapter.
 
-To avoid printing log messages during tests, ExUnit provides a neat feature called `:capture_log`. By setting `@tag :capture_log` before each test or `@moduletag :capture_log` for the whole test module, ExUnit will automatically capture anything that is logged while the test runs. In case our test fails, the captured logs will be printed alongside the ExUnit report.
+We added three tests, the first one tests most bucket actions, while the other two deal with error cases. Given there is a lot of shared setup across these tests, we used the `setup/2` macro to deal with common boilerplate. The macro receives the same *test context* as tests and starts a client TCP connection per test. It also defines a unique bucket name using the module name and the test name, making sure any space in the test name is replaced by `-` as to not interfere with our command parsing logic.
 
-Between `use ExUnit.Case` and `setup`, add the following call:
+Then, in each test, we pattern matched on the *test context*, extracting the socket or name as necessary. This is similar to the code we wrote in `test/kv/bucket_test.exs`:
 
 ```elixir
-@moduletag :capture_log
+  test "stores values by key on a named process", config do
 ```
 
-In case the test crashes, you will see a report as follows:
-
-```text
-  1) test server interaction (KVServerTest)
-     test/kv_server_test.exs:17
-     ** (RuntimeError) oops
-     stacktrace:
-       test/kv_server_test.exs:29
-
-     The following output was logged:
-
-     13:44:10.035 [notice] Application kv exited: :stopped
-```
-
-With this simple integration test, we start to see why integration tests may be slow. Not only can this test not run asynchronously, but it also requires the expensive setup of stopping and starting the `:kv` application.
-
-At the end of the day, it is up to you and your team to figure out the best testing strategy for your applications. You need to balance code quality, confidence, and test suite runtime. For example, we may start with testing the server only with integration tests, but if the server continues to grow in future releases, or it becomes a part of the application with frequent bugs, it is important to consider breaking it apart and writing more intensive unit tests that don't have the weight of an integration test.
+Except back then we matched on all config and, this time around, we matched only on the data we needed.
 
-Let's move to the next chapter. We will finally make our system distributed by adding a bucket routing mechanism. We will use this opportunity to also improve our testing chops.
+Let's move to the next chapter. We will finally make our system distributed by adding a tiny bit of configuration and, *spoiler alert*, changing one line of code.
diff --git a/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md b/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md
index 8c83e4bb783..78ea475582d 100644
--- a/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md
+++ b/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md
@@ -5,161 +5,192 @@
 
 # Supervising dynamic children
 
-We have now successfully defined our supervisor which is automatically started (and stopped) as part of our application life cycle.
+We have successfully learned how our supervision tree is automatically started (and stopped) as part of our application's life cycle. We can also name our buckets via the `:name` option. We also learned that, in practice, we should always start new processes inside supervisors. Let's apply these insights by ensuring our buckets are named and supervised.
 
-Remember, however, that our `KV.Registry` is both linking (via `start_link`) and monitoring (via `monitor`) bucket processes in the `handle_cast/2` callback:
+## Child specs
+
+Supervisors know how to start processes because they are given "child specifications". In our `lib/kv.ex` file, we defined a list of children with a single child spec:
 
 ```elixir
-{:ok, bucket} = KV.Bucket.start_link([])
-ref = Process.monitor(bucket)
+    children = [
+      {Registry, name: KV, keys: :unique}
+    ]
 ```
 
-Links are bidirectional, which implies that a crash in a bucket will crash the registry. Although we now have the supervisor, which guarantees the registry will be back up and running, crashing the registry still means we lose all data associating bucket names to their respective processes.
-
-In other words, we want the registry to keep on running even if a bucket crashes. Let's write a new registry test:
+When the child specification is a tuple (as above) or module, then it is equivalent to calling the `child_spec/1` function on said module, which then returns the full specification. The pair above is equivalent to:
 
 ```elixir
-test "removes bucket on crash", %{registry: registry} do
-  KV.Registry.create(registry, "shopping")
-  {:ok, bucket} = KV.Registry.lookup(registry, "shopping")
-
-  # Stop the bucket with non-normal reason
-  Agent.stop(bucket, :shutdown)
-  assert KV.Registry.lookup(registry, "shopping") == :error
-end
+iex> Registry.child_spec(name: KV, keys: :unique)
+%{
+  id: KV,
+  start: {Registry, :start_link, [[name: KV, keys: :unique]]},
+  type: :supervisor
+}
 ```
 
-The test is similar to "removes bucket on exit" except that we are being a bit more harsh by sending `:shutdown` as the exit reason instead of `:normal`. If a process terminates with a reason other than `:normal`, all linked processes receive an EXIT signal, causing the linked process to also terminate unless it is trapping exits.
+The underlying map returns the `:id` (required), the module-function-args triplet to invoke to start the process (required), the type of the process (optional), among other optional keys. In other words, the `child_spec/1` function allows us to compose and encapsulate specifications in modules.
 
-Since the bucket terminated, the registry also stopped, and our test fails when trying to `GenServer.call/3` it:
+Therefore, if we want to supervise `KV.Bucket`, we only need to define a `child_spec/1` function. Luckily for us, whenever we invoke `use Agent` (or `use GenServer` or `use Supervisor` and so forth), an implementation with reasonable defaults is provided. So let's take it for a spin. Back on `iex -S mix`, try this:
 
-```text
-  1) test removes bucket on crash (KV.RegistryTest)
-     test/kv/registry_test.exs:26
-     ** (exit) exited in: GenServer.call(#PID<0.148.0>, {:lookup, "shopping"}, 5000)
-         ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
-     code: assert KV.Registry.lookup(registry, "shopping") == :error
-     stacktrace:
-       (elixir) lib/gen_server.ex:770: GenServer.call/3
-       test/kv/registry_test.exs:33: (test)
+```elixir
+iex> KV.Bucket.child_spec([])
+%{id: KV.Bucket, start: {KV.Bucket, :start_link, [[]]}}
+iex> KV.Bucket.child_spec([name: :shopping])
+%{id: KV.Bucket, start: {KV.Bucket, :start_link, [[name: :shopping]]}}
 ```
 
-We are going to solve this issue by defining a new supervisor that will spawn and supervise all buckets. Opposite to the previous Supervisor we defined, the children are not known upfront, but they are rather started dynamically. For those situations, we use a supervisor optimized to such use cases called `DynamicSupervisor`. The `DynamicSupervisor` does not expect a list of children during initialization; instead each child is started manually via `DynamicSupervisor.start_child/2`.
+Let's try to start it as part of a supervisor then, using the `{module, options}` format to pass the bucket name (let's also use an atom as the name for convenience):
 
-## The bucket supervisor
+```elixir
+iex> children = [{KV.Bucket, name: :shopping}]
+iex> Supervisor.start_link(children, strategy: :one_for_one)
+iex> KV.Bucket.put(:shopping, "milk", 1)
+:ok
+iex> KV.Bucket.get(:shopping, "milk")
+1
+```
 
-Since a `DynamicSupervisor` does not define any children during initialization, the `DynamicSupervisor` also allows us to skip the work of defining a whole separate module with the usual `start_link` function and the `init` callback. Instead, we can define a `DynamicSupervisor` directly in the supervision tree, by giving it a name and a strategy.
+What happens now if we explicitly kill the bucket process?
+
+```elixir
+# Find the pid for the given name
+iex> pid = Process.whereis(:shopping)
+#PID<0.48.0>
+# Send it a kill exit signal
+iex> Process.exit(pid, :kill)
+true
+# But a new process is alive in its place
+iex> Process.whereis(:shopping)
+#PID<0.50.0>
+```
 
-Open up `lib/kv/supervisor.ex` and add the dynamic supervisor as a child as follows:
+Given our buckets can already be supervised, it is time to hook them into our supervision tree.
+
+## Dynamic supervisors
+
+Given our buckets can already be supervised, you may be thinking to start them as part of our application `start/2` callback, such as:
 
 ```elixir
-  def init(:ok) do
     children = [
-      {KV.Registry, name: KV.Registry},
-      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one}
+      {Registry, name: KV, keys: :unique}
+      {KV.Bucket, name: {:via, Registry, {KV, "shopping"}}}
     ]
-
-    Supervisor.init(children, strategy: :one_for_one)
-  end
 ```
 
-Remember that the name of a process can be any atom. So far, we have named processes with the same name as the modules that define their implementation. For example, the process defined by `KV.Registry` was given a process name of `KV.Registry`. This is simply a convention: If later there is an error in your system that says, "process named KV.Registry crashed with reason", we know exactly where to investigate.
-
-In this case, there is no module, so we picked the name `KV.BucketSupervisor`. It could have been any other name. We also chose the `:one_for_one` strategy, which is currently the only available strategy for dynamic supervisors.
+And while the above would definitely work, it comes with a huge caveat: it only starts a single bucket. In practice, we want the user to be able to create new buckets at any time. In other words, we need to start and supervise processes dynamically.
 
-Run `iex -S mix` so we can give our dynamic supervisor a try:
+While the `Supervisor` module has APIs for starting children after its initialization, it was not designed or optimized for the use case of having potentially millions of children. For this purpose, Elixir instead provides the `DynamicSupervisor` module. Using it is quite similar to `Supervisor` except that, instead of specifying the children during start, you do it afterwards. Let's take it for a spin:
 
 ```elixir
-iex> {:ok, bucket} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
-{:ok, #PID<0.72.0>}
-iex> KV.Bucket.put(bucket, "eggs", 3)
+iex> {:ok, sup_pid} = DynamicSupervisor.start_link(strategy: :one_for_one)
+iex> DynamicSupervisor.start_child(sup_pid, {KV.Bucket, name: :another_list})
+iex> KV.Bucket.put(:another_list, "milk", 1)
 :ok
-iex> KV.Bucket.get(bucket, "eggs")
-3
+iex> KV.Bucket.get(:another_list, "milk")
+1
 ```
 
-`DynamicSupervisor.start_child/2` expects the name of the supervisor and the child specification of the child to be started.
-
-The last step is to change the registry to use the dynamic supervisor:
+And it all works as expected. In fact, we can even give names to `DynamicSupervisor` themselves, instead of passing PIDs around and also use it to start buckets named using the registry:
 
 ```elixir
-  def handle_cast({:create, name}, {names, refs}) do
-    if Map.has_key?(names, name) do
-      {:noreply, {names, refs}}
-    else
-      {:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
-      ref = Process.monitor(pid)
-      refs = Map.put(refs, ref, name)
-      names = Map.put(names, name, pid)
-      {:noreply, {names, refs}}
-    end
-  end
+iex> DynamicSupervisor.start_link(strategy: :one_for_one, name: :dyn_sup)
+iex> name = {:via, Registry, {KV, "yet_another_list"}}
+iex> DynamicSupervisor.start_child(:dyn_sup, {KV.Bucket, name: name})
+iex> KV.Bucket.put(name, "milk", 1)
+:ok
+iex> KV.Bucket.get(name, "milk")
+1
 ```
 
-That's enough for our tests to pass but there is a resource leakage in our application. When a bucket terminates, the supervisor will start a new bucket in its place. After all, that's the role of the supervisor!
+Overall, processes can be named and supervised, regardless if they are supervisors, agents, etc, since all of Elixir standard library was designed around those capabilities.
 
-However, when the supervisor restarts the new bucket, the registry does not know about it. So we will have an empty bucket in the supervisor that nobody can access! To solve this, we want to say that buckets are actually temporary. If they crash, regardless of the reason, they should not be restarted.
-
-We can do this by passing the `restart: :temporary` option to `use Agent` in `KV.Bucket`:
+With all ingredients in place to supervise and name buckets, open up the `lib/kv.ex` module and let's add a new function called `KV.lookup_bucket/1`, which receives a name and either create or returns a bucket for the given name:
 
 ```elixir
-defmodule KV.Bucket do
-  use Agent, restart: :temporary
-```
+defmodule KV do
+  use Application
 
-Let's also add a test to `test/kv/bucket_test.exs` that guarantees the bucket is temporary:
+  @impl true
+  def start(_type, _args) do
+    children = [
+      {Registry, name: KV, keys: :unique},
+      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one}
+    ]
 
-```elixir
-  test "are temporary workers" do
-    assert Supervisor.child_spec(KV.Bucket, []).restart == :temporary
+    Supervisor.start_link(children, strategy: :one_for_one)
   end
-```
 
-Our test uses the `Supervisor.child_spec/2` function to retrieve the child specification out of a module and then assert its restart value is `:temporary`. At this point, you may be wondering why use a supervisor if it never restarts its children. It happens that supervisors provide more than restarts, they are also responsible for guaranteeing proper startup and shutdown, especially in case of crashes in a supervision tree.
-
-## Supervision trees
+  @doc """
+  Creates a bucket with the given name.
+  """
+  def create_bucket(name) do
+    DynamicSupervisor.start_child(KV.BucketSupervisor, {KV.Bucket, name: via(name)})
+  end
 
-When we added `KV.BucketSupervisor` as a child of `KV.Supervisor`, we began to have supervisors that supervise other supervisors, forming so-called "supervision trees".
+  @doc """
+  Looks up the given bucket.
+  """
+  def lookup_bucket(name) do
+    GenServer.whereis(via(name))
+  end
 
-Every time you add a new child to a supervisor, it is important to evaluate if the supervisor strategy is correct as well as the order of child processes. In this case, we are using `:one_for_one` and the `KV.Registry` is started before `KV.BucketSupervisor`.
+  defp via(name), do: {:via, Registry, {KV, name}}
+end
+```
 
-One flaw that shows up right away is the ordering issue. Since `KV.Registry` invokes `KV.BucketSupervisor`, then the `KV.BucketSupervisor` must be started before `KV.Registry`. Otherwise, it may happen that the registry attempts to reach the bucket supervisor before it has started.
+The code is relatively simple. First we changed `start/2` to also start a dynamic supervisor named `KV.BucketSupervisor`. Then, when implemented `KV.create_bucket/1` which receives a bucket and starts with using our registry and dynamic supervisor. And we also added `KV.lookup_bucket/1` that receives the same name and attempts to find its PID.
 
-The second flaw is related to the supervision strategy. If `KV.Registry` dies, all information linking `KV.Bucket` names to bucket processes is lost. Therefore the `KV.BucketSupervisor` and all children must terminate too - otherwise we will have orphan processes.
+To make sure it all works as expected, let's write a test. Open up `test/kv_test.exs` and add this:
 
-In light of this observation, we should consider moving to another supervision strategy. The two other candidates are `:one_for_all` and `:rest_for_one`. A supervisor using the `:rest_for_one` strategy will kill and restart child processes which were started *after* the crashed child. In this case, we would want `KV.BucketSupervisor` to terminate if `KV.Registry` terminates. This would require the bucket supervisor to be placed after the registry which violates the ordering constraints we have established two paragraphs above.
+```elixir
+defmodule KVTest do
+  use ExUnit.Case, async: true
 
-So our last option is to go all in and pick the `:one_for_all` strategy: the supervisor will kill and restart all of its children processes whenever any one of them dies. This is a completely reasonable approach for our application, since the registry can't work without the bucket supervisor, and the bucket supervisor should terminate without the registry. Let's reimplement `init/1` in `KV.Supervisor` to encode those properties:
+  test "creates and looks up buckets by any name" do
+    name = "a unique name that won't be shared"
+    assert is_nil(KV.lookup_bucket(name))
 
-```elixir
-  def init(:ok) do
-    children = [
-      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
-      {KV.Registry, name: KV.Registry}
-    ]
+    assert {:ok, bucket} = KV.create_bucket(name)
+    assert KV.lookup_bucket(name) == bucket
 
-    Supervisor.init(children, strategy: :one_for_all)
+    assert KV.create_bucket(name) == {:error, {:already_started, bucket}}
   end
+end
 ```
 
-There are two topics left before we move on to the next chapter.
+The test shows we are creating and locating buckets with any name, making sure we use a unique name to avoid conflicts between tests.
+
+## The `start_supervised` test helper
+
+Before we move on, let's do some clean up.
 
-## Shared state in tests
+In `test/kv/bucket_test.exs`, we explicitly invoked `KV.Bucket.start_link/1` to start our buckets. However, we now know that we should avoid calling `start_link/1` directly and instead start processes as part of supervision trees.
 
-So far we have been starting one registry per test to ensure they are isolated:
+In order to aid testing, `ExUnit` already starts a supervision tree per test and provides the `start_supervised` function to start processes within test-specific supervision tree. One advantage of this approach is that `ExUnit` guarantees any started process is shut down at the end of the test too. Let's rewrite our tests to use it instead:
 
 ```elixir
-setup do
-  registry = start_supervised!(KV.Registry)
-  %{registry: registry}
-end
-```
+defmodule KV.BucketTest do
+  use ExUnit.Case, async: true
+
+  test "stores values by key" do
+    {:ok, bucket} = start_supervised(KV.Bucket)
+    assert KV.Bucket.get(bucket, "milk") == nil
 
-Since we have changed our registry to use `KV.BucketSupervisor`, our tests are now relying on this shared supervisor even though each test has its own registry. The question is: should we?
+    KV.Bucket.put(bucket, "milk", 3)
+    assert KV.Bucket.get(bucket, "milk") == 3
+  end
+
+  test "stores values by key on a named process", config do
+    {:ok, _} = start_supervised({KV.Bucket, name: config.test})
+    assert KV.Bucket.get(config.test, "milk") == nil
 
-It depends. It is ok to rely on shared state as long as we depend only on a non-shared partition of this state. Although multiple registries may start buckets on the shared bucket supervisor, those buckets and registries are isolated from each other. We would only run into concurrency issues if we used a function like `DynamicSupervisor.count_children(KV.BucketSupervisor)` which would count all buckets from all registries, potentially giving different results when tests run concurrently.
+    KV.Bucket.put(config.test, "milk", 3)
+    assert KV.Bucket.get(config.test, "milk") == 3
+  end
+end
+```
 
-Since we have relied only on a non-shared partition of the bucket supervisor so far, we don't need to worry about concurrency issues in our test suite. In case it ever becomes a problem, we can start a supervisor per test and pass it as an argument to the registry `start_link` function.
+It is a small change, but our tests are now using all of the relevant best practices. Excellent!
 
 ## Observer
 
@@ -174,13 +205,10 @@ iex> :observer.start()
 > When running `iex` inside a project with `iex -S mix`, `observer` won't be available as a dependency. To do so, you will need to call the following functions before:
 >
 > ```elixir
-> iex> Mix.ensure_application!(:wx)             # Not necessary on Erlang/OTP 27+
-> iex> Mix.ensure_application!(:runtime_tools)  # Not necessary on Erlang/OTP 27+
-> iex> Mix.ensure_application!(:observer)
 > iex> :observer.start()
 > ```
 >
-> If any of the calls above fail, here is what may have happened: some package managers default to installing a minimized Erlang without WX bindings for GUI support. In some package managers, you may be able to replace the headless Erlang with a more complete package (look for packages named `erlang` vs `erlang-nox` on Debian/Ubuntu/Arch). In others managers, you may need to install a separate `erlang-wx` (or similarly named) package.
+> If the call above fails, here is what may have happened: some package managers default to installing a minimized Erlang without WX bindings for GUI support. In some package managers, you may be able to replace the headless Erlang with a more complete package (look for packages named `erlang` vs `erlang-nox` on Debian/Ubuntu/Arch). In others managers, you may need to install a separate `erlang-wx` (or similarly named) package.
 >
 > There are conversations to improve this experience in future releases.
 
@@ -193,12 +221,12 @@ In the Applications tab, you will see all applications currently running in your
 Not only that, as you create new buckets on the terminal, you should see new processes spawned in the supervision tree shown in Observer:
 
 ```elixir
-iex> KV.Registry.create(KV.Registry, "shopping")
-:ok
+iex> KV.lookup_bucket("shopping")
+#PID<0.89.0>
 ```
 
 We will leave it up to you to further explore what Observer provides. Note you can double-click any process in the supervision tree to retrieve more information about it, as well as right-click a process to send "a kill signal", a perfect way to emulate failures and see if your supervisor reacts as expected.
 
 At the end of the day, tools like Observer are one of the reasons you want to always start processes inside supervision trees, even if they are temporary, to ensure they are always reachable and introspectable.
 
-Now that our buckets are properly linked and supervised, let's see how we can speed things up.
+Now that our buckets are named and supervised, we are ready to start our server and start receiving requests.
diff --git a/lib/elixir/pages/mix-and-otp/genservers.md b/lib/elixir/pages/mix-and-otp/genservers.md
index 94e62ab34ab..30fcf2a7ce7 100644
--- a/lib/elixir/pages/mix-and-otp/genservers.md
+++ b/lib/elixir/pages/mix-and-otp/genservers.md
@@ -3,46 +3,70 @@
   SPDX-FileCopyrightText: 2021 The Elixir Team
 -->
 
-# Client-server communication with GenServer
+# Client-server with GenServer
 
-In the [previous chapter](agents.md), we used agents to represent our buckets. In the [introduction to mix](introduction-to-mix.md), we specified we would like to name each bucket so we can do the following:
+To wrap up our distributed key-value store, we will implement a feature where a client can subscribe to a bucket and receive realtime notifications of any modification happening in the bucket, regardless of where in the cluster the bucket is located.
 
-```elixir
-CREATE shopping
-OK
-
-PUT shopping milk 1
-OK
+We will do by adding a new command, called SUBSCRIBE, to be used like this:
 
-GET shopping milk
-1
-OK
+```text
+SUBSCRIBE shopping
+milk SET TO 1
+eggs SET TO 10
+milk DELETED
 ```
 
-In the session above we interacted with the "shopping" bucket.
+To make this work, we must change our `KV.Bucket` implementation to track subscriptions and emit broadcasts. However, as we will see, we cannot implement such on top of agents, and we will need to rewrite our bucket implementation to a `GenServer`.
+
+## Links and monitors
+
+Processes in Elixir are isolated. When they need to communicate, they do so by sending messages. However, how do you know when a process terminates, either because it has completed or due to a crash?
 
-Since agents are processes, each bucket has a process identifier (PID), but buckets do not have a name. Back [in the Process chapter](../getting-started/processes.md), we have learned that we can register processes in Elixir by giving them atom names:
+We have two options: links and monitors.
+
+We have used links extensively. Whenever we started a process, we typically did so by using `start_link` or similar. The idea behind links is that, if any of the processes crash, the other will crash due to the link. We talked about them in the [Process chapter of the Getting Started guide](../getting-started/processes.md). Here is a refresher:
 
 ```elixir
-iex> Agent.start_link(fn -> %{} end, name: :shopping)
-{:ok, #PID<0.43.0>}
-iex> KV.Bucket.put(:shopping, "milk", 1)
-:ok
-iex> KV.Bucket.get(:shopping, "milk")
-1
+iex> self()
+#PID<0.115.0>
+iex> spawn_link(fn -> :nothing_bad_will_happen end)
+#PID<0.116.0>
+iex> self()
+#PID<0.115.0>
 ```
 
-However, naming dynamic processes with atoms is a terrible idea! If we use atoms, we would need to convert the bucket name (often received from an external client) to atoms, and **we should never convert user input to atoms**. This is because atoms are not garbage collected. Once an atom is created, it is never reclaimed. Generating atoms from user input would mean the user can inject enough different names to exhaust our system memory!
+```elixir
+iex> spawn_link(fn -> raise "oops" end)
+#PID<0.117.0>
 
-In practice, it is more likely you will reach the Erlang VM limit for the maximum number of atoms before you run out of memory, which will bring your system down regardless.
+12:37:33.229 [error] Process #PID<0.117.0> raised an exception
+Interactive Elixir (1.18.4) - press Ctrl+C to exit (type h() ENTER for help)
+iex> self()
+#PID<0.118.0>
+```
 
-Instead of abusing the built-in name facility, we will create our own *process registry* that associates the bucket name to the bucket process.
+The reason why we links are so pervasive is because when we start a process inside a supervisor, we want our process to crash if the supervisor terminates. On the other hand, we don't want the supervisor to crash when a child terminates, and therefore supervisors trap exits from links by calling `Process.flag(:trap_exit, true)`.
 
-The registry needs to guarantee that it is always up to date. For example, if one of the bucket processes crashes due to a bug, the registry must notice this change and avoid serving stale entries. In Elixir, we say the registry needs to *monitor* each bucket. Because our *registry* needs to be able to receive and handle ad-hoc messages from the system, the `Agent` API is not enough.
+In other words, links create an intrinsic relationship between the processes. If we simply want to track when a process dies, without tying their exit signals to each other, a better solution is to use monitors. When a monitored process terminates, we receive a message in our inbox, regardless of the reason:
 
-We will use a `GenServer` to create a registry process that can monitor the bucket processes. GenServer provides industrial strength functionality for building servers in both Elixir and OTP.
+```elixir
+iex> pid = spawn(fn -> Process.sleep(5000) end)
+#PID<0.119.0>
+iex> Process.monitor(pid)
+#Reference<0.1076459149.2159017989.118674>
+iex> flush()
+:ok
+# Wait five seconds
+iex> flush()
+{:DOWN, #Reference<0.1076459149.2159017989.118674>, :process, #PID<0.119.0>, :normal}
+:ok
+```
+
+Once the process terminates, we receive a "DOWN message", represented in a five-element tuple. The last element is the reason why it crashed (`:normal` means it terminated successfully).
 
-Please read the `GenServer` module documentation for an overview if you haven't yet. Once you do so, we are ready to proceed.
+Monitors will play a very important role in our subscribe feature. When a client subscribes to a bucket, the bucket will store the client PID and send messages to it on every change. However, if the client terminates (for example because it was disconnected), the bucket must remove the client from its list of subscribers (otherwise the list would keep on growing forever as clients connect and disconnect).
+
+We chose the `Agent` module to implement our `KV.Bucket` and, unfortunately, agents cannot receive messages. So the first step is to rewrite our `KV.Bucket` to a `GenServer`. The `GenServer` module documentation has a good overview on what they are and how to implement them. Give it a read and then we are ready to proceed.
 
 ## GenServer callbacks
 
@@ -84,234 +108,255 @@ def handle_call({:put, key, value}, _from, state) do
 end
 ```
 
-There is quite a bit more ceremony in the GenServer code but, as we will see, it brings some benefits too.
-
-For now, we will write only the server callbacks for our bucket registering logic, without providing a proper API, which we will do later.
-
-Create a new file at `lib/kv/registry.ex` with the following contents:
+Let's go ahead and rewrite `KV.Bucket` at once. Open up `lib/kv/bucket.ex` and replace its contents with this new version:
 
 ```elixir
-defmodule KV.Registry do
+defmodule KV.Bucket do
   use GenServer
 
-  ## Missing Client API - will add this later
+  @doc """
+  Starts a new bucket.
+  """
+  def start_link(opts) do
+    GenServer.start_link(__MODULE__, %{}, opts)
+  end
 
-  ## Defining GenServer Callbacks
+  @doc """
+  Gets a value from the `bucket` by `key`.
+  """
+  def get(bucket, key) do
+    GenServer.call(bucket, {:get, key})
+  end
 
-  @impl true
-  def init(:ok) do
-    {:ok, %{}}
+  @doc """
+  Puts the `value` for the given `key` in the `bucket`.
+  """
+  def put(bucket, key, value) do
+    GenServer.call(bucket, {:put, key, value})
+  end
+
+  @doc """
+  Deletes `key` from `bucket`.
+
+  Returns the current value of `key`, if `key` exists.
+  """
+  def delete(bucket, key) do
+    GenServer.call(bucket, {:delete, key})
   end
 
+  ### Callbacks
+
   @impl true
-  def handle_call({:lookup, name}, _from, names) do
-    {:reply, Map.fetch(names, name), names}
+  def init(bucket) do
+    state = %{
+      bucket: bucket
+    }
+
+    {:ok, state}
   end
 
   @impl true
-  def handle_cast({:create, name}, names) do
-    if Map.has_key?(names, name) do
-      {:noreply, names}
-    else
-      {:ok, bucket} = KV.Bucket.start_link([])
-      {:noreply, Map.put(names, name, bucket)}
-    end
+  def handle_call({:get, key}, _from, state) do
+    value = get_in(state.bucket[key])
+    {:reply, value, state}
+  end
+
+  def handle_call({:put, key, value}, _from, state) do
+    state = put_in(state.bucket[key], value)
+    {:reply, :ok, state}
+  end
+
+  def handle_call({:delete, key}, _from, state) do
+    {value, state} = pop_in(state.bucket[key])
+    {:reply, value, state}
   end
 end
 ```
 
-There are two types of requests you can send to a GenServer: calls and casts. Calls are synchronous and the server **must** send a response back to such requests. While the server computes the response, the client is **waiting**. Casts are asynchronous: the server won't send a response back and therefore the client won't wait for one. Both requests are messages sent to the server, and will be handled in sequence. In the above implementation, we pattern-match on the `:create` messages, to be handled as cast, and on the `:lookup` messages, to be handled as call.
+The first function is `start_link/1`, which starts a new GenServer passing a list of options. `GenServer.start_link/3`, which takes three arguments:
 
-In order to invoke the callbacks above, we need to go through the corresponding `GenServer` functions. Let's start a registry, create a named bucket, and then look it up:
+1. The module where the server callbacks are implemented, in this case `__MODULE__` (meaning the current module)
 
-```elixir
-iex> {:ok, registry} = GenServer.start_link(KV.Registry, :ok)
-{:ok, #PID<0.136.0>}
-iex> GenServer.cast(registry, {:create, "shopping"})
-:ok
-iex> {:ok, bucket} = GenServer.call(registry, {:lookup, "shopping"})
-{:ok, #PID<0.174.0>}
-```
+2. The initialization arguments, in this case the empty bucket `%{}`
 
-Our `KV.Registry` process received a cast with `{:create, "shopping"}` and a call with `{:lookup, "shopping"}`, in this sequence. `GenServer.cast` will immediately return, as soon as the message is sent to the `registry`. The `GenServer.call` on the other hand, is where we would be waiting for an answer, provided by the above `KV.Registry.handle_call` callback.
+3. A list of options which can be used to specify things like the name of the server. Once again, we forward the list of options that we receive on `start_link/1` to `GenServer.start_link/3`, as we did for agents
 
-You may also have noticed that we have added `@impl true` before each callback. The `@impl true` informs the compiler that our intention for the subsequent function definition is to define a callback. If by any chance we make a mistake in the function name or in the number of arguments, like we define a `handle_call/2`, the compiler would warn us there isn't any `handle_call/2` to define, and would give us the complete list of known callbacks for the `GenServer` module.
+Once started, the GenServer will invoke the `init/1` callback, that receives the second argument given to `GenServer.start_link/3` and returns `{:ok, state}`, where state is a new map. We can already notice how the `GenServer` API makes the client/server segregation more apparent. `start_link/3` happens in the client, while `init/1` is the respective callback that runs on the server.
 
-This is all good and well, but we still want to offer our users an API that allows us to hide our implementation details.
+There are two types of requests you can send to a GenServer: calls and casts. Calls are synchronous and the server **must** send a response back to such requests. While the server computes the response, the client is **waiting**. Casts are asynchronous: the server won't send a response back and therefore the client won't wait for one. Both requests are messages sent to the server, and will be handled in sequence. So far we have only used `GenServer.call/2`, to keep the same semantics as the Agent, but we will give `cast` a try when implementing subscriptions. Given we kept the same behaviour, all tests will still pass.
 
-## The Client API
+Each request must be implemented as a specific callback. For `call/2` requests, we implement a `handle_call/3` callback that receives the `request`, the process from which we received the request (`_from`), and the current server state (`state`). The `handle_call/3` callback returns a tuple in the format `{:reply, reply, updated_state}`. The first element of the tuple, `:reply`, indicates that the server should send a reply back to the client. The second element, `reply`, is what will be sent to the client while the third, `updated_state` is the new server state.
 
-A GenServer is implemented in two parts: the client API and the server callbacks. You can either combine both parts into a single module or you can separate them into a client module and a server module. The client is any process that invokes the client function. The server is always the process identifier or process name that we will explicitly pass as argument to the client API. Here we'll use a single module for both the server callbacks and the client API.
+Another Elixir feature we used in the implementation above are the nested traversal functions: `get_in/1`, `put_in/2`, and `pop_in/1`. Instead of keeping the `bucket` as our GenServer state, we defined a state map with a `bucket` key inside. This will be important as we also need to track subscribers as part of the GenServer state. These new functions make it straight-forward to manipulate data structures nested in other data structures.
 
-Edit the file at `lib/kv/registry.ex`, filling in the blanks for the client API:
+With our GenServer in place, let's work on subscription, starting with the tests.
 
-```elixir
-  ## Client API
+## Implementing subscriptions
 
-  @doc """
-  Starts the registry.
-  """
-  def start_link(opts) do
-    GenServer.start_link(__MODULE__, :ok, opts)
-  end
+Our new test will subscribe to a bucket and then assert that, as operations are performed against the bucket, we receive messages of said events.
 
-  @doc """
-  Looks up the bucket pid for `name` stored in `server`.
+Open up `test/kv/bucket_test.exs` and key this in:
 
-  Returns `{:ok, pid}` if the bucket exists, `:error` otherwise.
-  """
-  def lookup(server, name) do
-    GenServer.call(server, {:lookup, name})
-  end
+```elixir
+  test "subscribes to puts and deletes" do
+    {:ok, bucket} = start_supervised(KV.Bucket)
+    KV.Bucket.subscribe(bucket)
 
-  @doc """
-  Ensures there is a bucket associated with the given `name` in `server`.
-  """
-  def create(server, name) do
-    GenServer.cast(server, {:create, name})
+    KV.Bucket.put(bucket, "milk", 3)
+    assert_receive {:put, "milk", 3}
+
+    # Also check it works even from another process
+    spawn(fn -> KV.Bucket.delete(bucket, "milk") end)
+    assert_receive {:delete, "milk"}
   end
 ```
 
-The first function is `start_link/1`, which starts a new GenServer passing a list of options. `start_link/1` calls out to `GenServer.start_link/3`, which takes three arguments:
-
-1. The module where the server callbacks are implemented, in this case `__MODULE__` (meaning the current module)
-
-2. The initialization arguments, in this case the atom `:ok`
+In order to make the test pass, we need to implement the `KV.Bucket.subscribe/1`. So let's add these three new functions to `KV.Bucket`:
 
-3. A list of options which can be used to specify things like the name of the server. For now, we forward the list of options that we receive on `start_link/1` to `GenServer.start_link/3`
-
-The next two functions, `lookup/2` and `create/2`, are responsible for sending these requests to the server.  In this case, we have used `{:lookup, name}` and `{:create, name}` respectively.  Requests are often specified as tuples, like this, in order to provide more than one "argument" in that first argument slot. It's common to specify the action being requested as the first element of a tuple, and arguments for that action in the remaining elements. Note that the requests must match the first argument to `handle_call/3` or `handle_cast/2`.
+```elixir
+  @doc """
+  Subscribes the current process to the bucket.
+  """
+  def subscribe(bucket) do
+    GenServer.cast(bucket, {:subscribe, self()})
+  end
 
-That's it for the client API. On the server side, we can implement a variety of callbacks to guarantee the server initialization, termination, and handling of requests. Those callbacks are optional and for now, we have only implemented the ones we care about. Let's recap.
+  @impl true
+  def handle_cast({:subscribe, pid}, state) do
+    Process.monitor(pid)
+    state = update_in(state.subscribers, &MapSet.put(&1, pid))
+    {:noreply, state}
+  end
 
-The first is the `init/1` callback, that receives the second argument given to `GenServer.start_link/3` and returns `{:ok, state}`, where state is a new map. We can already notice how the `GenServer` API makes the client/server segregation more apparent. `start_link/3` happens in the client, while `init/1` is the respective callback that runs on the server.
+  @impl true
+  def handle_info({:DOWN, _ref, _type, pid, _reason}, state) do
+    state = update_in(state.subscribers, &MapSet.delete(&1, pid))
+    {:noreply, state}
+  end
+```
 
-For `call/2` requests, we implement a `handle_call/3` callback that receives the `request`, the process from which we received the request (`_from`), and the current server state (`names`). The `handle_call/3` callback returns a tuple in the format `{:reply, reply, new_state}`. The first element of the tuple, `:reply`, indicates that the server should send a reply back to the client. The second element, `reply`, is what will be sent to the client while the third, `new_state` is the new server state.
+On subscription, we send a `cast/2` request with the current process identifier and implement its `handle_cast/2` callback that receives the `request` and the current server state. We then proceed to monitor the given `pid` and add it to the list of subscribers, which we are implementing using `MapSet`. The `handle_cast/2` callback returns a tuple in the format `{:noreply, updated_state}`. Note that in a real application we would have probably implemented it with a synchronous call, as it provides back pressure, instead of an asynchronous cast. We are doing it this way to illustrate how to implement a cast callback.
 
-For `cast/2` requests, we implement a `handle_cast/2` callback that receives the `request` and the current server state (`names`). The `handle_cast/2` callback returns a tuple in the format `{:noreply, new_state}`. Note that in a real application we would have probably implemented the callback for `:create` with a synchronous call instead of an asynchronous cast. We are doing it this way to illustrate how to implement a cast callback.
+Then, because we have monitored a process, once that process terminates, we will receive a "DOWN message". GenServers handle regular messages using the `handle_info/2` callback, which also typically return `{:noreply, updated_state}`. In this callback, we remove the PID that terminated from our list of subscribers.
 
-There are other tuple formats both `handle_call/3` and `handle_cast/2` callbacks may return. There are other callbacks like `terminate/2` and `code_change/3` that we could implement. You are welcome to explore the full `GenServer` documentation to learn more about those.
+We are almost there. We can see both `handle_cast/2` and `handle_info/2` callbacks assume there is a subscribers key in our state with a `MapSet`. So let's add it by updating the existing `init/1` to the following:
 
-For now, let's write some tests to guarantee our GenServer works as expected.
+```elixir
+  @impl true
+  def init(bucket) do
+    state = %{
+      bucket: bucket,
+      subscribers: MapSet.new()
+    }
 
-## Testing a GenServer
+    {:ok, state}
+  end
+```
 
-Testing a GenServer is not much different from testing an agent. We will spawn the server on a setup callback and use it throughout our tests. Create a file at `test/kv/registry_test.exs` with the following:
+And finally let's update the callbacks for `put/3` and `delete/2` to broadcast messages whenever they are invoked, like this:
 
 ```elixir
-defmodule KV.RegistryTest do
-  use ExUnit.Case, async: true
-
-  setup do
-    registry = start_supervised!(KV.Registry)
-    %{registry: registry}
+  def handle_call({:put, key, value}, _from, state) do
+    state = put_in(state.bucket[key], value)
+    broadcast(state, {:put, key, value})
+    {:reply, :ok, state}
   end
 
-  test "spawns buckets", %{registry: registry} do
-    assert KV.Registry.lookup(registry, "shopping") == :error
-
-    KV.Registry.create(registry, "shopping")
-    assert {:ok, bucket} = KV.Registry.lookup(registry, "shopping")
+  def handle_call({:delete, key}, _from, state) do
+    {value, state} = pop_in(state.bucket[key])
+    broadcast(state, {:delete, key})
+    {:reply, value, state}
+  end
 
-    KV.Bucket.put(bucket, "milk", 1)
-    assert KV.Bucket.get(bucket, "milk") == 1
+  defp broadcast(state, message) do
+    for pid <- state.subscribers do
+      send(pid, message)
+    end
   end
-end
 ```
 
-Our test case first asserts there are no buckets in our registry, creates a named bucket, looks it up, and asserts it behaves as a bucket.
-
-There is one important difference between the `setup` block we wrote for `KV.Registry` and the one we wrote for `KV.Bucket`. Instead of starting the registry by hand by calling `KV.Registry.start_link/1`, we instead called the `ExUnit.Callbacks.start_supervised!/2` function, passing the `KV.Registry` module.
+There is no need to modify the callback for `get/2`. And that's it, run the tests again, and our new test should pass!
 
-The `start_supervised!` function was injected into our test module by `use ExUnit.Case`. It does the job of starting the `KV.Registry` process, by calling its `start_link/1` function. The advantage of using `start_supervised!` is that ExUnit will guarantee that the registry process will be shutdown **before** the next test starts. In other words, it helps guarantee that the state of one test is not going to interfere with the next one in case they depend on shared resources.
+## Wiring it all up
 
-When starting processes during your tests, we should always prefer to use `start_supervised!`. We recommend you to change the `setup` block in `bucket_test.exs` to use `start_supervised!` too.
+Now that our bucket deals with subscriptions, we need to expose this new functionality in our server. Let's once again start with the test.
 
-Run the tests and they should all pass!
+Open up `test/kv/server_test.exs` and add this new test:
 
-## The need for monitoring
+```elixir
+  test "subscribes to buckets", %{socket: socket, name: name} do
+    assert send_and_recv(socket, "CREATE #{name}\r\n") == "OK\r\n"
+    :gen_tcp.send(socket, "SUBSCRIBE #{name}\r\n")
 
-Everything we have done so far could have been implemented with a `Agent`. In this section, we will see one of many things that we can achieve with a GenServer that is not possible with an Agent.
+    {:ok, other} = :gen_tcp.connect(~c"localhost", 4040, @socket_options)
 
-Let's start with a test that describes how we want the registry to behave if a bucket stops or crashes:
+    assert send_and_recv(other, "PUT #{name} milk 3\r\n") == "OK\r\n"
+    assert :gen_tcp.recv(socket, 0, 1000) == {:ok, "milk SET TO 3\r\n"}
 
-```elixir
-test "removes buckets on exit", %{registry: registry} do
-  KV.Registry.create(registry, "shopping")
-  {:ok, bucket} = KV.Registry.lookup(registry, "shopping")
-  Agent.stop(bucket)
-  assert KV.Registry.lookup(registry, "shopping") == :error
-end
+    assert send_and_recv(other, "DELETE #{name} milk\r\n") == "OK\r\n"
+    assert :gen_tcp.recv(socket, 0, 1000) == {:ok, "milk DELETED\r\n"}
+  end
 ```
 
-The test above will fail on the last assertion as the bucket name remains in the registry even after we stop the bucket process.
+The test creates a bucket and subscribes to it. Then it opens up another TCP connection to send commands. For each command sent, we expect the subscribed socket to receive a message.
 
-In order to fix this bug, we need the registry to monitor every bucket it spawns. Once we set up a monitor, the registry will receive a notification every time a bucket process exits, allowing us to clean the registry up.
-
-Let's first play with monitors by starting a new console with `iex -S mix`:
+To make the test pass, we need to change `KV.Command` to parse the new `SUBSCRIBE` command and then run it. Open up `lib/kv/commands.ex` and then first change the `parse/1` definition to the following:
 
 ```elixir
-iex> {:ok, pid} = KV.Bucket.start_link([])
-{:ok, #PID<0.66.0>}
-iex> Process.monitor(pid)
-#Reference<0.0.0.551>
-iex> Agent.stop(pid)
-:ok
-iex> flush()
-{:DOWN, #Reference<0.0.0.551>, :process, #PID<0.66.0>, :normal}
+  def parse(line) do
+    case String.split(line) do
+      ["SUBSCRIBE", bucket] -> {:ok, {:subscribe, bucket}}
+      ["CREATE", bucket] -> {:ok, {:create, bucket}}
+      ["GET", bucket, key] -> {:ok, {:get, bucket, key}}
+      ["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
+      ["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
+      _ -> {:error, :unknown_command}
+    end
+  end
 ```
 
-Note `Process.monitor(pid)` returns a unique reference that allows us to match upcoming messages to that monitoring reference. After we stop the agent, we can `flush/0` all messages and notice a `:DOWN` message arrived, with the exact reference returned by `monitor`, notifying that the bucket process exited with reason `:normal`.
-
-Let's reimplement the server callbacks to fix the bug and make the test pass. First, we will modify the GenServer state to two maps: one that contains `name -> pid` and another that holds `ref -> name`. Then we need to monitor the buckets on `handle_cast/2` as well as implement a `handle_info/2` callback to handle the monitoring messages. The full server callbacks implementation is shown below:
+We added a new clause that converts "SUBSCRIBE" into a tuple. Now we need to match on this tuple within `run/1`. We can do so by adding a new clause at the bottom of `run/1`, with the following code:
 
 ```elixir
-## Server callbacks
+  def run({:subscribe, bucket}, socket) do
+    lookup(bucket, fn pid ->
+      KV.Bucket.subscribe(pid)
+      :inet.setopts(socket, active: true)
+      receive_messages(socket)
+    end)
+  end
 
-@impl true
-def init(:ok) do
-  names = %{}
-  refs = %{}
-  {:ok, {names, refs}}
-end
+  defp receive_messages(socket) do
+    receive do
+      {:put, key, value} ->
+        :gen_tcp.send(socket, "#{key} SET TO #{value}\r\n")
+        receive_messages(socket)
 
-@impl true
-def handle_call({:lookup, name}, _from, state) do
-  {names, _} = state
-  {:reply, Map.fetch(names, name), state}
-end
+      {:delete, key} ->
+        :gen_tcp.send(socket, "#{key} DELETED\r\n")
+        receive_messages(socket)
+
+      {:tcp_closed, ^socket} ->
+        {:error, :closed}
 
-@impl true
-def handle_cast({:create, name}, {names, refs}) do
-  if Map.has_key?(names, name) do
-    {:noreply, {names, refs}}
-  else
-    {:ok, bucket} = KV.Bucket.start_link([])
-    ref = Process.monitor(bucket)
-    refs = Map.put(refs, ref, name)
-    names = Map.put(names, name, bucket)
-    {:noreply, {names, refs}}
+      # If we receive any message, including socket writes, we discard them
+      _ ->
+        receive_messages(socket)
+    end
   end
-end
+```
 
-@impl true
-def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
-  {name, refs} = Map.pop(refs, ref)
-  names = Map.delete(names, name)
-  {:noreply, {names, refs}}
-end
+Let's go over it by parts. We use the existing `lookup/2` private function to lookup for a bucket. If one is found, we subscribe the current process to the bucket. Then we call `:inet.setopts(socket, active: true)` (which we will explain soon) and `receive_messages/1`.
 
-@impl true
-def handle_info(msg, state) do
-  require Logger
-  Logger.debug("Unexpected message in KV.Registry: #{inspect(msg)}")
-  {:noreply, state}
-end
-```
+`receive_messages/1` awaits for messages from the bucket and then calls itself again, becoming a loop. We match on `{:put, key, value}` and `{:delete, key}` and write to those events to the socket. We also match on `{:tcp_closed, ^socket}`, which is a message that will be delivered if the TCP socket closes, and use it to abort the loop. We discard any other message.
+
+At this point you may be wondering: where does `{:tcp_closed, ^socket}` come from?
+
+So far, when receiving messages from the socket, we used `:gen_tcp.recv/3` to perform calls that will block the current process until content is available. This is known as "passive mode". However, we can also ask `:gen_tcp` to stream messages to the current process inbox as they arrive, which is known as "active mode", which is exactly what we configured when we called `:inet.setopts(socket, active: true)`. Those messages have the shape `{:tcp, socket, data}`. When the socket is in active mode and it is closed, it delivers a `{:tcp_closed, socket}` message. Once we receive this message, we exit the loop, which will exit the connection process. Since the bucket is monitoring the process, it will automatically remove the subscription too. You could verify this in practice by adding a `COUNT SUBSCRIPTIONS` command that returns the number of subscribers for a given bucket.
 
-Observe that we were able to considerably change the server implementation without changing any of the client API. That's one of the benefits of explicitly segregating the server and the client.
+In practice, many systems would prefer to call `:inet.setopts(socket, active: :once)` to specify only a single TCP message should be delivered to avoid overflowing message queues. Once the message is received, they call `:inet.setopts/2` again. In our case, we are simply discarding anything that arrives over the socket, so setting `active: true` is equally fine. In all scenarios, the benefit of using active mode is that the process can receive TCP messages as well as messages from other processes at the same time, instead of blocking on `:gen_tcp.recv/3`.
 
-Finally, different from the other callbacks, we have defined a "catch-all" clause for `handle_info/2` that discards and logs any unknown message. To understand why, let's move on to the next section.
+To wrap it all up, you should give our new feature a try in a distributed setting too. Start two `NODES=... PORT=... iex --sname ... -S mix` instances. In one of them, create a bucket. In the other, subscribe to the same bucket. Once you go back to the first shell, you will see that, even as you send commands to the bucket in one machine, the messages will be streamed to the other one. In other words, our subscription system is also distributed, and all we had to do is to send messages!
 
 ## `call`, `cast` or `info`?
 
@@ -319,25 +364,20 @@ So far we have used three callbacks: `handle_call/3`, `handle_cast/2` and `handl
 
 1. `handle_call/3` must be used for synchronous requests. This should be the default choice as waiting for the server reply is a useful back-pressure mechanism.
 
-2. `handle_cast/2` must be used for asynchronous requests, when you don't care about a reply. A cast does not guarantee the server has received the message and, for this reason, should be used sparingly. For example, the `create/2` function we have defined in this chapter should have used `call/2`. We have used `cast/2` for didactic purposes.
+2. `handle_cast/2` must be used for asynchronous requests, when you don't care about a reply. A cast does not guarantee the server has received the message and, for this reason, should be used sparingly. For example, the `subscribe/1` function we have defined in this chapter should have used `call/2`. We have used `cast/2` for educational purposes.
 
 3. `handle_info/2` must be used for all other messages a server may receive that are not sent via `GenServer.call/2` or `GenServer.cast/2`, including regular messages sent with `send/2`. The monitoring `:DOWN` messages are an example of this.
 
-Since any message, including the ones sent via `send/2`, go to `handle_info/2`, there is a chance that unexpected messages will arrive to the server. Therefore, if we don't define the catch-all clause, those messages could cause our registry to crash, because no clause would match. We don't need to worry about such cases for `handle_call/3` and `handle_cast/2` though. Calls and casts are only done via the `GenServer` API, so an unknown message is quite likely a developer mistake.
-
 To help developers remember the differences between call, cast and info, the supported return values and more, we have a tiny [GenServer cheat sheet](https://elixir-lang.org/downloads/cheatsheets/gen-server.pdf).
 
-## Monitors or links?
+## Agents or GenServers?
 
-We have previously learned about links in the [Process chapter](../getting-started/processes.md). Now, with the registry complete, you may be wondering: when should we use monitors and when should we use links?
+Before moving forward to the last chapter, you may be wondering: in the future, should you use an `Agent` or a `GenServer`?
 
-Links are bi-directional. If you link two processes and one of them crashes, the other side will crash too (unless it is trapping exits). A monitor is uni-directional: only the monitoring process will receive notifications about the monitored one. In other words: use links when you want linked crashes, and monitors when you just want to be informed of crashes, exits, and so on.
+As we saw throughout this guide, agents are straight-forward to get started but they are limited in what they can do. Agents are effectively a subset of GenServers. In fact, agents are implemented on top of GenServers. As well as supervisors, the `Registry` module, and many other features you will find in both Erlang and Elixir.
 
-Returning to our `handle_cast/2` implementation, you can see the registry is both linking and monitoring the buckets:
+In other words, GenServers are the most essential component for building concurrent and fault-tolerant systems in Elixir. They provide a robust and flexible framework for managing state and coordinating interactions between processes.
 
-```elixir
-{:ok, bucket} = KV.Bucket.start_link([])
-ref = Process.monitor(bucket)
-```
+For those reasons, many adopt a rule of thumb to never use Agents and jump straight into GenServers instead. On the other hand, others are more than fine with using agents to store a bit of state here and there. Either way, you will be fine!
 
-This is a bad idea, as we don't want the registry to crash when a bucket crashes. The proper fix is to actually not link the bucket to the registry. Instead, we will link each bucket to a special type of process called Supervisors, which are explicitly designed to handle failures and crashes. We will learn more about them in the next chapter.
+This is the last feature we have implemented for our distributed key-value store. In the next chapter, we will learn how to package our application before shipping it to production.
diff --git a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md
index 84888ce85a7..80b2bdd2688 100644
--- a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md
+++ b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md
@@ -9,8 +9,8 @@ In this guide, we will build a complete Elixir application, with its own supervi
 
 The requirements for this guide are (see `elixir -v`):
 
-  * Elixir 1.15.0 onwards
-  * Erlang/OTP 24 onwards
+  * Elixir 1.18.0 onwards
+  * Erlang/OTP 27 onwards
 
 The application works as a distributed key-value store. We are going to organize key-value pairs into buckets and distribute those buckets across multiple nodes. We will also build a simple client that allows us to connect to any of those nodes and send requests such as:
 
@@ -44,7 +44,7 @@ In this chapter, we will create our first project using Mix and explore differen
 
 > #### Source code {: .info}
 >
-> The final code for the application built in this guide is in [this repository](https://github.com/josevalim/kv_umbrella) and can be used as a reference.
+> The final code for the application built in this guide is in [this repository](https://github.com/josevalim/kv) and can be used as a reference.
 
 > #### Is this guide required reading? {: .info}
 >
@@ -82,7 +82,7 @@ Let's take a brief look at those generated files.
 
 > #### Executables in the `PATH` {: .info}
 >
-> Mix is an Elixir executable. This means that in order to run `mix`, you need to have both `mix` and `elixir` executables in your PATH. That's what happens when you install Elixir.
+> Mix is an Elixir executable. This means that in order to run `mix`, you need to have both `mix` and `elixir` executables in your [`PATH`](https://en.wikipedia.org/wiki/PATH_(variable)). That's what happens when you install Elixir.
 
 ## Project compilation
 
diff --git a/lib/elixir/pages/mix-and-otp/releases.md b/lib/elixir/pages/mix-and-otp/releases.md
new file mode 100644
index 00000000000..526d1bbb35d
--- /dev/null
+++ b/lib/elixir/pages/mix-and-otp/releases.md
@@ -0,0 +1,170 @@
+<!--
+  SPDX-License-Identifier: Apache-2.0
+  SPDX-FileCopyrightText: 2021 The Elixir Team
+-->
+
+# Releases
+
+Now that our application is ready, you may be wondering how we can package our application to run in production. After all, all of our code so far depends on Erlang and Elixir versions that are installed in your current system. To achieve this goal, Elixir provides releases.
+
+A release is a self-contained directory that consists of your application code, all of its dependencies, plus the whole Erlang Virtual Machine (VM) and runtime. Once a release is assembled, it can be packaged and deployed to a target as long as the target runs on the same operating system (OS) distribution and version as the machine that assembled the release.
+
+To get started, simply run `mix release` while setting `MIX_ENV=prod`:
+
+```console
+$ MIX_ENV=prod mix release
+Compiling 4 files (.ex)
+Generated kv app
+* assembling kv-0.1.0 on MIX_ENV=prod
+* using config/runtime.exs to configure the release at runtime
+
+Release created at _build/prod/rel/kv
+
+    # To start your system
+    _build/prod/rel/kv/bin/kv start
+
+Once the release is running:
+
+    # To connect to it remotely
+    _build/prod/rel/kv/bin/kv remote
+
+    # To stop it gracefully (you may also send SIGINT/SIGTERM)
+    _build/prod/rel/kv/bin/kv stop
+
+To list all commands:
+
+    _build/prod/rel/kv/bin/kv
+```
+
+Excellent! A release was assembled in `_build/prod/rel/kv`. Everything you need to run your application is inside that directory. In particular, there is a `bin/kv` file which is the entry point to your system. It supports multiple commands, such as:
+
+  * `bin/kv start`, `bin/kv start_iex`, `bin/kv restart`, and `bin/kv stop` — for general management of the release
+
+  * `bin/kv rpc COMMAND` and `bin/kv remote` — for running commands on the running system or to connect to the running system
+
+  * `bin/kv eval COMMAND` — to start a fresh system that runs a single command and then shuts down
+
+  * `bin/kv daemon` and `bin/kv daemon_iex` — to start the system as a daemon on Unix-like systems
+
+  * `bin/kv install` — to install the system as a service on Windows machines
+
+If you run `bin/kv start_iex` inside the release directory, it will start the system using a short name (`--sname`) equal to the release name, which in this case is `kv`. The next step is to start two instances, on different ports and different names, as we did earlier on. But before we do this, let's talk a bit about the benefits of releases.
+
+## Why releases?
+
+Releases allow developers to precompile and package all of their code and the runtime into a single unit. The benefits of releases are:
+
+  * Code preloading. The VM has two mechanisms for loading code: interactive and embedded. By default, it runs in the interactive mode which dynamically loads modules when they are used for the first time. The first time your application calls `Enum.map/2`, the VM will find the `Enum` module and load it. There's a downside. When you start a new server in production, it may need to load many other modules, causing the first requests to have an unusual spike in response time. Releases run in embedded mode, which loads all available modules upfront, guaranteeing your system is ready to handle requests after booting.
+
+  * Configuration and customization. Releases give developers fine grained control over system configuration and the VM flags used to start the system.
+
+  * Self-contained. A release does not require the source code to be included in your production artifacts. All of the code is precompiled and packaged. Releases do not even require Erlang or Elixir on your servers, as they include the Erlang VM and its runtime by default. Furthermore, both Erlang and Elixir standard libraries are stripped to bring only the parts you are actually using.
+
+  * Multiple releases. You can assemble different releases with different configuration per application or even with different applications altogether.
+
+We have written extensive documentation on releases, so [please check the official documentation for more information](`mix release`). For now, we will continue exploring some of the features outlined above.
+
+## Configuring releases
+
+Releases also provide built-in hooks for configuring almost every need of the production system:
+
+  * `config/config.exs` — provides build-time application configuration, which is executed before our application compiles. This file often imports configuration files based on the environment, such as `config/dev.exs` and `config/prod.exs`.
+
+  * `config/runtime.exs` — provides runtime application configuration. It is executed every time the release boots and is further extensible via config providers.
+
+  * `rel/env.sh.eex` and `rel/env.bat.eex` — template files that are copied into every release and executed on every command to set up environment variables, including ones specific to the VM, and the general environment.
+
+  * `rel/vm.args.eex` — a template file that is copied into every release and provides static configuration of the Erlang Virtual Machine and other runtime flags.
+
+In this case, we already have specified a `config/runtime.exs` that deals with both `PORT` and `NODES` environment variables. Furthermore, while releases don't accept a `--sname` parameter, they do allow us to set the name via the `RELEASE_NODE` env var. Therefore, we can start two copies of the system by jumping into `_build/prod/rel/kv` and typing this (remember to adjust `@computer-name` to your actual computer name):
+
+```console
+$ NODES="foo@computer-name,bar@computer-name" PORT=4040 RELEASE_NODE="foo" bin/kv start_iex
+```
+
+```console
+$ NODES="foo@computer-name,bar@computer-name" PORT=4041 RELEASE_NODE="bar" bin/kv start_iex
+```
+
+To verify it all worked out, you can type `Node.list` in the IEx section and see if it returns the other node. If it doesn't, you can start diagnosing, first by comparing the node names within each `iex>` prompt and calling `Node.connect/1` directly. With applications running, you can `telnet` into them as usual too.
+
+While the above is enough to get started, you may want to perform advanced configuration based on the environment you are replying to. Releases provide scripts for that, which are great to automate based on host, network, or cloud settings.
+
+## Operating System scripts
+
+Every release contains an environment file, named `env.sh` on Unix-like systems and `env.bat` on Windows machines, that executes before the Elixir system starts. In this file, you can execute any OS-level code, such as invoke other applications, set environment variables and so on. Some of those environment variables can even configure how the release itself runs.
+
+For instance, releases run using short-names (`--sname`). However, if you want to actually run a distributed key-value store in production, you will need multiple nodes and start the release with the `--name` option. We can achieve this by setting the `RELEASE_DISTRIBUTION` environment variable inside the `env.sh` and `env.bat` files. Mix already has a template for said files which we can customize, so let's ask Mix to copy them to our application:
+
+    $ mix release.init
+    * creating rel/vm.args.eex
+    * creating rel/remote.vm.args.eex
+    * creating rel/env.sh.eex
+    * creating rel/env.bat.eex
+
+If you open up `rel/env.sh.eex`, you will see:
+
+```shell
+#!/bin/sh
+
+# # Sets and enables heart (recommended only in daemon mode)
+# case $RELEASE_COMMAND in
+#   daemon*)
+#     HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND"
+#     export HEART_COMMAND
+#     export ELIXIR_ERL_OPTIONS="-heart"
+#     ;;
+#   *)
+#     ;;
+# esac
+
+# # Set the release to load code on demand (interactive) instead of preloading (embedded).
+# export RELEASE_MODE=interactive
+
+# # Set the release to work across nodes.
+# # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
+# export RELEASE_DISTRIBUTION=name
+# export RELEASE_NODE=<%= @release.name %>
+```
+
+The steps necessary to work across nodes is already commented out as an example. You can enable full distribution by setting the `RELEASE_DISTRIBUTION` variable to `name`.
+
+If you are on Windows, you will have to open up `rel/env.bat.eex`, where you will find this:
+
+```bat
+@echo off
+rem Set the release to load code on demand (interactive) instead of preloading (embedded).
+rem set RELEASE_MODE=interactive
+
+rem Set the release to work across nodes.
+rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
+rem set RELEASE_DISTRIBUTION=name
+rem set RELEASE_NODE=<%= @release.name %>
+```
+
+Once again, set the `RELEASE_DISTRIBUTION` variable to `name` and you are good to go!
+
+## VM arguments
+
+The `rel/vm.args.eex` allows you to specify low-level flags that control how the Erlang VM and its runtime operate. You specify entries as if you were specifying arguments in the command line with code comments also supported. Here is the default generated file:
+
+    ## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
+    ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
+
+    ## Increase number of concurrent ports/sockets
+    ##+Q 65536
+
+    ## Tweak GC to run more often
+    ##-env ERL_FULLSWEEP_AFTER 10
+
+You can see [a complete list of VM arguments and flags in the Erlang documentation](http://www.erlang.org/doc/man/erl.html).
+
+## Summing up
+
+Throughout the guide, we have built a very simple distributed key-value store as an opportunity to explore many constructs like generic servers, supervisors, tasks, agents, applications and more. Not only that, we have written tests for the whole application, got familiar with ExUnit, and learned how to use the Mix build tool to accomplish a wide range of tasks.
+
+If you are looking for a distributed key-value store to use in production, you should definitely look into [Riak](http://riak.com/products/riak-kv/), which also runs in the Erlang VM. In Riak, the buckets are replicated and stored across several nodes to avoid data loss.
+
+Of course, Elixir can be used for much more than distributed key-value stores. Embedded systems, data-processing and data-ingestion, web applications, audio/video streaming systems, machine learning, and others are many of the different domains Elixir excels at. We hope this guide has prepared you to explore any of those domains or any future domain you may desire to bring Elixir into.
+
+Happy coding!
diff --git a/lib/elixir/pages/mix-and-otp/supervisor-and-application.md b/lib/elixir/pages/mix-and-otp/supervisor-and-application.md
index 8c5839b2022..b3b51f5d090 100644
--- a/lib/elixir/pages/mix-and-otp/supervisor-and-application.md
+++ b/lib/elixir/pages/mix-and-otp/supervisor-and-application.md
@@ -3,142 +3,65 @@
   SPDX-FileCopyrightText: 2021 The Elixir Team
 -->
 
-# Supervision trees and applications
+# Registries and supervision trees
 
-In the previous chapter about `GenServer`, we implemented `KV.Registry` to manage buckets. At some point, we started monitoring buckets so we were able to take action whenever a `KV.Bucket` crashed. Although the change was relatively small, it introduced a question which is frequently asked by Elixir developers: what happens when something fails?
+In the [previous chapter](agents.md), we used agents to represent our buckets. In the [introduction to mix](introduction-to-mix.md), we specified we would like to name each bucket so we can do the following:
 
-Before we added monitoring, if a bucket crashed, the registry would forever point to a bucket that no longer exists. If a user tried to read or write to the crashed bucket, it would fail. Any attempt at creating a new bucket with the same name would just return the PID of the crashed bucket. In other words, that registry entry for that bucket would forever be in a bad state. Once we added monitoring, the registry automatically removes the entry for the crashed bucket. Trying to lookup the crashed bucket now (correctly) says the bucket does not exist and a user of the system can successfully create a new one if desired.
+```text
+CREATE shopping
+OK
 
-In practice, we are not expecting the processes working as buckets to fail. But, if it does happen, for whatever reason, we can rest assured that our system will continue to work as intended.
+PUT shopping milk 1
+OK
 
-If you have prior programming experience, you may be wondering: "could we just guarantee the bucket does not crash in the first place?". As we will see, Elixir developers tend to refer to those practices as "defensive programming". That's because a live production system has dozens of different reasons why something can go wrong. The disk can fail, memory can be corrupted, bugs, the network may stop working for a second, etc. If we were to write software that attempted to protect or circumvent all of those errors, we would spend more time handling failures than writing our own software!
-
-Therefore, an Elixir developer prefers to "let it crash" or "fail fast". And one of the most common ways we can recover from a failure is by restarting whatever part of the system crashed.
-
-For example, imagine your computer, router, printer, or whatever device is not working properly. How often do you fix it by restarting it? Once we restart the device, we reset the device back to its initial state, which is well-tested and guaranteed to work. In Elixir, we apply this same approach to software: whenever a process crashes, we start a new process to perform the same job as the crashed process.
-
-In Elixir, this is done by a Supervisor. A Supervisor is a process that supervises other processes and restarts them whenever they crash. To do so, Supervisors manage the whole life cycle of any supervised processes, including startup and shutdown.
-
-In this chapter, we will learn how to put those concepts into practice by supervising the `KV.Registry` process. After all, if something goes wrong with the registry, the whole registry is lost and no bucket could ever be found! To address this, we will define a `KV.Supervisor` module that guarantees that our `KV.Registry` is up and running at any given moment.
-
-At the end of the chapter, we will also talk about Applications. As we will see, Mix has been packaging all of our code into an application, and we will learn how to customize our application to guarantee that our Supervisor and the Registry are up and running whenever our system starts.
-
-## Our first supervisor
-
-A supervisor is a process which supervises other processes, which we refer to as child processes. The act of supervising a process includes three distinct responsibilities. The first one is to start child processes. Once a child process is running, the supervisor may restart a child process, either because it terminated abnormally or because a certain condition was reached. For example, a supervisor may restart all children if any child dies. Finally, a supervisor is also responsible for shutting down the child processes when the system is shutting down. Please see the `Supervisor` module for a more in-depth discussion.
-
-Creating a supervisor is not much different from creating a GenServer. We are going to define a module named `KV.Supervisor`, which will use the Supervisor behaviour, inside the `lib/kv/supervisor.ex` file:
-
-```elixir
-defmodule KV.Supervisor do
-  use Supervisor
-
-  def start_link(opts) do
-    Supervisor.start_link(__MODULE__, :ok, opts)
-  end
-
-  @impl true
-  def init(:ok) do
-    children = [
-      KV.Registry
-    ]
-
-    Supervisor.init(children, strategy: :one_for_one)
-  end
-end
+GET shopping milk
+1
+OK
 ```
 
-Our supervisor has a single child so far: `KV.Registry`. After we define a list of children, we call `Supervisor.init/2`, passing the children and the supervision strategy.
-
-The supervision strategy dictates what happens when one of the children crashes. `:one_for_one` means that if a child dies, it will be the only one restarted. Since we have only one child now, that's all we need. The `Supervisor` behaviour supports several strategies, which we will discuss in this chapter.
+In the example session above we interacted with the "shopping" bucket by referencing its name. Therefore, an important feature in our key-value store is to give names to processes.
 
-Once the supervisor starts, it will traverse the list of children and it will invoke the `child_spec/1` function on each module.
-
-The `child_spec/1` function returns the child specification which describes how to start the process, if the process is a worker or a supervisor, if the process is temporary, transient or permanent and so on. The `child_spec/1` function is automatically defined when we `use Agent`, `use GenServer`, `use Supervisor`, etc. Let's give it a try in the terminal with `iex -S mix`:
+We have also learned in the previous chapter we can already name our buckets. For example:
 
 ```elixir
-iex> KV.Registry.child_spec([])
-%{id: KV.Registry, start: {KV.Registry, :start_link, [[]]}}
-```
-
-We will learn those details as we move forward on this guide. If you would rather peek ahead, check the `Supervisor` docs.
-
-After the supervisor retrieves all child specifications, it proceeds to start its children one by one, in the order they were defined, using the information in the `:start` key in the child specification. For our current specification, it will call `KV.Registry.start_link([])`.
-
-Let's take the supervisor for a spin:
-
-```elixir
-iex> {:ok, sup} = KV.Supervisor.start_link([])
-{:ok, #PID<0.148.0>}
-iex> Supervisor.which_children(sup)
-[{KV.Registry, #PID<0.150.0>, :worker, [KV.Registry]}]
-```
-
-So far we have started the supervisor and listed its children. Once the supervisor started, it also started all of its children.
-
-What happens if we intentionally crash the registry started by the supervisor? Let's do so by sending it a bad input on `call`:
-
-```elixir
-iex> [{_, registry, _, _}] = Supervisor.which_children(sup)
-[{KV.Registry, #PID<0.150.0>, :worker, [KV.Registry]}]
-iex> GenServer.call(registry, :bad_input)
-08:52:57.311 [error] GenServer #PID<0.150.0> terminating
-** (FunctionClauseError) no function clause matching in KV.Registry.handle_call/3
-iex> Supervisor.which_children(sup)
-[{KV.Registry, #PID<0.157.0>, :worker, [KV.Registry]}]
-```
-
-Notice how the supervisor automatically started a new registry, with a new PID, in place of the first one once we caused it to crash due to a bad input.
-
-In the previous chapters, we have always started processes directly. For example, we would call `KV.Registry.start_link([])`, which would return `{:ok, pid}`, and that would allow us to interact with the registry via its `pid`. Now that processes are started by the supervisor, we have to directly ask the supervisor who its children are, and fetch the PID from the returned list of children. In practice, doing so every time would be very expensive. To address this, we often give names to processes, allowing them to be uniquely identified in a single machine from anywhere in our code.
-
-Let's learn how to do that.
-
-## Naming processes
-
-While our application will have many buckets, it will only have a single registry. Therefore, whenever we start the registry, we want to give it a unique name so we can reach out to it from anywhere. We do so by passing a `:name` option to `KV.Registry.start_link/1`.
-
-Let's slightly change our children definition (in `KV.Supervisor.init/1`) to be a list of tuples instead of a list of atoms:
-
-```elixir
-  def init(:ok) do
-    children = [
-      {KV.Registry, name: KV.Registry}
-    ]
+iex> KV.Bucket.start_link(name: :shopping)
+{:ok, #PID<0.43.0>}
+iex> KV.Bucket.put(:shopping, "milk", 1)
+:ok
+iex> KV.Bucket.get(:shopping, "milk")
+1
 ```
 
-With this in place, the supervisor will now start `KV.Registry` by calling `KV.Registry.start_link(name: KV.Registry)`.
+However, naming dynamic processes with atoms is a terrible idea! If we use atoms, we would need to convert the bucket name (often received from an external client) to atoms, and **we should never convert user input to atoms**. This is because atoms are not garbage collected. Once an atom is created, it is never reclaimed. Generating atoms from user input would mean the user can inject enough different names to exhaust our system memory!
 
-If you revisit the `KV.Registry.start_link/1` implementation, you will remember it simply passes the options to GenServer:
+In practice, it is more likely you will reach the Erlang VM limit for the maximum number of atoms before you run out of memory, which will bring your system down regardless.
 
-```elixir
-  def start_link(opts) do
-    GenServer.start_link(__MODULE__, :ok, opts)
-  end
-```
+Luckily, Elixir (and Erlang) comes with built-in abstractions for naming processes, called name registries, each with different trade-offs which we will explore throughout these guides.
 
-which in turn will register the process with the given name. The `:name` option expects an atom for locally named processes (locally named means it is available to this machine — there are other options, which we won't discuss here). Since module identifiers are atoms (try `i(KV.Registry)` in IEx), we can name a process after the module that implements it, provided there is only one process for that name. This helps when debugging and introspecting the system.
+## Local, decentralized, and scalable registry
 
-Let's give the updated supervisor a try inside `iex -S mix`:
+Elixir ships with a single-node process registry module aptly called `Registry`. Its main feature is that you can use any Elixir value to name a process, not only atoms. Let's take it for a spin in `iex`:
 
 ```elixir
-iex> KV.Supervisor.start_link([])
-{:ok, #PID<0.66.0>}
-iex> KV.Registry.create(KV.Registry, "shopping")
+iex> Registry.start_link(name: KV, keys: :unique)
+iex> name = {:via, Registry, {KV, "shopping"}}
+iex> KV.Bucket.start_link(name: name)
+{:ok, #PID<0.43.0>}
+iex> KV.Bucket.put(name, "milk", 1)
 :ok
-iex> KV.Registry.lookup(KV.Registry, "shopping")
-{:ok, #PID<0.70.0>}
+iex> KV.Bucket.get(name, "milk")
+1
 ```
 
-This time the supervisor started a named registry, allowing us to create buckets without having to explicitly fetch the PID from the supervisor. You should also know how to make the registry crash again, without looking up its PID: give it a try.
-
-> At this point, you may be wondering: should you also locally name bucket processes? Remember buckets are started dynamically based on user input. Since local names MUST be atoms, we would have to dynamically create atoms, which is a bad idea since once an atom is defined, it is never erased nor garbage collected. This means that, if we create atoms dynamically based on user input, we will eventually run out of memory (or to be more precise, the VM will crash because it imposes a hard limit on the number of atoms). This limitation is precisely why we created our own registry (or why one would use Elixir's built-in `Registry` module).
+As you can see, instead of passing an atom to the `:name` option, we pass a tuple of shape `{:via, registry_module, {registry_name, process_name}}`, and everything just worked. You could have used anything as the `process_name`, even an integer or a map! That's because all of Elixir built-in behaviours, agents, supervisors, tasks, etc, are compatible with naming registries, as long as you pass them using the "via" tuple format.
 
-We are getting closer and closer to a fully working system. The supervisor automatically starts the registry. But how can we automatically start the supervisor whenever our system starts? To answer this question, let's talk about applications.
+Therefore, all we need to do to name our buckets is to start a `Registry`, using `Registry.start_link/1`. But you may be wondering, where exactly should we place that?
 
 ## Understanding applications
 
-We have been working inside an application this entire time. Every time we changed a file and ran `mix compile`, we could see a `Generated kv app` message in the compilation output.
+Every Elixir project is an application. Elixir itself is defined in an application named `:elixir`. The `ExUnit.Case` module is part of the `:ex_unit` application. And so forth.
+
+In fact, we have been working inside an application this entire time. Every time we changed a file and ran `mix compile`, we could see a `Generated kv app` message in the compilation output.
 
 We can find the generated `.app` file at `_build/dev/lib/kv/ebin/kv.app`. Let's have a look at its contents:
 
@@ -146,8 +69,7 @@ We can find the generated `.app` file at `_build/dev/lib/kv/ebin/kv.app`. Let's
 {application,kv,
              [{applications,[kernel,stdlib,elixir,logger]},
               {description,"kv"},
-              {modules,['Elixir.KV','Elixir.KV.Bucket','Elixir.KV.Registry',
-                        'Elixir.KV.Supervisor']},
+              {modules,['Elixir.KV','Elixir.KV.Bucket']},
               {registered,[]},
               {vsn,"0.1.0"}]}.
 ```
@@ -156,7 +78,7 @@ This file contains Erlang terms (written using Erlang syntax). Even though we ar
 
 > The `logger` application ships as part of Elixir. We stated that our application needs it by specifying it in the `:extra_applications` list in `mix.exs`. See the [official documentation](`Logger`) for more information.
 
-In a nutshell, an application consists of all the modules defined in the `.app` file, including the `.app` file itself. An application has generally only two directories: `ebin`, for Elixir artifacts, such as `.beam` and `.app` files, and `priv`, with any other artifact or asset you may need in your application.
+In a nutshell, an application consists of all the modules defined in the `.app` file, including the `.app` file itself. The application itself is located at the `_build/dev/lib/kv` folder and typically has only two directories: `ebin`, for Elixir artifacts, such as `.beam` and `.app` files, and `priv`, with any other artifact or asset you may need in your application.
 
 Although Mix generates and maintains the `.app` file for us, we can customize its contents by adding new entries to the `application/0` function inside the `mix.exs` project file. We are going to do our first customization soon.
 
@@ -196,9 +118,9 @@ iex> Application.ensure_all_started(:kv)
 {:ok, [:logger, :kv]}
 ```
 
-In practice, our tools always start our applications for us, but there is an API available if you need fine-grained control.
+In practice, our tools always start our applications for us, and you don't have to worry about the above, but it is good to know how it all works behind the scenes.
 
-## The application callback
+### The application callback
 
 Whenever we invoke `iex -S mix`, Mix automatically starts our application by calling `Application.start(:kv)`. But can we customize what happens when our application starts? As a matter of fact, we can! To do so, we define an application callback.
 
@@ -213,50 +135,119 @@ The first step is to tell our application definition (for example, our `.app` fi
   end
 ```
 
-The `:mod` option specifies the "application callback module", followed by the arguments to be passed on application start. The application callback module can be any module that implements the `Application` behaviour.
+The `:mod` option specifies the "application callback module", followed by the arguments to be passed on application start. The application callback module can be any module that invokes `use Application`. Since we have specified `KV` as the module callback, let's change the `KV` module defined in `lib/kv.ex` to the following:
 
-To implement the `Application` behaviour, we have to `use Application` and define a `start/2` function. The goal of `start/2` is to start a supervisor, which will then start any child services or execute any other code our application may need. Let's use this opportunity to start the `KV.Supervisor` we have implemented earlier in this chapter.
+```elixir
+defmodule KV do
+  use Application
+end
+```
 
-Since we have specified `KV` as the module callback, let's change the `KV` module defined in `lib/kv.ex` to implement a `start/2` function:
+Now run `mix test` and you will see a couple things happening. First of all, you will get a compilation warning:
+
+```text
+Compiling 1 file (.ex)
+    warning: function start/2 required by behaviour Application is not implemented (in module KV)
+    │
+  1 │ defmodule KV do
+    │ ~~~~~~~~~~~~~~~
+    │
+    └─ lib/kv.ex:1: KV (module)
+```
+
+This warning is telling us that `use Application` actually defines a behaviour, which expects us to implement to a `start/2` function in our `KV` module.
+
+Then our application does not even boot because the `start/2` function is not actually implemented:
+
+```text
+18:29:39.109 [notice] Application kv exited: exited in: KV.start(:normal, [])
+    ** (EXIT) an exception was raised:
+        ** (UndefinedFunctionError) function KV.start/2 is undefined or private
+```
+
+Implementing the `start/2` callback is relatively straight-forward, all we need to do is to start a supervision tree, and return `{:ok, root_supervisor_pid}`. The `Supervisor.start_link/2` function does precisely that, it only expects a list of children and the supervision strategy. Let's just pass an empty list of children for now:
 
 ```elixir
 defmodule KV do
   use Application
 
+  # The @impl true annotation says we are implementing a callback
   @impl true
   def start(_type, _args) do
-    # Although we don't use the supervisor name below directly,
-    # it can be useful when debugging or introspecting the system.
-    KV.Supervisor.start_link(name: KV.Supervisor)
+    Supervisor.start_link([], strategy: :one_for_one)
   end
 end
 ```
 
-> Please note that by doing this, we are breaking the boilerplate test case which tested the `hello` function in `KV`. You can simply remove that test case.
+Now run `mix test` again and our app should boot but we should see one failure. When we changed the `KV` module, we broke the boilerplate test case which tested the `KV.hello/0` function. You can simply remove that test case and we are back to a green suite.
+
+We wrote very little code but we did something incredibly powerful. We now have a function, `KV.start/2` that is invoked whenever your application starts. This gives us the perfect place to start our key-value registry. The `Application` module also allows us to define a `stop/1` callback and other funtionality. You can check the `Application` and `Supervisor` modules for extensive documentation on their uses.
+
+Let's finally start our registry.
+
+## Supervision trees
 
-When we `use Application`, we may define a couple of functions, similar to when we used `Supervisor` or `GenServer`. This time we only had to define a `start/2` function. The `Application` behaviour also has a `stop/1` callback, but it is rarely used in practice. You can check the documentation for more information.
+Now that we have the `start/2` callback, we can finally go ahead and start our registry. You may be tempted to do it like this:
 
-Now that you have defined an application callback which starts our supervisor, we expect the `KV.Registry` process to be up and running as soon as we start `iex -S mix`. Let's give it another try:
+```elixir
+  def start(_type, _args) do
+    Registry.start_link(name: KV, keys: :unique)
+    Supervisor.start_link([], strategy: :one_for_one)
+  end
+```
+
+However, this would not be a good idea. In Elixir, we typically start processes inside supervision trees. In fact, we rarely use the `start_link` functions to start processes (except at the root of the supervision tree itself). Instead, do this:
 
 ```elixir
-iex> KV.Registry.create(KV.Registry, "shopping")
+  def start(_type, _args) do
+    children = [
+      {Registry, name: KV, keys: :unique}
+    ]
+
+    Supervisor.start_link(children, strategy: :one_for_one)
+  end
+```
+
+A supervisor receives one or more child specifications that tell it exactly how to start each child. A child specification is typically represented by a `{module, options}` pair, as shown above, and often as simply the module name. Sometimes, these children are supervisors themselves, giving us supervision trees.
+
+Let's take it for a spin and see if we can indeed name our buckets using our new registry. Let's make sure to start a new `iex -S mix` (`recompile()` is not enough, as it does not reload your supervision tree) and then:
+
+```iex
+iex> name = {:via, Registry, {KV, "shopping"}}
+iex> KV.Bucket.start_link(name: name)
+{:ok, #PID<0.43.0>}
+iex> KV.Bucket.put(name, "milk", 1)
 :ok
-iex> KV.Registry.lookup(KV.Registry, "shopping")
-{:ok, #PID<0.88.0>}
+iex> KV.Bucket.get(name, "milk")
+1
 ```
 
-Let's recap what is happening. Whenever we invoke `iex -S mix`, it automatically starts our application by calling `Application.start(:kv)`, which then invokes the application callback. The application callback's job is to start a **supervision tree**. Right now, our supervisor has a single child named `KV.Registry`, started with name `KV.Registry`. Our supervisor could have other children, and some of these children could be their own supervisors with their own children, leading to the so-called supervision trees.
+Perfect, this time we didn't need to start the registry inside `iex`, as it was started as part of the application itself.
+
+By starting processes inside supervisors, we gain important properties such as:
+
+  * **Introspection**: for each application, you can fully introspect and visualize each process in its supervision tree, its memory usage, message queue, etc
+
+  * **Resilience**: when a process fails for an unexpected reason, its supervisor controls if and how those processes should be restarted, leading to self-healing systems
+
+  * **Graceful shutdown**: when your application is shutting down, the children of a supervision tree are terminated in the opposite order they were started, leading to graceful shutdowns
 
 ## Projects or applications?
 
-Mix makes a distinction between projects and applications. Based on the contents of our `mix.exs` file, we would say we have a Mix project that defines the `:kv` application. As we will see in later chapters, there are projects that don't define any application.
+Mix makes a distinction between projects and applications. Based on the contents of our `mix.exs` file, we would say we have a Mix project that defines the `:kv` application.
 
 When we say "project" you should think about Mix. Mix is the tool that manages your project. It knows how to compile your project, test your project and more. It also knows how to compile and start the application relevant to your project.
 
 When we talk about applications, we talk about OTP. Applications are the entities that are started and stopped as a whole by the runtime. You can learn more about applications and how they relate to booting and shutting down of your system as a whole in the documentation for the `Application` module.
 
-## Next steps
+## Summing up
+
+We learned important concepts in this chapter:
+
+  * Naming registries allow us to find processes in a given machine (or, as we will see in the future, even in a cluster)
+
+  * Applications bundle our modules, its dependencies, and how code starts and stops
 
-Although this chapter was the first time we implemented a supervisor, it was not the first time we used one! In the previous chapter, when we used `start_supervised!` to start the registry during our tests, `ExUnit` started the registry under a supervisor managed by the ExUnit framework itself. By defining our own supervisor, we provide more structure on how we initialize, shutdown and supervise processes in our applications, aligning our production code and tests with best practices.
+  * Processes are started as part of supervisors for introspection and fault-tolerance
 
-But we are not done yet. So far we are supervising the registry but our application is also starting buckets. Since buckets are started dynamically, we can use a special type of supervisor called `DynamicSupervisor`, which is optimized to handle such scenarios. Let's explore it next.
+In the next chapter, we will tie it all up by making sure all our buckets are named and supervised. To do so, we will learn a new tool called dynamic supervisors.
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 3a2a7389109..fe037ee3dcc 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
@@ -5,7 +5,7 @@
 
 # Task and gen_tcp
 
-In this chapter, we are going to learn how to use Erlang's [`:gen_tcp` module](`:gen_tcp`) to serve requests. This provides a great opportunity to explore Elixir's `Task` module. In future chapters, we will expand our server so that it can actually serve the commands.
+In this chapter, we are going to learn how to use Erlang's [`:gen_tcp` module](`:gen_tcp`) to serve requests. This provides a great opportunity to explore Elixir's `Task` module. In future chapters, we will expand our server so that it can actually interact with buckets.
 
 ## Echo server
 
@@ -17,10 +17,10 @@ A TCP server, in broad strokes, performs the following steps:
   2. Waits for a client connection on that port and accepts it
   3. Reads the client request and writes a response back
 
-Let's implement those steps. Move to the `apps/kv_server` application, open up `lib/kv_server.ex`, and add the following functions:
+Let's implement those steps. Create a new `lib/kv/server.ex` and add the following functions:
 
 ```elixir
-defmodule KVServer do
+defmodule KV.Server do
   require Logger
 
   def accept(port) do
@@ -62,7 +62,7 @@ defmodule KVServer do
 end
 ```
 
-We are going to start our server by calling `KVServer.accept(4040)`, where 4040 is the port. The first step in `accept/1` is to listen to the port until the socket becomes available and then call `loop_acceptor/1`. `loop_acceptor/1` is a loop accepting client connections. For each accepted connection, we call `serve/1`.
+We are going to start our server by calling `KV.Server.accept(4040)`, where 4040 is the port. The first step in `accept/1` is to listen to the port until the socket becomes available and then call `loop_acceptor/1`. `loop_acceptor/1` is a loop accepting client connections. For each accepted connection, we call `serve/1`.
 
 `serve/1` is another loop that reads a line from the socket and writes those lines back to the socket. Note that the `serve/1` function uses the pipe operator `|>/2` to express this flow of operations. The pipe operator evaluates the left side and passes its result as the first argument to the function on the right side. The example above:
 
@@ -85,7 +85,7 @@ This is pretty much all we need to implement our echo server. Let's give it a tr
 Start an IEx session inside the `kv_server` application with `iex -S mix`. Inside IEx, run:
 
 ```elixir
-iex> KVServer.accept(4040)
+iex> KV.Server.accept(4040)
 ```
 
 The server is now running, and you will even notice the console is blocked. Let's use [a `telnet` client](https://en.wikipedia.org/wiki/Telnet) to access our server. There are clients available on most operating systems, and their command lines are generally similar:
@@ -109,10 +109,12 @@ My particular telnet client can be exited by typing `ctrl + ]`, typing `quit`, a
 
 Once you exit the telnet client, you will likely see an error in the IEx session:
 
-    ** (MatchError) no match of right hand side value: {:error, :closed}
-        (kv_server) lib/kv_server.ex:45: KVServer.read_line/1
-        (kv_server) lib/kv_server.ex:37: KVServer.serve/1
-        (kv_server) lib/kv_server.ex:30: KVServer.loop_acceptor/1
+```text
+** (MatchError) no match of right hand side value: {:error, :closed}
+    (kv) lib/kv/server.ex:45: KV.Server.read_line/1
+    (kv) lib/kv/server.ex:37: KV.Server.serve/1
+    (kv) lib/kv/server.ex:30: KV.Server.loop_acceptor/1
+```
 
 That's because we were expecting data from `:gen_tcp.recv/2` but the client closed the connection. We need to handle such cases better in future revisions of our server.
 
@@ -120,36 +122,25 @@ For now, there is a more important bug we need to fix: what happens if our TCP a
 
 ## Tasks
 
-We have learned about agents, generic servers, and supervisors. They are all meant to work with multiple messages or manage state. But what do we use when we only need to execute some task and that is it?
-
-The `Task` module provides this functionality exactly. For example, it has a `Task.start_link/1` function that receives an anonymous function and executes it inside a new process that will be part of a supervision tree.
+Whenever you have an existing function and you simply want to execute it when your application starts, the `Task` module is exactly you need. For example, it has a `Task.start_link/1` function that receives an anonymous function and executes it inside a new process that will be part of a supervision tree.
 
-Let's give it a try. Open up `lib/kv_server/application.ex`, and let's change the supervisor in the `start/2` function to the following:
+Let's give it a try. Open up `lib/kv.ex` and let's add a new child:
 
 ```elixir
   def start(_type, _args) do
     children = [
-      {Task, fn -> KVServer.accept(4040) end}
+      {Registry, name: KV, keys: :unique},
+      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
+      {Task, fn -> KV.Server.accept(4040) end}
     ]
 
-    opts = [strategy: :one_for_one, name: KVServer.Supervisor]
-    Supervisor.start_link(children, opts)
+    Supervisor.start_link(children, strategy: :one_for_one)
   end
 ```
 
-As usual, we've passed a two-element tuple as a child specification, which in turn will invoke `Task.start_link/1`.
-
-With this change, we are saying that we want to run `KVServer.accept(4040)` as a task. We are hardcoding the port for now but this could be changed in a few ways, for example, by reading the port out of the system environment when starting the application:
-
-```elixir
-port = String.to_integer(System.get_env("PORT") || "4040")
-# ...
-{Task, fn -> KVServer.accept(port) end}
-```
+With this change, we are saying that we want to run `KV.Server.accept(4040)` as a task. We are hardcoding the port for now but we will make this a configuration in later chapters. As usual, we've passed a two-element tuple as a child specification, which in turn will invoke `Task.start_link/1`.
 
-Insert these changes in your code and now you may start your application using the following command `PORT=4321 mix run --no-halt`, notice how we are passing the port as a variable, but still defaults to 4040 if none is given.
-
-Now that the server is part of the supervision tree, it should start automatically when we run the application. Start your server, now passing the port, and once again use the `telnet` client to make sure that everything still works:
+Now that the server is part of the supervision tree, it should start automatically when we run the application. Run `iex -S mix` to boot the app and use the `telnet` client to make sure that everything still works:
 
 ```console
 $ telnet 127.0.0.1 4321
@@ -178,7 +169,7 @@ HELLOOOOOO?
 
 It doesn't seem to work at all. That's because we are serving requests in the same process that are accepting connections. When one client is connected, we can't accept another client.
 
-## Task supervisor
+## Adding (flawed) concurrency
 
 In order to make our server handle simultaneous connections, we need to have one process working as an acceptor that spawns other processes to serve requests. One solution would be to change:
 
@@ -195,43 +186,49 @@ to also use `Task.start_link/1`:
 ```elixir
 defp loop_acceptor(socket) do
   {:ok, client} = :gen_tcp.accept(socket)
-  Task.start_link(fn -> serve(client) end)
+  {:ok, pid} = Task.start_link(fn -> serve(client) end)
+  :ok = :gen_tcp.controlling_process(client, pid)
   loop_acceptor(socket)
 end
 ```
 
-We are starting a linked Task directly from the acceptor process. But we've already made this mistake once. Do you remember?
+In the new acceptor loop, we are starting a new task every time there is a new client. Now, if you attempt to connect two clients at the same time, it should work!
+
+Or does it? For example, what happens when you exit one telnet session? The other session should crash! The reason of this crash is two fold:
+
+1. We have a bug in our server where we don't expect `:gen_tcp.recv/2` to return an `{:error, :closed}` tuple
+
+2. Because each server task is linked to the acceptor process, if one task crashes, the acceptor process will also crash, taking down all other tasks and clients
 
-This is similar to the mistake we made when we called `KV.Bucket.start_link/1` straight from the registry. That meant a failure in any bucket would bring the whole registry down.
+An important rule thumb throughout this guide is to always start processes as children of supervisors. The code above is an excellent example of what happens when we don't. If we don't isolate the different parts of our systems, failures can now cascade through our system, as it would happen in other languages.
 
-The code above would have the same flaw: if we link the `serve(client)` task to the acceptor, a crash when serving a request would bring the acceptor, and consequently all other connections, down.
+To fix this, we could use a `DynamicSupervisor`, but tasks also provide a specialized `Task.Supervisor` which has better ergonomics and is optimized for supervising tasks themselves. Let's give it a try.
 
-We fixed the issue for the registry by using a simple one for one supervisor. We are going to use the same tactic here, except that this pattern is so common with tasks that `Task` already comes with a solution: a simple one for one supervisor that starts temporary tasks as part of our supervision tree.
+## Adding a task supervisor
 
-Let's change `start/2` once again, to add a supervisor to our tree:
+Let's change `start/2` in `lib/kv.ex` once more, to add the task supervisor to our tree:
 
 ```elixir
   def start(_type, _args) do
-    port = String.to_integer(System.get_env("PORT") || "4040")
-
     children = [
-      {Task.Supervisor, name: KVServer.TaskSupervisor},
-      {Task, fn -> KVServer.accept(port) end}
+      {Registry, name: KV, keys: :unique},
+      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
+      {Task.Supervisor, name: KV.ServerSupervisor},
+      {Task, fn -> KV.Server.accept(4040) end}
     ]
 
-    opts = [strategy: :one_for_one, name: KVServer.Supervisor]
-    Supervisor.start_link(children, opts)
+    Supervisor.start_link(children, strategy: :one_for_one)
   end
 ```
 
-We'll now start a `Task.Supervisor` process with name `KVServer.TaskSupervisor`. Remember, since the acceptor task depends on this supervisor, the supervisor must be started first.
+We'll now start a `Task.Supervisor` process with name `KV.TaskSupervisor`. Keep in mind that the order children are started matters. For example, the acceptor must come last because, if it comes first, it means our application can start accepting requests before the `Task.Supervisor` is running or before we can locate buckets. Shutting down an application will also stop the children in reverse order, guaranteeing a clean termination.
 
 Now we need to change `loop_acceptor/1` to use `Task.Supervisor` to serve each request:
 
 ```elixir
 defp loop_acceptor(socket) do
   {:ok, client} = :gen_tcp.accept(socket)
-  {:ok, pid} = Task.Supervisor.start_child(KVServer.TaskSupervisor, fn -> serve(client) end)
+  {:ok, pid} = Task.Supervisor.start_child(KV.BucketSupervisor, fn -> serve(client) end)
   :ok = :gen_tcp.controlling_process(client, pid)
   loop_acceptor(socket)
 end
@@ -239,76 +236,72 @@ end
 
 You might notice that we added a line, `:ok = :gen_tcp.controlling_process(client, pid)`. This makes the child process the "controlling process" of the `client` socket. If we didn't do this, the acceptor would bring down all the clients if it crashed because sockets would be tied to the process that accepted them (which is the default behavior).
 
-Start a new server with `PORT=4040 mix run --no-halt` and we can now open up many concurrent telnet clients. You will also notice that quitting a client does not bring the acceptor down. Excellent!
+Now start a new server with `iex -S mix` and try to open up many concurrent telnet clients. You will notice that quitting a client does not bring the acceptor down, even though we haven't fixed the bug in `:gen_tcp.recv/2` yet (which we will address in the next chapter). Excellent!
 
-Here is the full echo server implementation:
+## Restart strategies
 
-```elixir
-defmodule KVServer do
-  require Logger
+There is one important topic we haven't explored yet with the necessary depth. What happens when a supervised process crashes?
 
-  @doc """
-  Starts accepting connections on the given `port`.
-  """
-  def accept(port) do
-    {:ok, socket} = :gen_tcp.listen(port,
-                      [:binary, packet: :line, active: false, reuseaddr: true])
-    Logger.info "Accepting connections on port #{port}"
-    loop_acceptor(socket)
-  end
+In the previous chapter, when we started a bucket and killed it, the supervisor automatically started one in its place:
 
-  defp loop_acceptor(socket) do
-    {:ok, client} = :gen_tcp.accept(socket)
-    {:ok, pid} = Task.Supervisor.start_child(KVServer.TaskSupervisor, fn -> serve(client) end)
-    :ok = :gen_tcp.controlling_process(client, pid)
-    loop_acceptor(socket)
-  end
+```elixir
+iex> children = [{KV.Bucket, name: :shopping}]
+iex> Supervisor.start_link(children, strategy: :one_for_one)
+iex> KV.Bucket.put(:shopping, "milk", 1)
+iex> pid = Process.whereis(:shopping)
+#PID<0.48.0>
+iex> Process.exit(pid, :kill)
+true
+iex> Process.whereis(:shopping)
+#PID<0.50.0>
+```
 
-  defp serve(socket) do
-    socket
-    |> read_line()
-    |> write_line(socket)
+What exactly happens when a process terminates is part of its child specification. For `KV.Bucket`, we have this:
 
-    serve(socket)
-  end
+```elixir
+iex> KV.Bucket.child_spec([])
+%{id: KV.Bucket, start: {KV.Bucket, :start_link, [[]]}}
+```
 
-  defp read_line(socket) do
-    {:ok, data} = :gen_tcp.recv(socket, 0)
-    data
-  end
+However, for tasks, we have this:
 
-  defp write_line(line, socket) do
-    :gen_tcp.send(socket, line)
-  end
-end
+```elixir
+iex> Task.child_spec(fn -> :ok end)
+%{
+  id: Task,
+  restart: :temporary,
+  start: {Task, :start_link, [#Function<43.39164016/0 in :erl_eval.expr/6>]}
+}
 ```
 
-Since we have changed the supervisor specification, we need to ask: is our supervision strategy still correct?
+Notice that a task says `:restart` is `:temporary`. `KV.Bucket` says nothing, which means it defaults to `:permanent`. `:temporary` means that a process is never restarted, regardless of why it crashed. `:permanent` means a process is always restarted, regardless of the exit reason. There is also `:transient`, which means it won't be restarted as long as it terminates successfully.
+
+Now we must ask ourselves, are those the correct settings?
 
-In this case, the answer is yes: if the acceptor crashes, there is no need to crash the existing connections. On the other hand, if the task supervisor crashes, there is no need to crash the acceptor too.
+For `KV.Bucket`, using `:permanent` seem logical, as should not request the user to recreate a bucket they have previous created. Although currently we would lose the bucket data, in actual system we would add mechanisms to recover it on initialization. However, for tasks, we have used them in two opposing ways in this chapter, which means at least one of them is wrong.
 
-However, there is still one concern left, which are the restart strategies. Tasks, by default, have the `:restart` value set to `:temporary`, which means they are not restarted. This is an excellent default for the connections started via the `Task.Supervisor`, as it makes no sense to restart a failed connection, but it is a bad choice for the acceptor. If the acceptor crashes, we want to bring the acceptor up and running again.
+We use a task to start the acceptor. The acceptor is a critical component of our infrastructure. If it crashes, it means we won't accept further requests, and our server would then be useless as no one can connect to it. On the other hand, we also use `Task.Supervisor` to start tasks that deal with each connection. In this case, restarting may not be useful at all, given the reason we crashed could just as well be a connection issue, and attempting to restart over the same connection would lead to further failures.
 
-Let's fix this. We know that for a child of shape `{Task, fun}`, Elixir will invoke `Task.child_spec(fun)` to retrieve the underlying child specification. Therefore, one might imagine that to change the `{Task, fun}` specification to have a `:restart` of `:permanent`, we would need to change the `Task` module. However, that's impossible to do, as the `Task` module is defined as part of Elixir's standard library (and even if it was possible, it is unlikely it would be a good idea).
-Luckily, this can be done by using `Supervisor.child_spec/2`, which allows us to configure a child specification with new values. Let's rewrite `start/2` in `KVServer.Application` once more:
+Therefore, we want the acceptor to actually run in `:permanent` mode, while we preserve the `Task.Supervisor` as `:temporary`. Luckily Elixir has an API that allows us to change an existing child specification, which we use below.
+
+Let's change `start/2` in `lib/kv.ex` once more to the following:
 
 ```elixir
   def start(_type, _args) do
-    port = String.to_integer(System.get_env("PORT") || "4040")
-
     children = [
-      {Task.Supervisor, name: KVServer.TaskSupervisor},
-      Supervisor.child_spec({Task, fn -> KVServer.accept(port) end}, restart: :permanent)
+      {Registry, name: KV, keys: :unique},
+      {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
+      {Task.Supervisor, name: KV.ServerSupervisor},
+      Supervisor.child_spec({Task, fn -> KV.Server.accept(4040) end}, restart: :permanent)
     ]
 
-    opts = [strategy: :one_for_one, name: KVServer.Supervisor]
-    Supervisor.start_link(children, opts)
+    Supervisor.start_link(children, strategy: :one_for_one)
   end
 ```
 
 Now we have an always running acceptor that starts temporary task processes under an always running task supervisor.
 
-## Wrapping up
+## Leveraging the ecosystem
 
 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).
 
diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md
index 84ee8250522..f44bf9acd0d 100644
--- a/lib/elixir/pages/references/compatibility-and-deprecations.md
+++ b/lib/elixir/pages/references/compatibility-and-deprecations.md
@@ -14,12 +14,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
 
 Elixir version | Support
 :------------- | :-----------------------------
-1.19           | Development
-1.18           | Bug fixes and security patches
+1.19           | Bug fixes and security patches
+1.18           | Security patches only
 1.17           | Security patches only
 1.16           | Security patches only
 1.15           | Security patches only
-1.14           | 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).
 
@@ -49,7 +48,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
 
 Elixir version | Supported Erlang/OTP versions
 :------------- | :-------------------------------
-1.19           | 26 - 27
+1.19           | 26 - 28
 1.18           | 25 - 27
 1.17           | 25 - 27
 1.16           | 24 - 26
@@ -236,4 +235,4 @@ Version | Deprecated feature                                  | Replaced by (ava
 [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/v1.17/CHANGELOG.md#4-hard-deprecations
 [v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
-[v1.19]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
+[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
diff --git a/lib/elixir/pages/references/gradual-set-theoretic-types.md b/lib/elixir/pages/references/gradual-set-theoretic-types.md
index b33ba852d0f..b63097ee682 100644
--- a/lib/elixir/pages/references/gradual-set-theoretic-types.md
+++ b/lib/elixir/pages/references/gradual-set-theoretic-types.md
@@ -97,7 +97,7 @@ If you give it an integer, it negates it. If you give it a boolean, it negates i
 
 We can say this function has the type `(integer() -> integer())` because it is capable of receiving an integer and returning an integer. In this case, `(integer() -> integer())` is a set that represents all functions that can receive an integer and return an integer. Even though this function can receive other arguments and return other values, it is still part of the `(integer() -> integer())` set.
 
-This function also has the type `(boolean() -> boolean())`, because it receives booleans and returns booleans. Therefore, we can say the overall type of the function is `(integer() -> integer()) and (boolean() -> boolean())`. The intersection means the function belongs to both sets.
+This function also has the type `(boolean() -> boolean())`, because it also receives booleans and returns booleans. If you pass the function above to another function that expects `(boolean() -> boolean())`, type checking will succeed. Therefore, we can say the overall type of the function is `(integer() -> integer()) and (boolean() -> boolean())`. The intersection means the function belongs to both sets.
 
 At this point, you may ask, why not a union? As a real-world example, take a t-shirt with green and yellow stripes. We can say the t-shirt belongs to the set of "t-shirts with green color". We can also say the t-shirt belongs to the set of "t-shirts with yellow color". Let's see the difference between unions and intersections:
 
@@ -105,7 +105,7 @@ At this point, you may ask, why not a union? As a real-world example, take a t-s
 
   * `(t_shirts_with_green() and t_shirts_with_yellow())` - contains t-shirts with both green and yellow (and maybe other colors)
 
-Since the t-shirt has both colors, we say it belongs to the intersection of both sets. The same way that a function that goes from `(integer() -> integer())` and `(boolean() -> boolean())` is also an intersection. In practice, it does not make sense to define the union of two functions in Elixir, so the compiler will always point to the right direction.
+Since the t-shirt has both colors, we could say it belongs to the union of green and yellow t-shirts, but doing so would not capture the fact it is both green and yellow. Therefore it is more precise to say it belongs to the intersection of both sets. The same way that a function that goes from `(integer() -> integer())` and `(boolean() -> boolean())` is also an intersection. In practice, it is not useful to define the union of two functions in Elixir, so the compiler will point you to the right direction if you specify the wrong one.
 
 ## The `dynamic()` type
 
diff --git a/lib/elixir/pages/references/patterns-and-guards.md b/lib/elixir/pages/references/patterns-and-guards.md
index 551fe29b6b1..2029a495ca4 100644
--- a/lib/elixir/pages/references/patterns-and-guards.md
+++ b/lib/elixir/pages/references/patterns-and-guards.md
@@ -83,6 +83,19 @@ iex> _
 ** (CompileError) iex:3: invalid use of _
 ```
 
+A pinned value represents the value itself and not its – even if syntatically equal – pattern. The right hand side is compared to be equal to the pinned value:
+
+```iex
+iex> x = %{}
+%{}
+iex> {:ok, %{}} = {:ok, %{a: 13}}
+{:ok, %{a: 13}}
+iex> {:ok, ^x} = {:ok, %{a: 13}}
+** (MatchError) no match of right hand side value: {:ok, %{a: 13}}
+    (stdlib 6.2) erl_eval.erl:667: :erl_eval.expr/6
+    iex:2: (file)
+```
+
 ### Literals (numbers and atoms)
 
 Atoms and numbers (integers and floats) can appear in patterns and they are always represented as is. For example, an atom will only match an atom if they are the same atom:
diff --git a/lib/elixir/scripts/elixir_docs.exs b/lib/elixir/scripts/elixir_docs.exs
index 74571ac0f8d..a25cb8830ff 100644
--- a/lib/elixir/scripts/elixir_docs.exs
+++ b/lib/elixir/scripts/elixir_docs.exs
@@ -17,17 +17,17 @@ canonical = System.fetch_env!("CANONICAL")
     "lib/elixir/pages/getting-started/binaries-strings-and-charlists.md",
     "lib/elixir/pages/getting-started/keywords-and-maps.md",
     "lib/elixir/pages/getting-started/modules-and-functions.md",
-    "lib/elixir/pages/getting-started/recursion.md",
-    "lib/elixir/pages/getting-started/enumerable-and-streams.md",
-    "lib/elixir/pages/getting-started/processes.md",
-    "lib/elixir/pages/getting-started/io-and-the-file-system.md",
     "lib/elixir/pages/getting-started/alias-require-and-import.md",
     "lib/elixir/pages/getting-started/module-attributes.md",
     "lib/elixir/pages/getting-started/structs.md",
-    "lib/elixir/pages/getting-started/protocols.md",
+    "lib/elixir/pages/getting-started/recursion.md",
+    "lib/elixir/pages/getting-started/enumerable-and-streams.md",
     "lib/elixir/pages/getting-started/comprehensions.md",
+    "lib/elixir/pages/getting-started/protocols.md",
     "lib/elixir/pages/getting-started/sigils.md",
     "lib/elixir/pages/getting-started/try-catch-and-rescue.md",
+    "lib/elixir/pages/getting-started/processes.md",
+    "lib/elixir/pages/getting-started/io-and-the-file-system.md",
     "lib/elixir/pages/getting-started/writing-documentation.md",
     "lib/elixir/pages/getting-started/optional-syntax.md",
     "lib/elixir/pages/getting-started/erlang-libraries.md",
@@ -49,15 +49,13 @@ canonical = System.fetch_env!("CANONICAL")
     "lib/elixir/pages/references/unicode-syntax.md",
     "lib/elixir/pages/mix-and-otp/introduction-to-mix.md",
     "lib/elixir/pages/mix-and-otp/agents.md",
-    "lib/elixir/pages/mix-and-otp/genservers.md",
     "lib/elixir/pages/mix-and-otp/supervisor-and-application.md",
     "lib/elixir/pages/mix-and-otp/dynamic-supervisor.md",
-    "lib/elixir/pages/mix-and-otp/erlang-term-storage.md",
-    "lib/elixir/pages/mix-and-otp/dependencies-and-umbrella-projects.md",
     "lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md",
     "lib/elixir/pages/mix-and-otp/docs-tests-and-with.md",
-    "lib/elixir/pages/mix-and-otp/distributed-tasks.md",
-    "lib/elixir/pages/mix-and-otp/config-and-releases.md",
+    "lib/elixir/pages/mix-and-otp/config-and-distribution.md",
+    "lib/elixir/pages/mix-and-otp/genservers.md",
+    "lib/elixir/pages/mix-and-otp/releases.md",
     "lib/elixir/pages/meta-programming/quote-and-unquote.md",
     "lib/elixir/pages/meta-programming/macros.md",
     "lib/elixir/pages/meta-programming/domain-specific-languages.md",
@@ -73,9 +71,9 @@ canonical = System.fetch_env!("CANONICAL")
   groups_for_extras: [
     "Getting started": ~r"pages/getting-started/.*\.md$",
     Cheatsheets: ~r"pages/cheatsheets/.*\.cheatmd$",
+    "Mix & OTP": ~r"pages/mix-and-otp/.*\.md$",
     "Anti-patterns": ~r"pages/anti-patterns/.*\.md$",
     "Meta-programming": ~r"pages/meta-programming/.*\.md$",
-    "Mix & OTP": ~r"pages/mix-and-otp/.*\.md$",
     References: ~r"pages/references/.*\.md$"
   ],
   groups_for_docs: [
diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl
index e6769e2ba80..346097e9a76 100644
--- a/lib/elixir/src/elixir_dispatch.erl
+++ b/lib/elixir/src/elixir_dispatch.erl
@@ -172,7 +172,11 @@ expand_import(Meta, Name, Arity, E, Extra, AllowLocals, Trace) ->
       do_expand_import(Dispatch, Meta, Name, Arity, Module, E, Trace);
 
     _ ->
-      Local = AllowLocals andalso elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E),
+      Local = case AllowLocals of
+        false -> false;
+        true  -> elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E);
+        Fun when is_function(Fun, 0) -> Fun()
+      end,
 
       case Dispatch of
         %% There is a local and an import. This is a conflict unless
diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl
index c4f7bb92f29..5a012050ff7 100644
--- a/lib/elixir/src/elixir_erl.erl
+++ b/lib/elixir/src/elixir_erl.erl
@@ -182,12 +182,7 @@ 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;
-    (debug_info) -> false;
-    (_) -> true
-  end, Opts),
-
+  FilteredOpts = proplists:delete(debug_info, proplists:delete(no_warn_undefined, Opts)),
   Location = {elixir_utils:characters_to_list(RelativeFile), Line},
 
   Prefix = [{attribute, Line, file, Location},
@@ -598,15 +593,12 @@ get_moduledoc_meta(Set) ->
 
 get_docs(Set, Module, Definitions, Kind) ->
   [{Key,
-    maybe_generated(erl_anno:new(Line), Ctx),
+    erl_anno:new(Line),
     [signature_to_binary(Module, Name, Signature)],
     doc_value(Doc, Name),
     Meta#{source_annos => [?ann(DefinitionMeta)]}
    } || {{Name, Arity}, DefinitionMeta} <- Definitions,
-        {Key, Ctx, Line, Signature, Doc, Meta} <- ets:lookup(Set, {Kind, Name, Arity})].
-
-maybe_generated(Ann, nil) -> Ann;
-maybe_generated(Ann, _Ctx) -> erl_anno:set_generated(true, Ann).
+        {Key, _Ctx, Line, Signature, Doc, Meta} <- ets:lookup(Set, {Kind, Name, Arity})].
 
 get_callback_docs(Set, Callbacks) ->
   [{Key,
@@ -670,7 +662,7 @@ checker_chunk(Map, Def, ChunkOpts) ->
     end
   },
 
-  [{<<"ExCk">>, term_to_binary({elixir_checker_v1, Contents}, ChunkOpts)}].
+  [{<<"ExCk">>, term_to_binary({elixir_checker_v2, Contents}, ChunkOpts)}].
 
 prepend_behaviour_info(true, Def) -> [{{behaviour_info, 1}, []} | Def];
 prepend_behaviour_info(false, Def) -> Def.
diff --git a/lib/elixir/src/elixir_erl_pass.erl b/lib/elixir/src/elixir_erl_pass.erl
index 9a65052fe97..1fc7bac8d4f 100644
--- a/lib/elixir/src/elixir_erl_pass.erl
+++ b/lib/elixir/src/elixir_erl_pass.erl
@@ -628,7 +628,8 @@ translate_remote(Left, Right, Meta, Args, S) ->
         [TOne, TTwo] -> {{op, Ann, Right, TOne, TTwo}, SA}
       end;
     {inline_pure, Result} ->
-      translate(Result, Ann, S);
+      Generated = erl_anno:set_generated(true, Ann),
+      translate(Result, Generated, S);
     {inline_args, NewArgs} ->
       {TLeft, SL} = translate(Left, Ann, S),
       {TArgs, SA} = translate_args(NewArgs, Ann, SL),
@@ -717,7 +718,7 @@ generate_struct_name_guard([Field | Rest], Acc, S) ->
 %% TODO: Make this a runtime error on Elixir v2.0
 no_parens_remote(nil, _Key) -> {error, {badmap, nil}};
 no_parens_remote(false, _Key) -> {error, {badmap, false}};
-no_parens_remote(true, _Key) -> {error, {badmap, false}};
+no_parens_remote(true, _Key) -> {error, {badmap, true}};
 no_parens_remote(Atom, Fun) when is_atom(Atom) ->
   Message = fun() ->
     io_lib:format(
diff --git a/lib/elixir/src/elixir_fn.erl b/lib/elixir/src/elixir_fn.erl
index d598868f90b..9a2a436d67f 100644
--- a/lib/elixir/src/elixir_fn.erl
+++ b/lib/elixir/src/elixir_fn.erl
@@ -131,7 +131,7 @@ capture_expr(Meta, Expr, S, E, Escaped, ArgsType) ->
       {expand, Fn, S, E};
     {EExpr, EDict} ->
       EVars = validate(Meta, EDict, 1, E),
-      Fn = {fn, Meta, [{'->', Meta, [EVars, EExpr]}]},
+      Fn = {fn, [{capture, true} | Meta], [{'->', Meta, [EVars, EExpr]}]},
       {expand, Fn, S, E}
   end.
 
@@ -154,7 +154,7 @@ escape({'&', Meta, [Pos]}, E, Dict) when is_integer(Pos), Pos > 0 ->
       {Var, Dict};
     error ->
       Next = elixir_module:next_counter(?key(E, module)),
-      Var = {capture, [{counter, Next} | Meta], nil},
+      Var = {capture, [{counter, Next}, {capture, Pos} | Meta], nil},
       {Var, orddict:store(Pos, Var, Dict)}
   end;
 escape({'&', Meta, [Pos]}, E, _Dict) when is_integer(Pos) ->
diff --git a/lib/elixir/src/elixir_map.erl b/lib/elixir/src/elixir_map.erl
index afc2e76a249..3b501493f38 100644
--- a/lib/elixir/src/elixir_map.erl
+++ b/lib/elixir/src/elixir_map.erl
@@ -18,7 +18,7 @@ expand_map(Meta, Args, S, E) ->
   validate_kv(Meta, EArgs, Args, E),
   {{'%{}', Meta, EArgs}, SE, EE}.
 
-expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs} = Right, S, #{context := Context} = E) ->
+expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} = E) ->
   CleanMapArgs = delete_struct_key(Meta, MapArgs, E),
   {[ELeft, ERight], SE, EE} = elixir_expand:expand_args([Left, {'%{}', MapMeta, CleanMapArgs}], S, E),
 
@@ -29,7 +29,6 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs} = Right, S, #{context := Con
           %% The update syntax for structs is deprecated,
           %% so we return only the update syntax downstream.
           %% TODO: Remove me on Elixir v2.0
-          file_warn(MapMeta, ?key(E, file), ?MODULE, {deprecated_update, ELeft, Right}),
           _ = load_struct_info(Meta, ELeft, Assocs, EE),
           {{'%', Meta, [ELeft, ERight]}, SE, EE};
 
@@ -303,9 +302,4 @@ format_error({invalid_key_for_struct, Key}) ->
   io_lib:format("invalid key ~ts for struct, struct keys must be atoms, got: ",
                 ['Elixir.Macro':to_string(Key)]);
 format_error(ignored_struct_key_in_struct) ->
-  "key :__struct__ is ignored when using structs";
-format_error({deprecated_update, Struct, MapUpdate}) ->
-  io_lib:format("the struct update syntax is deprecated:\n\n~ts\n\n"
-                "Instead, prefer to pattern matching on structs when the variable is first defined and "
-                "use the regular map update syntax instead:\n\n~ts\n",
-                ['Elixir.Macro':to_string({'%', [], [Struct, MapUpdate]}), 'Elixir.Macro':to_string(MapUpdate)]).
+  "key :__struct__ is ignored when using structs".
\ No newline at end of file
diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl
index 4f3bc99a5af..70fa6bd7e7f 100644
--- a/lib/elixir/src/elixir_parser.yrl
+++ b/lib/elixir/src/elixir_parser.yrl
@@ -1016,7 +1016,13 @@ build_bin_string({bin_string, Location, Args}, ExtraMeta) ->
   {'<<>>', Meta, string_parts(Args)}.
 
 build_list_string({list_string, _Location, [H]} = Token, ExtraMeta) when is_binary(H) ->
-  handle_literal(elixir_utils:characters_to_list(H), Token, ExtraMeta);
+  try
+    List = elixir_utils:characters_to_list(H),
+    handle_literal(List, Token, ExtraMeta)
+  catch
+    error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+      return_error(?location(Token), elixir_utils:characters_to_list(Message), "'")
+  end;
 build_list_string({list_string, Location, Args}, ExtraMeta) ->
   Meta = meta_from_location(Location),
   MetaWithExtra =
diff --git a/lib/elixir/src/elixir_rewrite.erl b/lib/elixir/src/elixir_rewrite.erl
index 04ed0206a3a..697a1b9170c 100644
--- a/lib/elixir/src/elixir_rewrite.erl
+++ b/lib/elixir/src/elixir_rewrite.erl
@@ -355,15 +355,23 @@ allowed_guard(Right, Arity) ->
   erl_internal:guard_bif(Right, Arity) orelse elixir_utils:guard_op(Right, Arity).
 
 format_error({invalid_guard, Receiver, Right, Arity, Context}) ->
-  io_lib:format("cannot invoke remote function ~ts.~ts/~B inside a ~ts",
+  io_lib:format(cannot_invoke_or_maybe_require(Receiver, Right, Arity) ++ " ~ts.~ts/~B inside a ~ts",
                 ['Elixir.Macro':to_string(Receiver), Right, Arity, Context]);
 format_error({invalid_match, Receiver, Right, Arity}) ->
-  io_lib:format("cannot invoke remote function ~ts.~ts/~B inside a match",
+  io_lib:format(cannot_invoke_or_maybe_require(Receiver, Right, Arity) ++ " ~ts.~ts/~B inside a match",
                 ['Elixir.Macro':to_string(Receiver), Right, Arity]);
 format_error({invalid_match_append, Arg}) ->
   io_lib:format("invalid argument for ++ operator inside a match, expected a literal proper list, got: ~ts",
                 ['Elixir.Macro':to_string(Arg)]).
 
+cannot_invoke_or_maybe_require(Receiver, Fun, Arity) ->
+  try
+    true = lists:member({Fun, Arity}, Receiver:'__info__'(macros)),
+    ["you must require the module", 'Elixir.Macro':to_string(Receiver), " before invoking macro"]
+  catch
+    _:_ -> "cannot invoke remote function"
+  end.
+
 is_always_string({{'.', _, [Module, Function]}, _, Args}) ->
   is_always_string(Module, Function, length(Args));
 is_always_string(Ast) ->
diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl
index e90d259454b..3c38062ec17 100644
--- a/lib/elixir/src/elixir_tokenizer.erl
+++ b/lib/elixir/src/elixir_tokenizer.erl
@@ -948,17 +948,22 @@ handle_dot([$., H | T] = Original, Line, Column, DotInfo, BaseScope, Tokens) whe
           InterScope
       end,
 
-      {ok, [UnescapedPart]} = unescape_tokens([Part], Line, Column, NewScope),
+      case unescape_tokens([Part], Line, Column, NewScope) of
+        {ok, [UnescapedPart]} ->
+          case unsafe_to_atom(UnescapedPart, Line, Column, NewScope) of
+            {ok, Atom} ->
+              Token = check_call_identifier(Line, Column, H, Atom, Rest),
+              TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens),
+              tokenize(Rest, NewLine, NewColumn, NewScope, [Token | TokensSoFar]);
 
-      case unsafe_to_atom(UnescapedPart, Line, Column, NewScope) of
-        {ok, Atom} ->
-          Token = check_call_identifier(Line, Column, H, Atom, Rest),
-          TokensSoFar = add_token_with_eol({'.', DotInfo}, Tokens),
-          tokenize(Rest, NewLine, NewColumn, NewScope, [Token | TokensSoFar]);
+            {error, Reason} ->
+              error(Reason, Original, NewScope, Tokens)
+          end;
 
         {error, Reason} ->
           error(Reason, Original, NewScope, Tokens)
       end;
+
     {_NewLine, _NewColumn, _Parts, Rest, NewScope} ->
       Message = "interpolation is not allowed when calling function/macro. Found interpolation in a call starting with: ",
       error({?LOC(Line, Column), Message, [H]}, Rest, NewScope, Tokens);
@@ -1018,32 +1023,94 @@ is_unnecessary_quote(_Parts, _Scope) ->
 unsafe_to_atom(Part, Line, Column, #elixir_tokenizer{}) when
     is_binary(Part) andalso byte_size(Part) > 255;
     is_list(Part) andalso length(Part) > 255 ->
-  {error, {?LOC(Line, Column), "atom length must be less than system limit: ", elixir_utils:characters_to_list(Part)}};
+  try
+    PartList = elixir_utils:characters_to_list(Part),
+    {error, {?LOC(Line, Column), "atom length must be less than system limit: ", PartList}}
+  catch
+    error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+      {error, {?LOC(Line, Column), "invalid encoding in atom: ", elixir_utils:characters_to_list(Message)}}
+  end;
 unsafe_to_atom(Part, Line, Column, #elixir_tokenizer{static_atoms_encoder=StaticAtomsEncoder}) when
     is_function(StaticAtomsEncoder) ->
-  Value = elixir_utils:characters_to_binary(Part),
-  case StaticAtomsEncoder(Value, [{line, Line}, {column, Column}]) of
-    {ok, Term} ->
-      {ok, Term};
-    {error, Reason} when is_binary(Reason) ->
-      {error, {?LOC(Line, Column), elixir_utils:characters_to_list(Reason) ++ ": ", elixir_utils:characters_to_list(Part)}}
+  EncodeResult = try
+    ValueEncBin = elixir_utils:characters_to_binary(Part),
+    ValueEncList = elixir_utils:characters_to_list(Part),
+    {ok, ValueEncBin, ValueEncList}
+  catch
+    error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+      {error, {?LOC(Line, Column), "invalid encoding in atom: ", elixir_utils:characters_to_list(Message)}}
+  end,
+
+  case EncodeResult of
+    {ok, Value, ValueList} ->
+      case StaticAtomsEncoder(Value, [{line, Line}, {column, Column}]) of
+        {ok, Term} ->
+          {ok, Term};
+        {error, Reason} when is_binary(Reason) ->
+          {error, {?LOC(Line, Column), elixir_utils:characters_to_list(Reason) ++ ": ", ValueList}}
+      end;
+    EncError -> EncError
   end;
 unsafe_to_atom(Binary, Line, Column, #elixir_tokenizer{existing_atoms_only=true}) when is_binary(Binary) ->
   try
     {ok, binary_to_existing_atom(Binary, utf8)}
   catch
-    error:badarg -> {error, {?LOC(Line, Column), "unsafe atom does not exist: ", elixir_utils:characters_to_list(Binary)}}
+    error:badarg ->
+      % Check if it's a UTF-8 issue by trying to convert to list
+      try
+        List = elixir_utils:characters_to_list(Binary),
+        % If we get here, it's not a UTF-8 issue
+        {error, {?LOC(Line, Column), "unsafe atom does not exist: ", List}}
+      catch
+        error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+          {error, {?LOC(Line, Column), "invalid encoding in atom: ", elixir_utils:characters_to_list(Message)}}
+      end
+  end;
+unsafe_to_atom(Binary, Line, Column, #elixir_tokenizer{}) when is_binary(Binary) ->
+  try
+    {ok, binary_to_atom(Binary, utf8)}
+  catch
+    error:badarg ->
+      % Try to convert using elixir_utils to get proper UnicodeConversionError
+      try
+        List = elixir_utils:characters_to_list(Binary),
+        % If we get here, it's not a UTF-8 issue, so it's some other badarg
+        {error, {?LOC(Line, Column), "invalid atom: ", List}}
+      catch
+        error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+          {error, {?LOC(Line, Column), "invalid encoding in atom: ", elixir_utils:characters_to_list(Message)}}
+      end
   end;
-unsafe_to_atom(Binary, _Line, _Column, #elixir_tokenizer{}) when is_binary(Binary) ->
-  {ok, binary_to_atom(Binary, utf8)};
 unsafe_to_atom(List, Line, Column, #elixir_tokenizer{existing_atoms_only=true}) when is_list(List) ->
   try
     {ok, list_to_existing_atom(List)}
   catch
-    error:badarg -> {error, {?LOC(Line, Column), "unsafe atom does not exist: ", List}}
+    error:badarg ->
+      % Try to convert using elixir_utils to get proper UnicodeConversionError
+      try
+        elixir_utils:characters_to_binary(List),
+        % If we get here, it's not a UTF-8 issue
+        {error, {?LOC(Line, Column), "unsafe atom does not exist: ", List}}
+      catch
+        error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+          {error, {?LOC(Line, Column), "invalid encoding in atom: ", elixir_utils:characters_to_list(Message)}}
+      end
   end;
-unsafe_to_atom(List, _Line, _Column, #elixir_tokenizer{}) when is_list(List) ->
-  {ok, list_to_atom(List)}.
+unsafe_to_atom(List, Line, Column, #elixir_tokenizer{}) when is_list(List) ->
+  try
+    {ok, list_to_atom(List)}
+  catch
+    error:badarg ->
+      % Try to convert using elixir_utils to get proper UnicodeConversionError
+      try
+        elixir_utils:characters_to_binary(List),
+        % If we get here, it's not a UTF-8 issue, so it's some other badarg
+        {error, {?LOC(Line, Column), "invalid atom: ", List}}
+      catch
+        error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+          {error, {?LOC(Line, Column), "invalid encoding in atom: ", elixir_utils:characters_to_list(Message)}}
+      end
+  end.
 
 collect_modifiers([H | T], Buffer) when ?is_downcase(H) or ?is_upcase(H) or ?is_digit(H) ->
   collect_modifiers(T, [H | Buffer]);
@@ -1066,7 +1133,12 @@ extract_heredoc_with_interpolation(Line, Column, Scope, Interpol, T, H) ->
           {Parts1, {ShouldWarn, _}} = lists:mapfoldl(Fun, {false, Line}, Parts0),
           Parts2 = extract_heredoc_head(Parts1),
           NewScope = maybe_heredoc_warn(ShouldWarn, Column, InterScope, H),
-          {ok, NewLine, NewColumn, tokens_to_binary(Parts2), Rest, NewScope};
+          try
+            {ok, NewLine, NewColumn, tokens_to_binary(Parts2), Rest, NewScope}
+          catch
+            error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+              {error, interpolation_format(Message, " (for heredoc starting at line ~B)", [Line], Line, Column, [H, H, H], [H, H, H])}
+          end;
 
         {error, Reason} ->
           {error, interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line], Line, Column, [H, H, H], [H, H, H])}
@@ -1137,8 +1209,13 @@ unescape_tokens(Tokens, Line, Column, #elixir_tokenizer{unescape=true}) ->
     {error, Message, Token} ->
       {error, {?LOC(Line, Column), Message ++ ". Syntax error after: ", Token}}
   end;
-unescape_tokens(Tokens, _Line, _Column, #elixir_tokenizer{unescape=false}) ->
-  {ok, tokens_to_binary(Tokens)}.
+unescape_tokens(Tokens, Line, Column, #elixir_tokenizer{unescape=false}) ->
+  try
+    {ok, tokens_to_binary(Tokens)}
+  catch
+    error:#{'__struct__' := 'Elixir.UnicodeConversionError', message := Message} ->
+      {error, {?LOC(Line, Column), "invalid encoding in tokens: ", elixir_utils:characters_to_list(Message)}}
+  end.
 
 tokens_to_binary(Tokens) ->
   [if is_list(Token) -> elixir_utils:characters_to_binary(Token); true -> Token end
diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs
index cd046019674..497ca3ae0bf 100644
--- a/lib/elixir/test/elixir/code_fragment_test.exs
+++ b/lib/elixir/test/elixir/code_fragment_test.exs
@@ -127,8 +127,6 @@ defmodule CodeFragmentTest do
       assert CF.cursor_context("hello(\t") == {:local_call, ~c"hello"}
       assert CF.cursor_context("hello(\n") == {:local_call, ~c"hello"}
       assert CF.cursor_context("hello(\r\n") == {:local_call, ~c"hello"}
-      assert CF.cursor_context("...(") == {:local_call, ~c"..."}
-      assert CF.cursor_context("...(\s") == {:local_call, ~c"..."}
     end
 
     test "dot_arity" do
@@ -304,6 +302,7 @@ defmodule CodeFragmentTest do
     end
 
     test "operators" do
+      assert CF.cursor_context("/") == {:operator, ~c"/"}
       assert CF.cursor_context("+") == {:operator, ~c"+"}
       assert CF.cursor_context("++") == {:operator, ~c"++"}
       assert CF.cursor_context("!") == {:operator, ~c"!"}
@@ -326,6 +325,11 @@ defmodule CodeFragmentTest do
       assert CF.cursor_context("<~> ") == {:operator_call, ~c"<~>"}
       assert CF.cursor_context(":: ") == {:operator_call, ~c"::"}
 
+      assert CF.cursor_context("...(") == {:operator_call, ~c"..."}
+      assert CF.cursor_context("...(\s") == {:operator_call, ~c"..."}
+      assert CF.cursor_context("+(") == {:operator_call, ~c"+"}
+      assert CF.cursor_context("++(\s") == {:operator_call, ~c"++"}
+
       assert CF.cursor_context("+/") == {:operator_arity, ~c"+"}
       assert CF.cursor_context("++/") == {:operator_arity, ~c"++"}
       assert CF.cursor_context("!/") == {:operator_arity, ~c"!"}
@@ -358,6 +362,15 @@ defmodule CodeFragmentTest do
       assert CF.cursor_context("~r/") == :none
       assert CF.cursor_context("~r<") == :none
 
+      assert CF.cursor_context("~r''") == :none
+      assert CF.cursor_context("~r' '") == :none
+      assert CF.cursor_context("~r'foo'") == :none
+
+      # The slash is used in sigils, arities, and operators, so there is ambiguity
+      assert CF.cursor_context("~r//") == {:operator, ~c"/"}
+      assert CF.cursor_context("~r/ /") == {:operator, ~c"/"}
+      assert CF.cursor_context("~r/foo/") == {:local_arity, ~c"foo"}
+
       assert CF.cursor_context("~R") == {:sigil, ~c"R"}
       assert CF.cursor_context("~R/") == :none
       assert CF.cursor_context("~R<") == :none
@@ -562,14 +575,20 @@ defmodule CodeFragmentTest do
       assert CF.surround_context("안녕_세상", {1, 6}) == :none
 
       # Keywords are not local or var
-      for keyword <- ~w(do end after catch else rescue fn true false nil)c do
-        keyword_length = length(keyword) + 1
-
-        assert %{
-                 context: {:keyword, ^keyword},
+      for keyword <- ~w(do end after catch else rescue fn true false nil)c,
+          length = length(keyword),
+          i <- 1..length do
+        assert CF.surround_context(keyword, {1, i}) == %{
+                 context: {:keyword, keyword},
                  begin: {1, 1},
-                 end: {1, ^keyword_length}
-               } = CF.surround_context(keyword, {1, 1})
+                 end: {1, length + 1}
+               }
+
+        assert CF.surround_context(~c"Foo " ++ keyword, {1, 4 + i}) == %{
+                 context: {:keyword, keyword},
+                 begin: {1, 5},
+                 end: {1, length + 5}
+               }
       end
     end
 
@@ -664,6 +683,12 @@ defmodule CodeFragmentTest do
                  begin: {1, 1},
                  end: {1, byte_size(op) + 1}
                }
+
+        assert CF.surround_context("Foo #{op}", {1, 4 + i}) == %{
+                 context: {:operator, String.to_charlist(op)},
+                 begin: {1, 5},
+                 end: {1, byte_size(op) + 5}
+               }
       end
     end
 
diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs
index be0052d7513..23bc49f0acb 100644
--- a/lib/elixir/test/elixir/code_test.exs
+++ b/lib/elixir/test/elixir/code_test.exs
@@ -514,6 +514,92 @@ defmodule CodeTest do
              }
   end
 
+  test "string_to_quoted handles unescape errors properly" do
+    # Test invalid hex escape character
+    assert {:error, {meta, message, token}} = Code.string_to_quoted("a.'\\xg'")
+
+    assert meta[:line] == 1
+    assert meta[:column] == 3
+
+    assert message ==
+             "invalid hex escape character, expected \\xHH where H is a hexadecimal digit. Syntax error after: "
+
+    assert token == "\\x"
+
+    # Test invalid Unicode escape character
+    assert {:error, {meta2, message2, token2}} = Code.string_to_quoted("a.'\\ug'")
+
+    assert meta2[:line] == 1
+    assert meta2[:column] == 3
+
+    assert message2 ==
+             "invalid Unicode escape character, expected \\uHHHH or \\u{H*} where H is a hexadecimal digit. Syntax error after: "
+
+    assert token2 == "\\u"
+
+    # Test invalid Unicode code point (surrogate pair)
+    assert {:error, {meta3, message3, token3}} = Code.string_to_quoted("a.'\\u{D800}'")
+
+    assert meta3[:line] == 1
+    assert meta3[:column] == 3
+
+    assert message3 == "invalid or reserved Unicode code point \\u{D800}. Syntax error after: "
+    assert token3 == "\\u"
+
+    # Test Unicode code point beyond valid range
+    assert {:error, {meta4, message4, token4}} = Code.string_to_quoted("a.'\\u{110000}'")
+
+    assert meta4[:line] == 1
+    assert meta4[:column] == 3
+
+    assert message4 == "invalid or reserved Unicode code point \\u{110000}. Syntax error after: "
+    assert token4 == "\\u"
+  end
+
+  test "string_to_quoted returns error for invalid UTF-8 in strings" do
+    invalid_utf8_cases = [
+      # charlist
+      "'\\xFF'",
+      # charlist heredoc
+      "'''\n\\xFF\\\n'''"
+    ]
+
+    for code <- invalid_utf8_cases do
+      assert {:error, {_, message, _}} = Code.string_to_quoted(code)
+      assert message =~ "invalid encoding starting at <<255>>"
+    end
+  end
+
+  test "string_to_quoted returns error for invalid UTF-8 in quoted atoms and function calls" do
+    invalid_utf8_cases = [
+      # charlist
+      # ~S{'\xFF'},
+      # charlist heredoc
+      # ~s{'''\n\xFF\n'''},
+      # Quoted atom
+      ~S{:"\xFF"},
+      ~S{:'\xFF'},
+      # Quoted keyword identifier
+      ~S{["\xFF": 1]},
+      ~S{['\xFF': 1]},
+      # Quoted function call
+      ~S{foo."\xFF"()},
+      ~S{foo.'\xFF'()}
+    ]
+
+    for code <- invalid_utf8_cases do
+      assert {:error, {_, message, detail}} = Code.string_to_quoted(code)
+      assert message =~ "invalid encoding in atom: "
+      assert detail =~ "invalid encoding starting at <<255>>"
+
+      assert {:error, {_, message, detail}} =
+               Code.string_to_quoted(code, existing_atoms_only: true)
+
+      assert message =~ "invalid encoding in atom: "
+      assert detail =~ "invalid encoding starting at <<255>>"
+    end
+  end
+
   @tag :requires_source
   test "compile source" do
     assert __MODULE__.__info__(:compile)[:source] == String.to_charlist(__ENV__.file)
diff --git a/lib/elixir/test/elixir/fixtures/dialyzer/opaqueness.ex b/lib/elixir/test/elixir/fixtures/dialyzer/opaqueness.ex
new file mode 100644
index 00000000000..12857f515fe
--- /dev/null
+++ b/lib/elixir/test/elixir/fixtures/dialyzer/opaqueness.ex
@@ -0,0 +1,17 @@
+defmodule Dialyzer.Opaqueness do
+  @spec bar(MapSet.t()) :: term()
+  def bar(set) do
+    set
+  end
+
+  def foo() do
+    # inlining of literals should not violate opaqueness check
+    bar(MapSet.new([1, 2, 3]))
+  end
+
+  # Task.Supervisor returns a Task.t() containing an opaque Task.ref()
+  @spec run_task() :: Task.t()
+  def run_task do
+    Task.Supervisor.async(SupervisorName, fn -> :ok end)
+  end
+end
diff --git a/lib/elixir/test/elixir/kernel/dialyzer_test.exs b/lib/elixir/test/elixir/kernel/dialyzer_test.exs
index ba5cbfb5bce..01aa977ecae 100644
--- a/lib/elixir/test/elixir/kernel/dialyzer_test.exs
+++ b/lib/elixir/test/elixir/kernel/dialyzer_test.exs
@@ -32,6 +32,7 @@ defmodule Kernel.DialyzerTest do
       :elixir_env,
       :elixir_erl_pass,
       :maps,
+      :sets,
       ArgumentError,
       Atom,
       Code,
@@ -45,10 +46,13 @@ defmodule Kernel.DialyzerTest do
       List,
       Macro,
       Macro.Env,
+      MapSet,
       Module,
       Protocol,
       String,
-      String.Chars
+      String.Chars,
+      Task,
+      Task.Supervisor
     ]
 
     files = Enum.map(mods, &:code.which/1)
@@ -176,6 +180,11 @@ defmodule Kernel.DialyzerTest do
     assert_dialyze_no_warnings!(context)
   end
 
+  test "no warning due to opaqueness edge cases", context do
+    copy_beam!(context, Dialyzer.Opaqueness)
+    assert_dialyze_no_warnings!(context)
+  end
+
   defp copy_beam!(context, module) do
     name = "#{module}.beam"
     File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name))
diff --git a/lib/elixir/test/elixir/kernel/docs_test.exs b/lib/elixir/test/elixir/kernel/docs_test.exs
index 1926eece0bf..fde228722a4 100644
--- a/lib/elixir/test/elixir/kernel/docs_test.exs
+++ b/lib/elixir/test/elixir/kernel/docs_test.exs
@@ -430,7 +430,7 @@ defmodule Kernel.DocsTest do
     write_beam(
       defmodule ToBeUsed do
         defmacro __using__(_) do
-          quote do
+          quote generated: true do
             @doc "Hello"
             def foo, do: :bar
           end
@@ -446,11 +446,12 @@ defmodule Kernel.DocsTest do
 
     {:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(WillBeUsing)
 
-    location = :erl_anno.new(line + 15)
+    doc_anno = :erl_anno.new(line + 15)
+    source_anno = :erl_anno.set_generated(true, :erl_anno.new(line + 15))
 
     assert [
-             {{:function, :foo, 0}, [generated: true, location: ^location], ["foo()"],
-              %{"en" => "Hello"}, %{}}
+             {{:function, :foo, 0}, ^doc_anno, ["foo()"], %{"en" => "Hello"},
+              %{source_annos: [^source_anno]}}
            ] = docs
   end
 end
diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs
index 82169bcaa4f..9f77db07c13 100644
--- a/lib/elixir/test/elixir/kernel/expansion_test.exs
+++ b/lib/elixir/test/elixir/kernel/expansion_test.exs
@@ -806,6 +806,15 @@ defmodule Kernel.ExpansionTest do
       end)
     end
 
+    test "in guards with macros" do
+      message =
+        ~r"you must require the moduleInteger before invoking macro Integer.is_even/1 inside a guard"
+
+      assert_compile_error(message, fn ->
+        expand(quote(do: fn arg when Integer.is_even(arg) -> arg end))
+      end)
+    end
+
     test "in guards with bitstrings" do
       message = ~r"cannot invoke remote function String.Chars.to_string/1 inside a guard"
 
@@ -1203,8 +1212,14 @@ defmodule Kernel.ExpansionTest do
 
     test "keeps position meta on & variables" do
       assert expand(Code.string_to_quoted!("& &1")) |> clean_meta([:counter]) ==
-               {:fn, [{:line, 1}],
-                [{:->, [{:line, 1}], [[{:capture, [line: 1], nil}], {:capture, [line: 1], nil}]}]}
+               {:fn, [capture: true, line: 1],
+                [
+                  {:->, [line: 1],
+                   [
+                     [{:capture, [capture: 1, line: 1], nil}],
+                     {:capture, [capture: 1, line: 1], nil}
+                   ]}
+                ]}
     end
 
     test "removes no_parens when expanding 0-arity capture to fn" do
diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs
index f118456f9d0..eb0dd65930b 100644
--- a/lib/elixir/test/elixir/kernel/warning_test.exs
+++ b/lib/elixir/test/elixir/kernel/warning_test.exs
@@ -1818,6 +1818,20 @@ defmodule Kernel.WarningTest do
       purge([Sample1, Sample2, Sample3])
     end
 
+    test "invalid fun" do
+      assert_warn_eval(
+        [
+          "nofile:2: ",
+          "fun/1 is not valid in typespecs. Either specify fun() or use (... -> return) instead"
+        ],
+        """
+        defmodule InvalidFunType do
+          @type my_type :: fun(integer())
+        end
+        """
+      )
+    end
+
     test "invalid type annotations" do
       assert_warn_eval(
         [
diff --git a/lib/elixir/test/elixir/macro/env_test.exs b/lib/elixir/test/elixir/macro/env_test.exs
index 3138f542519..07e6a5b9546 100644
--- a/lib/elixir/test/elixir/macro/env_test.exs
+++ b/lib/elixir/test/elixir/macro/env_test.exs
@@ -207,6 +207,25 @@ defmodule Macro.EnvTest do
       assert fun.([generated: true], [quote(do: hello())]) == quote(generated: true, do: hello())
     end
 
+    defmacro allow_locals_example, do: :ok
+
+    test "allow_locals" do
+      {:macro, Macro.EnvTest, fun} =
+        expand_import(env(), meta(), :allow_locals_example, 0)
+
+      assert fun.([], []) == :ok
+
+      assert expand_import(env(), meta(), :allow_locals_example, 0, allow_locals: false) ==
+               {:error, :not_found}
+
+      assert expand_import(env(), meta(), :allow_locals_example, 0,
+               allow_locals: fn -> send(self(), false) end
+             ) ==
+               {:error, :not_found}
+
+      assert_received false
+    end
+
     test "with tracing and deprecations" do
       message = "MacroEnvMacros.my_deprecated_macro/1 is deprecated"
 
diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs
index e53111362cd..c3f752eea40 100644
--- a/lib/elixir/test/elixir/macro_test.exs
+++ b/lib/elixir/test/elixir/macro_test.exs
@@ -1645,31 +1645,32 @@ defmodule MacroTest do
 
   test "struct_info!/2 expands structs multiple levels deep" do
     defmodule StructBang do
+      @enforce_keys [:b]
       defstruct [:a, :b]
 
       assert Macro.struct_info!(StructBang, __ENV__) == [
-               %{field: :a, default: nil},
-               %{field: :b, default: nil}
+               %{field: :a, default: nil, required: false},
+               %{field: :b, default: nil, required: true}
              ]
 
       def within_function do
         assert Macro.struct_info!(StructBang, __ENV__) == [
-                 %{field: :a, default: nil},
-                 %{field: :b, default: nil}
+                 %{field: :a, default: nil, required: false},
+                 %{field: :b, default: nil, required: true}
                ]
       end
 
       defmodule Nested do
         assert Macro.struct_info!(StructBang, __ENV__) == [
-                 %{field: :a, default: nil},
-                 %{field: :b, default: nil}
+                 %{field: :a, default: nil, required: false},
+                 %{field: :b, default: nil, required: true}
                ]
       end
     end
 
     assert Macro.struct_info!(StructBang, __ENV__) == [
-             %{field: :a, default: nil},
-             %{field: :b, default: nil}
+             %{field: :a, default: nil, required: false},
+             %{field: :b, default: nil, required: true}
            ]
   end
 
diff --git a/lib/elixir/test/elixir/map_test.exs b/lib/elixir/test/elixir/map_test.exs
index dd1dd6c5d83..c3ee4356674 100644
--- a/lib/elixir/test/elixir/map_test.exs
+++ b/lib/elixir/test/elixir/map_test.exs
@@ -401,7 +401,7 @@ defmodule MapTest do
     end
 
     info = Macro.struct_info!(WithBitstring, __ENV__)
-    assert info == [%{default: <<255, 127::7>>, field: :bitstring}]
+    assert info == [%{default: <<255, 127::7>>, field: :bitstring, required: false}]
   end
 
   test "defstruct can only be used once in a module" do
diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs
index 76c1e14021e..8120c8f5ab8 100644
--- a/lib/elixir/test/elixir/module/types/descr_test.exs
+++ b/lib/elixir/test/elixir/module/types/descr_test.exs
@@ -16,6 +16,16 @@ defmodule Module.Types.DescrTest do
 
   import Module.Types.Descr, except: [fun: 1]
 
+  defmacrop domain_key(key), do: {:domain_key, key}
+
+  defp number(), do: union(integer(), float())
+  defp empty_tuple(), do: tuple([])
+  defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n))
+  defp tuple_of_size(n) when is_integer(n) and n >= 0, do: tuple(List.duplicate(term(), n))
+  defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type))
+  defp map_with_default(descr), do: open_map([], if_set(descr))
+  defp nil_or_type(type), do: union(type, atom([nil]))
+
   describe "union" do
     test "bitmap" do
       assert union(integer(), float()) == union(float(), integer())
@@ -101,8 +111,28 @@ defmodule Module.Types.DescrTest do
       a_integer_open = open_map(a: integer())
       assert equal?(union(closed_map(a: integer()), a_integer_open), a_integer_open)
 
-      assert difference(open_map(a: integer()), closed_map(b: boolean()))
-             |> equal?(open_map(a: integer()))
+      # Domain key types
+      atom_to_atom = open_map([{domain_key(:atom), atom()}])
+      atom_to_integer = open_map([{domain_key(:atom), integer()}])
+
+      # Test union identity and different type maps
+      assert union(atom_to_atom, atom_to_atom) == atom_to_atom
+
+      # Test subtype relationships with domain key maps
+      refute open_map([{domain_key(:atom), union(atom(), integer())}])
+             |> subtype?(union(atom_to_atom, atom_to_integer))
+
+      assert union(atom_to_atom, atom_to_integer)
+             |> subtype?(open_map([{domain_key(:atom), union(atom(), integer())}]))
+
+      # Test unions with empty and open maps
+      assert union(empty_map(), open_map([{domain_key(:integer), atom()}]))
+             |> equal?(open_map([{domain_key(:integer), atom()}]))
+
+      assert union(open_map(), open_map([{domain_key(:integer), atom()}])) == open_map()
+
+      # Test union of open map and map with domain key
+      assert union(open_map(), open_map([{domain_key(:integer), atom()}])) == open_map()
     end
 
     test "list" do
@@ -263,7 +293,64 @@ defmodule Module.Types.DescrTest do
       assert empty?(intersection(closed_map(a: integer()), closed_map(a: atom())))
     end
 
-    defp number(), do: union(integer(), float())
+    test "map with domain keys" do
+      # %{..., int => t1, atom => t2} and %{int => t3}
+      # intersection is %{int => t1 and t3, atom => none}
+      map1 = open_map([{domain_key(:integer), integer()}, {domain_key(:atom), atom()}])
+      map2 = closed_map([{domain_key(:integer), number()}])
+
+      intersection = intersection(map1, map2)
+
+      expected =
+        closed_map([{domain_key(:integer), integer()}, {domain_key(:atom), none()}])
+
+      assert equal?(intersection, expected)
+
+      # %{..., int => t1, atom => t2} and %{int => t3, pid => t4}
+      # intersection is %{int =>t1 and t3, atom => none, pid => t4}
+      map1 = open_map([{domain_key(:integer), integer()}, {domain_key(:atom), atom()}])
+      map2 = closed_map([{domain_key(:integer), float()}, {domain_key(:pid), binary()}])
+
+      intersection = intersection(map1, map2)
+
+      expected =
+        closed_map([
+          {domain_key(:integer), intersection(integer(), float())},
+          {domain_key(:atom), none()},
+          {domain_key(:pid), binary()}
+        ])
+
+      assert equal?(intersection, expected)
+
+      # %{..., int => t1, string => t3} and %{int => t4}
+      # intersection is %{int => t1 and t4, string => none}
+      map1 = open_map([{domain_key(:integer), integer()}, {domain_key(:binary), binary()}])
+      map2 = closed_map([{domain_key(:integer), float()}])
+
+      intersection = intersection(map1, map2)
+
+      assert equal?(
+               intersection,
+               closed_map([
+                 {domain_key(:integer), intersection(integer(), float())},
+                 {domain_key(:binary), none()}
+               ])
+             )
+
+      assert subtype?(empty_map(), closed_map([{domain_key(:integer), atom()}]))
+
+      t1 = closed_map([{domain_key(:integer), atom()}])
+      t2 = closed_map([{domain_key(:integer), binary()}])
+
+      assert equal?(intersection(t1, t2), empty_map())
+
+      t1 = closed_map([{domain_key(:integer), atom()}])
+      t2 = closed_map([{domain_key(:atom), term()}])
+
+      # their intersection is the empty map
+      refute empty?(intersection(t1, t2))
+      assert equal?(intersection(t1, t2), empty_map())
+    end
 
     test "list" do
       assert intersection(list(term()), list(term())) == list(term())
@@ -376,10 +463,6 @@ defmodule Module.Types.DescrTest do
       assert empty?(difference(dynamic(integer()), integer()))
     end
 
-    defp empty_tuple(), do: tuple([])
-    defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n))
-    defp tuple_of_size(n) when is_integer(n) and n >= 0, do: tuple(List.duplicate(term(), n))
-
     test "tuple" do
       assert empty?(difference(open_tuple([atom()]), open_tuple([term()])))
       refute empty?(difference(tuple(), empty_tuple()))
@@ -449,9 +532,57 @@ defmodule Module.Types.DescrTest do
              |> equal?(open_map(a: atom()))
 
       refute empty?(difference(open_map(), empty_map()))
+
+      assert difference(open_map(a: integer()), closed_map(b: boolean()))
+             |> equal?(open_map(a: integer()))
     end
 
-    defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type))
+    test "map with domain keys" do
+      # Non-overlapping domain keys
+      t1 = closed_map([{domain_key(:integer), atom()}])
+      t2 = closed_map([{domain_key(:atom), binary()}])
+      assert equal?(difference(t1, t2) |> union(empty_map()), t1)
+      assert empty?(difference(t1, t1))
+
+      # %{atom() => t1} and not %{atom() => t2} is not %{atom() => t1 and not t2}
+      t3 = closed_map([{domain_key(:integer), atom()}])
+      t4 = closed_map([{domain_key(:integer), atom([:ok])}])
+      assert subtype?(difference(t3, t4), t3)
+
+      refute difference(t3, t4)
+             |> equal?(closed_map([{domain_key(:integer), difference(atom(), atom([:ok]))}]))
+
+      # Difference with a non-domain key map
+      t5 = closed_map([{domain_key(:integer), union(atom(), integer())}])
+      t6 = closed_map(a: atom())
+      assert equal?(difference(t5, t6), t5)
+
+      # Removing atom keys from a map with defined atom keys
+      a_number = closed_map(a: number())
+      a_number_and_pids = closed_map([{:a, number()}, {domain_key(:atom), pid()}])
+      atom_to_float = closed_map([{domain_key(:atom), float()}])
+      atom_to_term = closed_map([{domain_key(:atom), term()}])
+      atom_to_pid = closed_map([{domain_key(:atom), pid()}])
+      t_diff = difference(a_number, atom_to_float)
+
+      # Removing atom keys that map to float, make the :a key point to integer only.
+      assert map_fetch(t_diff, :a) == {false, integer()}
+      # %{a => number, atom => pid} and not %{atom => float} gives numbers on :a
+      assert map_fetch(difference(a_number_and_pids, atom_to_float), :a) == {false, number()}
+
+      assert map_fetch(t_diff, :foo) == :badkey
+
+      assert subtype?(a_number, atom_to_term)
+      refute subtype?(a_number, atom_to_float)
+
+      # Removing all atom keys from map %{:a => type} means there is nothing left.
+      assert empty?(difference(a_number, atom_to_term))
+      refute empty?(intersection(atom_to_term, a_number))
+      assert empty?(intersection(atom_to_pid, a_number))
+
+      # (%{:a => number} and not %{:a => float}) is %{:a => integer}
+      assert equal?(difference(a_number, atom_to_float), closed_map(a: integer()))
+    end
 
     test "list" do
       # Basic list type differences
@@ -556,6 +687,16 @@ defmodule Module.Types.DescrTest do
 
       assert dynamic(open_map(a: union(integer(), binary()))) ==
                open_map(a: dynamic(integer()) |> union(binary()))
+
+      # For domains too
+      t1 = dynamic(open_map([{domain_key(:integer), integer()}]))
+      t2 = open_map([{domain_key(:integer), dynamic(integer())}])
+      assert t1 == t2
+
+      # if_set on dynamic fields also must work
+      t1 = dynamic(open_map(a: if_set(integer())))
+      t2 = open_map(a: if_set(dynamic(integer())))
+      assert t1 == t2
     end
   end
 
@@ -612,6 +753,27 @@ defmodule Module.Types.DescrTest do
       assert subtype?(closed_map(a: integer()), closed_map(a: if_set(integer())))
       refute subtype?(closed_map(a: if_set(term())), closed_map(a: term()))
       assert subtype?(closed_map(a: term()), closed_map(a: if_set(term())))
+
+      # With domains
+      t1 = closed_map([{domain_key(:integer), number()}])
+      t2 = closed_map([{domain_key(:integer), integer()}])
+
+      assert subtype?(t2, t1)
+
+      t1_minus_t2 = difference(t1, t2)
+      refute empty?(t1_minus_t2)
+
+      assert subtype?(map_with_default(number()), open_map())
+      t = difference(open_map(), map_with_default(number()))
+      refute empty?(t)
+      refute subtype?(open_map(), map_with_default(number()))
+      assert subtype?(map_with_default(integer()), map_with_default(number()))
+      refute subtype?(map_with_default(float()), map_with_default(atom()))
+
+      assert equal?(
+               intersection(map_with_default(number()), map_with_default(float())),
+               map_with_default(float())
+             )
     end
 
     test "list" do
@@ -668,6 +830,11 @@ defmodule Module.Types.DescrTest do
       assert subtype?(f, fun([none(), integer()], term()))
       assert subtype?(fun([none(), number()], atom()), f)
       assert subtype?(fun([tuple(), number()], atom()), f)
+
+      # (none, float -> atom) is not a subtype of (none, integer -> atom)
+      # because float has an empty intersection with integer.
+      # it's only possible to find this out by doing the
+      # intersection one by one.
       refute subtype?(fun([none(), float()], atom()), f)
       refute subtype?(fun([pid(), float()], atom()), f)
       # A function with the wrong arity is refused
@@ -761,54 +928,72 @@ defmodule Module.Types.DescrTest do
                intersection(fun([integer()], atom()), fun([float()], binary()))
     end
 
-    test "fun_from_overlapping_clauses" do
+    test "fun_from_inferred_clauses" do
       # No overlap
-      assert fun_from_overlapping_clauses([{[integer()], atom()}, {[float()], binary()}])
+      assert fun_from_inferred_clauses([{[integer()], atom()}, {[float()], binary()}])
              |> equal?(
-               fun_from_non_overlapping_clauses([{[integer()], atom()}, {[float()], binary()}])
+               intersection(
+                 fun_from_non_overlapping_clauses([{[integer()], atom()}, {[float()], binary()}]),
+                 fun([number()], dynamic())
+               )
              )
 
       # Subsets
-      assert fun_from_overlapping_clauses([{[integer()], atom()}, {[number()], binary()}])
+      assert fun_from_inferred_clauses([{[integer()], atom()}, {[number()], binary()}])
              |> equal?(
-               fun_from_non_overlapping_clauses([
-                 {[integer()], union(atom(), binary())},
-                 {[float()], binary()}
-               ])
+               intersection(
+                 fun_from_non_overlapping_clauses([
+                   {[integer()], union(atom(), binary())},
+                   {[float()], binary()}
+                 ]),
+                 fun([number()], dynamic())
+               )
              )
 
-      assert fun_from_overlapping_clauses([{[number()], binary()}, {[integer()], atom()}])
+      assert fun_from_inferred_clauses([{[number()], binary()}, {[integer()], atom()}])
              |> equal?(
-               fun_from_non_overlapping_clauses([
-                 {[integer()], union(atom(), binary())},
-                 {[float()], binary()}
-               ])
+               intersection(
+                 fun_from_non_overlapping_clauses([
+                   {[integer()], union(atom(), binary())},
+                   {[float()], binary()}
+                 ]),
+                 fun([number()], dynamic())
+               )
              )
 
       # Partial
-      assert fun_from_overlapping_clauses([
+      assert fun_from_inferred_clauses([
                {[union(integer(), pid())], atom()},
                {[union(float(), pid())], binary()}
              ])
              |> equal?(
-               fun_from_non_overlapping_clauses([
-                 {[integer()], atom()},
-                 {[float()], binary()},
-                 {[pid()], union(atom(), binary())}
-               ])
+               intersection(
+                 fun_from_non_overlapping_clauses([
+                   {[integer()], atom()},
+                   {[float()], binary()},
+                   {[pid()], union(atom(), binary())}
+                 ]),
+                 fun([union(number(), pid())], dynamic())
+               )
              )
 
       # Difference
-      assert fun_from_overlapping_clauses([
+      assert fun_from_inferred_clauses([
                {[integer(), union(pid(), atom())], atom()},
                {[number(), pid()], binary()}
              ])
              |> equal?(
-               fun_from_non_overlapping_clauses([
-                 {[float(), pid()], binary()},
-                 {[integer(), atom()], atom()},
-                 {[integer(), pid()], union(atom(), binary())}
-               ])
+               intersection(
+                 fun_from_non_overlapping_clauses([
+                   {[float(), pid()], binary()},
+                   {[integer(), atom()], atom()},
+                   {[integer(), pid()], union(atom(), binary())}
+                 ]),
+                 fun_from_non_overlapping_clauses([
+                   {[integer(), union(pid(), atom())], dynamic()},
+                   {[number(), pid()], dynamic()}
+                 ])
+               )
              )
     end
   end
@@ -890,16 +1075,14 @@ defmodule Module.Types.DescrTest do
       assert fun_apply(fun([dynamic()], term()), [dynamic()]) == {:ok, term()}
       assert fun_apply(fun([integer()], dynamic()), [integer()]) == {:ok, dynamic()}
 
-      assert fun_apply(fun([dynamic()], integer()), [dynamic()])
-             |> elem(1)
-             |> equal?(integer())
+      assert fun_apply(fun([dynamic()], integer()), [dynamic()]) ==
+               {:ok, union(integer(), dynamic())}
 
-      assert fun_apply(fun([dynamic(), atom()], float()), [dynamic(), atom()])
-             |> elem(1)
-             |> equal?(float())
+      assert fun_apply(fun([dynamic(), atom()], float()), [dynamic(), atom()]) ==
+               {:ok, union(float(), dynamic())}
 
       fun = fun([dynamic(integer())], atom())
-      assert fun_apply(fun, [dynamic(integer())]) |> elem(1) |> equal?(atom())
+      assert fun_apply(fun, [dynamic(integer())]) == {:ok, union(atom(), dynamic())}
       assert fun_apply(fun, [dynamic(number())]) == {:ok, dynamic()}
       assert fun_apply(fun, [integer()]) == {:ok, dynamic()}
       assert fun_apply(fun, [float()]) == {:badarg, [dynamic(integer())]}
@@ -1113,6 +1296,8 @@ defmodule Module.Types.DescrTest do
     test "tuple_fetch" do
       assert tuple_fetch(term(), 0) == :badtuple
       assert tuple_fetch(integer(), 0) == :badtuple
+      assert tuple_fetch(tuple([none(), atom()]), 1) == :badtuple
+      assert tuple_fetch(tuple([none()]), 0) == :badtuple
 
       assert tuple_fetch(tuple([integer(), atom()]), 0) == {false, integer()}
       assert tuple_fetch(tuple([integer(), atom()]), 1) == {false, atom()}
@@ -1124,7 +1309,7 @@ defmodule Module.Types.DescrTest do
 
       assert tuple_fetch(tuple([integer(), atom()]), -1) == :badindex
       assert tuple_fetch(empty_tuple(), 0) == :badindex
-      assert difference(tuple(), tuple()) |> tuple_fetch(0) == :badindex
+      assert difference(tuple(), tuple()) |> tuple_fetch(0) == :badtuple
 
       assert tuple([atom()]) |> difference(empty_tuple()) |> tuple_fetch(0) ==
                {false, atom()}
@@ -1149,7 +1334,7 @@ defmodule Module.Types.DescrTest do
 
       assert tuple([integer(), atom(), integer()])
              |> difference(tuple([integer(), term(), integer()]))
-             |> tuple_fetch(1) == :badindex
+             |> tuple_fetch(1) == :badtuple
 
       assert tuple([integer(), atom(), integer()])
              |> difference(tuple([integer(), term(), atom()]))
@@ -1177,6 +1362,7 @@ defmodule Module.Types.DescrTest do
       assert tuple_delete_at(empty_tuple(), 0) == :badindex
       assert tuple_delete_at(integer(), 0) == :badtuple
       assert tuple_delete_at(term(), 0) == :badtuple
+      assert tuple_delete_at(tuple([none()]), 0) == :badtuple
 
       # Test deleting an element from a closed tuple
       assert tuple_delete_at(tuple([integer(), atom(), boolean()]), 1) ==
@@ -1195,8 +1381,8 @@ defmodule Module.Types.DescrTest do
                dynamic(tuple([integer()]))
 
       # Test deleting from a union of tuples
-      assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) ==
-               union(tuple([integer()]), tuple([float()]))
+      assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1)
+             |> equal?(tuple([union(integer(), float())]))
 
       # Test deleting from an intersection of tuples
       assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()]))
@@ -1287,6 +1473,7 @@ defmodule Module.Types.DescrTest do
 
     test "tuple_values" do
       assert tuple_values(integer()) == :badtuple
+      assert tuple_values(tuple([none()])) == :badtuple
       assert tuple_values(tuple([])) == none()
       assert tuple_values(tuple()) == term()
       assert tuple_values(open_tuple([integer()])) == term()
@@ -1413,7 +1600,6 @@ defmodule Module.Types.DescrTest do
 
     test "map_fetch with dynamic" do
       assert map_fetch(dynamic(), :a) == {true, dynamic()}
-
       assert map_fetch(union(dynamic(), integer()), :a) == :badmap
       assert map_fetch(union(dynamic(open_map(a: integer())), integer()), :a) == :badmap
       assert map_fetch(union(dynamic(integer()), integer()), :a) == :badmap
@@ -1430,6 +1616,104 @@ defmodule Module.Types.DescrTest do
              |> map_fetch(:a) == {false, union(dynamic(atom()), integer())}
     end
 
+    test "map_fetch with domain keys" do
+      integer_to_atom = open_map([{domain_key(:integer), atom()}])
+      assert map_fetch(integer_to_atom, :foo) == :badkey
+
+      # the key :a is for sure of type pid and exists in type
+      # %{atom() => pid()} and not %{:a => not_set()}
+      t1 = closed_map([{domain_key(:atom), pid()}])
+      t2 = closed_map(a: not_set())
+      t3 = open_map(a: not_set())
+
+      # Indeed, t2 is equivalent to the empty map
+      assert map_fetch(difference(t1, t2), :a) == :badkey
+      assert map_fetch(difference(t1, t3), :a) == {false, pid()}
+
+      t4 = closed_map([{domain_key(:pid), atom()}])
+      assert map_fetch(difference(t1, t4) |> difference(t3), :a) == {false, pid()}
+
+      assert map_fetch(closed_map([{domain_key(:atom), pid()}]), :a) == :badkey
+
+      assert map_fetch(dynamic(closed_map([{domain_key(:atom), pid()}])), :a) ==
+               {true, dynamic(pid())}
+
+      assert closed_map([{domain_key(:atom), number()}])
+             |> difference(open_map(a: if_set(integer())))
+             |> map_fetch(:a) == {false, float()}
+
+      assert closed_map([{domain_key(:atom), number()}])
+             |> difference(closed_map(b: if_set(integer())))
+             |> map_fetch(:a) == :badkey
+    end
+
+    test "map_get with domain keys" do
+      assert map_get(term(), term()) == :badmap
+
+      map_type = closed_map([{domain_key(:tuple), binary()}])
+      assert map_get(map_type, tuple()) == {:ok, nil_or_type(binary())}
+
+      # Type with all domain types
+      # %{:bar => :ok, integer() => :int, float() => :float, atom() => binary(), binary() => integer(), tuple() => float(), map() => pid(), reference() => port(), pid() => boolean()}
+      all_domains =
+        closed_map([
+          {:bar, atom([:ok])},
+          {domain_key(:integer), atom([:int])},
+          {domain_key(:float), atom([:float])},
+          {domain_key(:atom), binary()},
+          {domain_key(:binary), integer()},
+          {domain_key(:tuple), float()},
+          {domain_key(:map), pid()},
+          {domain_key(:reference), port()},
+          {domain_key(:pid), reference()},
+          {domain_key(:port), boolean()}
+        ])
+
+      assert map_get(all_domains, atom([:bar])) == {:ok_present, atom([:ok])}
+
+      assert map_get(all_domains, integer()) == {:ok, atom([:int]) |> nil_or_type()}
+      assert map_get(all_domains, number()) == {:ok, atom([:int, :float]) |> nil_or_type()}
+
+      assert map_get(all_domains, empty_list()) == {:ok_absent, atom([nil])}
+      assert map_get(all_domains, atom([:foo])) == {:ok, binary() |> nil_or_type()}
+      assert map_get(all_domains, binary()) == {:ok, integer() |> nil_or_type()}
+      assert map_get(all_domains, tuple([integer(), atom()])) == {:ok, nil_or_type(float())}
+      assert map_get(all_domains, empty_map()) == {:ok, pid() |> nil_or_type()}
+
+      # Union
+      assert map_get(all_domains, union(tuple(), empty_map())) ==
+               {:ok, union(float(), pid() |> nil_or_type())}
+
+      # Removing all maps with tuple keys
+      t_no_tuple = difference(all_domains, closed_map([{domain_key(:tuple), float()}]))
+      t_really_no_tuple = difference(all_domains, open_map([{domain_key(:tuple), float()}]))
+      assert subtype?(all_domains, open_map())
+      # It's only closed maps, so it should not change
+      assert map_get(t_no_tuple, tuple()) == {:ok, float() |> nil_or_type()}
+      # This time we actually removed all tuple to float keys
+      assert map_get(t_really_no_tuple, tuple()) == {:ok_absent, atom([nil])}
+
+      t1 = closed_map([{domain_key(:tuple), integer()}])
+      t2 = closed_map([{domain_key(:tuple), float()}])
+      t3 = union(t1, t2)
+      assert map_get(t3, tuple()) == {:ok, number() |> nil_or_type()}
+    end
+
+    test "map_get with dynamic" do
+      {_answer, type_selected} = map_get(dynamic(), term())
+      assert equal?(type_selected, dynamic() |> nil_or_type())
+    end
+
+    test "map_get with atom fall back" do
+      map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {domain_key(:atom), pid()}])
+      assert map_get(map, atom([:a, :b])) == {:ok_present, atom([:a, :b])}
+      assert map_get(map, atom([:a, :c])) == {:ok, union(atom([:a]), pid() |> nil_or_type())}
+      assert map_get(map, atom() |> difference(atom([:a, :b]))) == {:ok, pid() |> nil_or_type()}
+
+      assert map_get(map, atom() |> difference(atom([:a]))) ==
+               {:ok, union(atom([:b]), pid() |> nil_or_type())}
+    end
+
     test "map_delete" do
       assert map_delete(term(), :a) == :badmap
       assert map_delete(integer(), :a) == :badmap
@@ -1468,7 +1752,10 @@ defmodule Module.Types.DescrTest do
 
       # Deleting from a difference of maps
       {:ok, type} =
-        map_delete(difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())), :b)
+        map_delete(
+          difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())),
+          :b
+        )
 
       assert equal?(type, closed_map(a: integer()))
 
@@ -1476,6 +1763,12 @@ defmodule Module.Types.DescrTest do
       assert equal?(type, open_map(a: not_set()))
     end
 
+    test "map_delete with atom fallback" do
+      assert closed_map([{:a, integer()}, {:b, atom()}, {domain_key(:atom), pid()}])
+             |> map_delete(:a) ==
+               {:ok, closed_map([{:a, not_set()}, {:b, atom()}, {domain_key(:atom), pid()}])}
+    end
+
     test "map_take" do
       assert map_take(term(), :a) == :badmap
       assert map_take(integer(), :a) == :badmap
@@ -1563,11 +1856,15 @@ defmodule Module.Types.DescrTest do
 
       assert equal?(
                type,
-               union(closed_map(a: integer(), c: boolean()), closed_map(b: atom(), c: boolean()))
+               union(
+                 closed_map(a: integer(), c: boolean()),
+                 closed_map(b: atom(), c: boolean())
+               )
              )
 
       # Put a key-value pair in a dynamic map
-      assert map_put(dynamic(open_map()), :a, integer()) == {:ok, dynamic(open_map(a: integer()))}
+      assert map_put(dynamic(open_map()), :a, integer()) ==
+               {:ok, dynamic(open_map(a: integer()))}
 
       # Put a key-value pair in an intersection of maps
       {:ok, type} =
@@ -1592,6 +1889,64 @@ defmodule Module.Types.DescrTest do
       {false, type} = map_fetch(map, :a)
       assert equal?(type, atom())
     end
+
+    test "map_put with domain keys" do
+      # Using a literal key or an expression of that singleton key is the same
+      assert map_refresh(empty_map(), atom([:a]), integer()) == {:ok, closed_map(a: integer())}
+
+      # Several keys
+      assert map_refresh(empty_map(), atom([:a, :b]), integer()) ==
+               {:ok, closed_map(a: if_set(integer()), b: if_set(integer()))}
+
+      assert map_refresh(empty_map(), integer(), integer()) ==
+               {:ok, closed_map([{domain_key(:integer), integer()}])}
+
+      assert map_refresh(closed_map([{domain_key(:integer), integer()}]), integer(), float()) ==
+               {:ok, closed_map([{domain_key(:integer), number()}])}
+
+      assert map_refresh(open_map(), integer(), integer()) == {:ok, open_map()}
+
+      # TODO: Revisit this
+      # {:ok, type} = map_refresh(empty_map(), integer(), dynamic())
+      # assert equal?(type, dynamic(closed_map([{domain_key(:integer), term()}])))
+
+      # Adding a key of type float to a dynamic only guarantees that we have a map
+      # as we cannot express "has at least one key of type float => float"
+      {:ok, type} = map_refresh(dynamic(), float(), float())
+      assert equal?(type, dynamic(open_map()))
+
+      assert closed_map([{domain_key(:integer), integer()}])
+             |> difference(open_map())
+             |> empty?()
+
+      assert closed_map([{domain_key(:integer), integer()}])
+             |> difference(open_map())
+             |> map_refresh(integer(), float()) == :badmap
+
+      assert map_refresh(empty_map(), number(), float()) ==
+               {:ok,
+                closed_map([
+                  {domain_key(:integer), float()},
+                  {domain_key(:float), float()}
+                ])}
+
+      # Tricky cases with atoms:
+      # We add one atom fields that maps to an integer, which is not :a. So we do not touch
+      # :a, add integer to :b, and add a domain field.
+      assert map_refresh(
+               closed_map(a: pid(), b: pid()),
+               atom() |> difference(atom([:a])),
+               integer()
+             ) ==
+               {:ok,
+                closed_map([
+                  {:a, pid()},
+                  {:b, union(pid(), integer())},
+                  {domain_key(:atom), integer()}
+                ])}
+
+      assert map_refresh(empty_map(), term(), integer()) == {:ok, map_with_default(integer())}
+    end
   end
 
   describe "disjoint" do
@@ -1697,7 +2052,8 @@ defmodule Module.Types.DescrTest do
                "empty_list() or non_empty_list(float() or integer(), pid())"
 
       # Merge last element types
-      assert union(list(atom([:ok]), integer()), list(atom([:ok]), float())) |> to_quoted_string() ==
+      assert union(list(atom([:ok]), integer()), list(atom([:ok]), float()))
+             |> to_quoted_string() ==
                "empty_list() or non_empty_list(:ok, float() or integer())"
 
       assert union(dynamic(list(integer(), float())), dynamic(list(integer(), pid())))
@@ -1722,11 +2078,6 @@ defmodule Module.Types.DescrTest do
       assert tuple([closed_map(a: integer()), open_map()]) |> to_quoted_string() ==
                "{%{a: integer()}, map()}"
 
-      # TODO: eliminate tuple differences
-      # 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()}"
 
@@ -1795,7 +2146,12 @@ defmodule Module.Types.DescrTest do
         )
 
       decimal_int =
-        closed_map(__struct__: atom([Decimal]), coef: integer(), exp: integer(), sign: integer())
+        closed_map(
+          __struct__: atom([Decimal]),
+          coef: integer(),
+          exp: integer(),
+          sign: integer()
+        )
 
       assert atom([:error])
              |> union(
@@ -1883,7 +2239,7 @@ defmodule Module.Types.DescrTest do
                """
     end
 
-    test "map" do
+    test "map as records" do
       assert empty_map() |> to_quoted_string() == "empty_map()"
       assert open_map() |> to_quoted_string() == "map()"
 
@@ -1943,9 +2299,15 @@ defmodule Module.Types.DescrTest do
                "%{..., a: float() or integer()}"
 
       # Fusing complex nested maps with unions
-      assert closed_map(status: atom([:ok]), data: closed_map(value: term(), count: empty_list()))
+      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()))
+               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])))
@@ -1969,7 +2331,10 @@ defmodule Module.Types.DescrTest do
                "%{data: %{x: float() or integer(), y: atom()}, meta: map()}"
 
       # Test complex combinations
-      assert intersection(open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean()))
+      assert intersection(
+               open_map(a: number(), b: atom()),
+               open_map(a: integer(), c: boolean())
+             )
              |> union(difference(open_map(x: atom()), open_map(x: boolean())))
              |> to_quoted_string() ==
                "%{..., a: integer(), b: atom(), c: boolean()} or %{..., x: atom() and not boolean()}"
@@ -1992,6 +2357,14 @@ defmodule Module.Types.DescrTest do
              |> to_quoted_string() == "%{..., a: float(), b: atom(), c: port()}"
     end
 
+    test "maps as dictionaries" do
+      assert closed_map([{domain_key(:integer), integer()}])
+             |> to_quoted_string() == "%{integer() => if_set(integer())}"
+
+      assert closed_map([{domain_key(:integer), integer()}, {:float, float()}])
+             |> to_quoted_string() == "%{integer() => if_set(integer()), float: float()}"
+    end
+
     test "structs" do
       assert open_map(__struct__: atom([URI])) |> to_quoted_string() ==
                "%{..., __struct__: URI}"
diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs
index 5b0bb46ebc1..a6e49d2d2b3 100644
--- a/lib/elixir/test/elixir/module/types/expr_test.exs
+++ b/lib/elixir/test/elixir/module/types/expr_test.exs
@@ -43,16 +43,6 @@ defmodule Module.Types.ExprTest do
       assert typecheck!([x], [:ok | x]) == dynamic(non_empty_list(term(), term()))
     end
 
-    test "inference" do
-      assert typecheck!(
-               [x, y, z],
-               (
-                 List.to_integer([x, y | z])
-                 {x, y, z}
-               )
-             ) == dynamic(tuple([integer(), integer(), list(integer())]))
-    end
-
     test "hd" do
       assert typecheck!([x = [123, :foo]], hd(x)) == dynamic(union(atom([:foo]), integer()))
       assert typecheck!([x = [123 | :foo]], hd(x)) == dynamic(integer())
@@ -127,35 +117,48 @@ defmodule Module.Types.ExprTest do
   end
 
   describe "funs" do
-    test "infers calls" do
-      assert typecheck!(
-               [x],
-               (
-                 x.(1, 2)
-                 x
-               )
-             ) == dynamic(fun(2))
-    end
-
     test "infers functions" do
-      assert typecheck!(& &1) == fun([dynamic()], dynamic())
-      assert typecheck!(fn -> :ok end) == fun([], atom([:ok]))
+      assert typecheck!(& &1) |> equal?(fun([term()], dynamic()))
+
+      assert typecheck!(fn -> :ok end) |> equal?(fun([], dynamic(atom([:ok]))))
 
       assert typecheck!(fn
                <<"ok">>, {} -> :ok
                <<"error">>, {} -> :error
                [_ | _], %{} -> :list
-             end) ==
+             end)
+             |> equal?(
                intersection(
                  fun(
-                   [dynamic(non_empty_list(term(), term())), dynamic(open_map())],
-                   atom([:list])
+                   [non_empty_list(term(), term()), open_map()],
+                   dynamic(atom([:list]))
                  ),
                  fun(
-                   [dynamic(binary()), dynamic(tuple([]))],
-                   atom([:ok, :error])
+                   [binary(), tuple([])],
+                   dynamic(atom([:ok, :error]))
                  )
                )
+             )
+    end
+
+    test "application" do
+      assert typecheck!(
+               [map],
+               (fn
+                  %{a: a} = data -> %{data | b: a}
+                  data -> data
+                end).(map)
+             ) == dynamic()
+
+      assert typecheck!(
+               [],
+               [true, false]
+               |> Enum.random()
+               |> then(fn
+                 true -> :ok
+                 _ -> :error
+               end)
+             ) == dynamic(atom([:ok, :error]))
     end
 
     test "bad function" do
@@ -253,61 +256,26 @@ defmodule Module.Types.ExprTest do
 
              but function has type:
 
-                 (dynamic(map()) -> :map)
+                 (map() -> dynamic(:map))
              """
     end
-  end
 
-  describe "remotes" do
-    test "dynamic calls" do
-      assert typecheck!([%x{}], x.foo_bar()) == dynamic()
-    end
-
-    test "infers atoms" do
-      assert typecheck!(
-               [x],
-               (
-                 x.foo_bar()
-                 x
-               )
-             ) == dynamic(atom())
+    test "capture printing" do
+      assert typeerror!(123 = &{:ok, &1}) == """
+             the following pattern will never match:
 
-      assert typecheck!(
-               [x],
-               (
-                 x.foo_bar(123)
-                 x
-               )
-             ) == dynamic(atom())
+                 123 = &{:ok, &1}
 
-      assert typecheck!(
-               [x],
-               (
-                 &x.foo_bar/1
-                 x
-               )
-             ) == dynamic(atom())
-    end
+             because the right-hand side has type:
 
-    test "infers maps" do
-      assert typecheck!(
-               [x],
-               (
-                 :foo = x.foo_bar
-                 123 = x.baz_bat
-                 x
-               )
-             ) == dynamic(open_map(foo_bar: atom([:foo]), baz_bat: integer()))
+                 (term() -> dynamic({:ok, term()}))
+             """
     end
+  end
 
-    test "infers args" do
-      assert typecheck!(
-               [x, y],
-               (
-                 z = Integer.to_string(x + y)
-                 {x, y, z}
-               )
-             ) == dynamic(tuple([integer(), integer(), binary()]))
+  describe "remotes" do
+    test "dynamic calls" do
+      assert typecheck!([%x{}], x.foo_bar()) == dynamic()
     end
 
     test "undefined function warnings" do
@@ -535,16 +503,6 @@ defmodule Module.Types.ExprTest do
   end
 
   describe "binaries" do
-    test "inference" do
-      assert typecheck!(
-               [x, y],
-               (
-                 <<x::float-size(y)>>
-                 {x, y}
-               )
-             ) == dynamic(tuple([union(float(), integer()), integer()]))
-    end
-
     test "warnings" do
       assert typeerror!([<<x::binary-size(2)>>], <<x::float>>) ==
                ~l"""
@@ -644,16 +602,6 @@ defmodule Module.Types.ExprTest do
       assert typecheck!([x], {:ok, x}) == dynamic(tuple([atom([:ok]), term()]))
     end
 
-    test "inference" do
-      assert typecheck!(
-               [x, y],
-               (
-                 {:ok, :error} = {x, y}
-                 {x, y}
-               )
-             ) == dynamic(tuple([atom([:ok]), atom([:error])]))
-    end
-
     test "elem/2" do
       assert typecheck!(elem({:ok, 123}, 0)) == atom([:ok])
       assert typecheck!(elem({:ok, 123}, 1)) == integer()
@@ -946,6 +894,87 @@ defmodule Module.Types.ExprTest do
              """
     end
 
+    test "updating structs" do
+      integer_date_type =
+        dynamic(
+          closed_map(
+            __struct__: atom([Date]),
+            day: integer(),
+            calendar: term(),
+            month: term(),
+            year: term()
+          )
+        )
+
+      # When we know the type
+      assert typecheck!([], %Date{Date.new!(1, 1, 1) | day: 31}) ==
+               integer_date_type
+
+      assert typecheck!([], %Date{%Date{Date.new!(1, 1, 1) | day: 13} | day: 31}) ==
+               integer_date_type
+
+      # When we don't know the type of var
+      assert typeerror!([x], %Date{x | day: 31}) == ~l"""
+             a struct for Date is expected on struct update:
+
+                 %Date{x | day: 31}
+
+             but got type:
+
+                 dynamic()
+
+             where "x" was given the type:
+
+                 # type: dynamic()
+                 # from: types_test.ex:LINE
+                 x
+
+             when defining the variable "x", you must also pattern match on "%Date{}".
+
+             hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of:
+
+                 user = some_fun()
+                 %User{user | name: "John Doe"}
+
+             it is enough to write:
+
+                 %User{} = user = some_fun()
+                 %{user | name: "John Doe"}
+             """
+
+      # When we don't know the type of capture
+      assert typeerror!([], &%Date{&1 | day: 31}) =~ ~l"""
+             a struct for Date is expected on struct update:
+
+                 %Date{&1 | day: 31}
+
+             but got type:
+
+                 dynamic()
+
+             where "capture" was given the type:
+
+                 # type: dynamic()
+                 # from: types_test.ex:LINE
+                 &1
+
+             instead of using &1, you must define an anonymous function, define a variable and pattern match on "%Date{}".
+             """
+
+      # When we don't know the type of expression
+      assert typeerror!([], %Date{SomeMod.fun() | day: 31}) =~ """
+             a struct for Date is expected on struct update:
+
+                 %Date{SomeMod.fun() | day: 31}
+
+             but got type:
+
+                 dynamic()
+
+             you must assign "SomeMod.fun()" to variable and pattern match on "%Date{}".
+             """
+    end
+
     test "updating to open maps" do
       assert typecheck!(
                [key],
@@ -1005,7 +1034,7 @@ defmodule Module.Types.ExprTest do
     end
 
     test "nested map" do
-      assert typecheck!([x = %{}], x.foo.bar) == dynamic()
+      assert typecheck!([x = %{}], x.foo.bar) |> equal?(dynamic())
     end
 
     test "accessing a field on not a map" do
@@ -1458,16 +1487,6 @@ defmodule Module.Types.ExprTest do
              ) == dynamic(atom([:ok, :error, :timeout]))
     end
 
-    test "infers type for timeout" do
-      assert typecheck!(
-               [x],
-               receive do
-               after
-                 x -> x
-               end
-             ) == dynamic(union(integer(), atom([:infinity])))
-    end
-
     test "resets branches" do
       assert typecheck!(
                [x, timeout = :infinity],
@@ -1854,16 +1873,6 @@ defmodule Module.Types.ExprTest do
                """
     end
 
-    test "infers binary generators" do
-      assert typecheck!(
-               [x],
-               (
-                 for <<_ <- x>>, do: :ok
-                 x
-               )
-             ) == dynamic(binary())
-    end
-
     test ":into" do
       assert typecheck!([binary], for(<<x <- binary>>, do: x)) == list(integer())
       assert typecheck!([binary], for(<<x <- binary>>, do: x, into: [])) == list(integer())
@@ -1901,20 +1910,6 @@ defmodule Module.Types.ExprTest do
                end
              ) == union(atom([:ok]), union(integer(), float()))
     end
-
-    test ":reduce inference" do
-      assert typecheck!(
-               [list, x],
-               (
-                 123 =
-                   for _ <- list, reduce: x do
-                     x -> x
-                   end
-
-                 x
-               )
-             ) == dynamic(integer())
-    end
   end
 
   describe "info" do
diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs
index 9cc4ff155a4..94c3b785c81 100644
--- a/lib/elixir/test/elixir/module/types/infer_test.exs
+++ b/lib/elixir/test/elixir/module/types/infer_test.exs
@@ -51,20 +51,6 @@ defmodule Module.Types.InferTest do
     assert types[{:fun4, 4}] == {:infer, nil, [{args, atom([:ok])}]}
   end
 
-  test "infer types from expressions", config do
-    types =
-      infer config do
-        def fun(x) do
-          x.foo + x.bar
-        end
-      end
-
-    number = union(integer(), float())
-
-    assert types[{:fun, 1}] ==
-             {:infer, nil, [{[dynamic(open_map(foo: number, bar: number))], dynamic(number)}]}
-  end
-
   test "infer with Elixir built-in", config do
     types =
       infer config do
@@ -95,33 +81,6 @@ defmodule Module.Types.InferTest do
               ]}
   end
 
-  test "infers return types from private functions", config do
-    types =
-      infer config do
-        def pub(x), do: priv(x)
-        defp priv(:ok), do: :ok
-        defp priv(:error), do: :error
-      end
-
-    assert types[{:pub, 1}] ==
-             {:infer, nil, [{[dynamic(atom([:ok, :error]))], dynamic(atom([:ok, :error]))}]}
-
-    assert types[{:priv, 1}] == nil
-  end
-
-  test "infers return types from super functions", config do
-    types =
-      infer config do
-        def pub(:ok), do: :ok
-        def pub(:error), do: :error
-        defoverridable pub: 1
-        def pub(x), do: super(x)
-      end
-
-    assert types[{:pub, 1}] ==
-             {:infer, nil, [{[dynamic(atom([:ok, :error]))], dynamic(atom([:ok, :error]))}]}
-  end
-
   test "infers return types even with loops", config do
     types =
       infer config do
diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs
index 97cb75bd0c3..2377cb75ee1 100644
--- a/lib/elixir/test/elixir/module/types/integration_test.exs
+++ b/lib/elixir/test/elixir/module/types/integration_test.exs
@@ -85,77 +85,6 @@ defmodule Module.Types.IntegrationTest do
              ]
     end
 
-    test "writes exports with inferred map types" do
-      files = %{
-        "a.ex" => """
-        defmodule A do
-          defstruct [:x, :y, :z]
-
-          def struct_create_with_atom_keys(x) do
-            infer(y = %A{x: x})
-            {x, y}
-          end
-
-          def map_create_with_atom_keys(x) do
-            infer(%{__struct__: A, x: x, y: nil, z: nil})
-            x
-          end
-
-          def map_update_with_atom_keys(x) do
-            infer(%{x | y: nil})
-            x
-          end
-
-          def map_update_with_unknown_keys(x, y) do
-            infer(%{x | y => 123})
-            x
-          end
-
-          defp infer(%A{x: <<_::binary>>, y: nil}) do
-            :ok
-          end
-        end
-        """
-      }
-
-      modules = compile_modules(files)
-      exports = read_chunk(modules[A]).exports |> Map.new()
-
-      return = fn name, arity ->
-        pair = {name, arity}
-        %{^pair => %{sig: {:infer, nil, [{_, return}]}}} = exports
-        return
-      end
-
-      assert return.(:struct_create_with_atom_keys, 1) ==
-               dynamic(
-                 tuple([
-                   binary(),
-                   closed_map(
-                     __struct__: atom([A]),
-                     x: binary(),
-                     y: atom([nil]),
-                     z: atom([nil])
-                   )
-                 ])
-               )
-
-      assert return.(:map_create_with_atom_keys, 1) == dynamic(binary())
-
-      assert return.(:map_update_with_atom_keys, 1) ==
-               dynamic(
-                 closed_map(
-                   __struct__: atom([A]),
-                   x: binary(),
-                   y: term(),
-                   z: term()
-                 )
-               )
-
-      assert return.(:map_update_with_unknown_keys, 2) ==
-               dynamic(open_map())
-    end
-
     test "writes exports with inferred function types" do
       files = %{
         "a.ex" => """
@@ -180,8 +109,8 @@ defmodule Module.Types.IntegrationTest do
       assert return.(:captured, 0)
              |> equal?(
                fun_from_non_overlapping_clauses([
-                 {[dynamic(binary())], atom([:ok, :error])},
-                 {[dynamic(non_empty_list(term(), term()))], atom([:list])}
+                 {[binary()], dynamic(atom([:ok, :error]))},
+                 {[non_empty_list(term(), term())], dynamic(atom([:list]))}
                ])
              )
     end
@@ -455,7 +384,7 @@ defmodule Module.Types.IntegrationTest do
       assert_no_warnings(files)
     end
 
-    test "mismatched impl" do
+    test "mismatched implementation" do
       files = %{
         "a.ex" => """
         defprotocol Itself do
@@ -491,6 +420,50 @@ defmodule Module.Types.IntegrationTest do
       assert_warnings(files, warnings)
     end
 
+    @tag :require_ast
+    test "no implementation" do
+      files = %{
+        "a.ex" => """
+        defprotocol NoImplProtocol do
+          def callback(data)
+        end
+        """,
+        "b.ex" => """
+        defmodule NoImplProtocol.Caller do
+          def run do
+            NoImplProtocol.callback(:hello)
+          end
+        end
+        """
+      }
+
+      warnings = [
+        """
+            warning: incompatible types given to NoImplProtocol.callback/1:
+
+                NoImplProtocol.callback(:hello)
+
+            given types:
+
+                -:hello-
+
+            but the protocol was not yet implemented for any type and therefore will always fail. \
+        This error typically happens within libraries that define protocols and will disappear as \
+        soon as there is one implementation. If you expect the protocol to be implemented later on, \
+        you can define an implementation specific for development/test.
+
+            typing violation found at:
+            │
+          3 │     NoImplProtocol.callback(:hello)
+            │                    ~
+            │
+            └─ b.ex:3:20: NoImplProtocol.Caller.run/0
+        """
+      ]
+
+      assert_warnings(files, warnings, consolidate_protocols: true)
+    end
+
     @tag :require_ast
     test "String.Chars protocol dispatch" do
       files = %{
@@ -1494,7 +1467,7 @@ defmodule Module.Types.IntegrationTest do
 
   defp read_chunk(binary) do
     assert {:ok, {_module, [{~c"ExCk", chunk}]}} = :beam_lib.chunks(binary, [~c"ExCk"])
-    assert {:elixir_checker_v1, map} = :erlang.binary_to_term(chunk)
+    assert {:elixir_checker_v2, map} = :erlang.binary_to_term(chunk)
     map
   end
 
diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs
index 626c4c91478..ea947423e90 100644
--- a/lib/elixir/test/elixir/module/types/pattern_test.exs
+++ b/lib/elixir/test/elixir/module/types/pattern_test.exs
@@ -106,16 +106,6 @@ defmodule Module.Types.PatternTest do
              ) == uri_type
     end
 
-    test "refines types" do
-      assert typecheck!(
-               [x, foo = :foo, bar = 123],
-               (
-                 {^foo, ^bar} = x
-                 x
-               )
-             ) == dynamic(tuple([atom([:foo]), integer()]))
-    end
-
     test "reports incompatible types" do
       assert typeerror!([x = {:ok, _}], [_ | _] = x) == ~l"""
              the following pattern will never match:
diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs
index 5eedaa71246..a167086ad4b 100644
--- a/lib/elixir/test/elixir/module_test.exs
+++ b/lib/elixir/test/elixir/module_test.exs
@@ -413,6 +413,17 @@ defmodule ModuleTest do
     assert map.module == ModuleCreateDebugInfo
   end
 
+  test "uses the debug_info chunk when explicitly set to true" do
+    {:module, ModuleCreateDebugInfoTrue, binary, _} =
+      Module.create(ModuleCreateDebugInfoTrue, quote(do: @compile({:debug_info, true})), __ENV__)
+
+    {:ok, {_, [debug_info: {:debug_info_v1, backend, data}]}} =
+      :beam_lib.chunks(binary, [:debug_info])
+
+    {:ok, map} = backend.debug_info(:elixir_v1, ModuleCreateDebugInfoTrue, data, [])
+    assert map.module == ModuleCreateDebugInfoTrue
+  end
+
   test "uses the debug_info chunk even if debug_info is set to false" do
     {:module, ModuleCreateNoDebugInfo, binary, _} =
       Module.create(ModuleCreateNoDebugInfo, quote(do: @compile({:debug_info, false})), __ENV__)
diff --git a/lib/elixir/test/elixir/option_parser_test.exs b/lib/elixir/test/elixir/option_parser_test.exs
index 89b46968b46..737a3832e26 100644
--- a/lib/elixir/test/elixir/option_parser_test.exs
+++ b/lib/elixir/test/elixir/option_parser_test.exs
@@ -100,21 +100,46 @@ defmodule OptionParserTest do
   end
 
   test "parse!/2 raises an exception for an unknown option using strict" do
-    msg = "1 error found!\n--doc-bar : Unknown option. Did you mean --docs-bar?"
+    msg =
+      """
+      1 error found!
+      --doc-bar : Unknown option. Did you mean --docs-bar?
+
+      Supported options:
+        --docs-bar STRING
+        --source STRING\
+      """
 
     assert_raise OptionParser.ParseError, msg, fn ->
       argv = ["--source", "from_docs/", "--doc-bar", "show"]
       OptionParser.parse!(argv, strict: [source: :string, docs_bar: :string])
     end
 
-    assert_raise OptionParser.ParseError, "1 error found!\n--foo : Unknown option", fn ->
-      argv = ["--source", "from_docs/", "--foo", "show"]
-      OptionParser.parse!(argv, strict: [source: :string, docs: :string])
-    end
+    assert_raise OptionParser.ParseError,
+                 """
+                 1 error found!
+                 --foo : Unknown option
+
+                 Supported options:
+                   --docs STRING
+                   --source STRING\
+                 """,
+                 fn ->
+                   argv = ["--source", "from_docs/", "--foo", "show"]
+                   OptionParser.parse!(argv, strict: [source: :string, docs: :string])
+                 end
   end
 
   test "parse!/2 raises an exception for an unknown option using strict when it is only off by underscores" do
-    msg = "1 error found!\n--docs_bar : Unknown option. Did you mean --docs-bar?"
+    msg =
+      """
+      1 error found!
+      --docs_bar : Unknown option. Did you mean --docs-bar?
+
+      Supported options:
+        --docs-bar STRING
+        --source STRING\
+      """
 
     assert_raise OptionParser.ParseError, msg, fn ->
       argv = ["--source", "from_docs/", "--docs_bar", "show"]
@@ -123,14 +148,57 @@ defmodule OptionParserTest do
   end
 
   test "parse!/2 raises an exception when an option is of the wrong type" do
-    assert_raise OptionParser.ParseError, fn ->
-      argv = ["--bad", "opt", "foo", "-o", "bad", "bar"]
-      OptionParser.parse!(argv, switches: [bad: :integer])
+    assert_raise OptionParser.ParseError,
+                 """
+                 1 error found!
+                 --bad : Expected type integer, got "opt"
+
+                 Supported options:
+                   --bad INTEGER\
+                 """,
+                 fn ->
+                   argv = ["--bad", "opt", "foo", "-o", "bad", "bar"]
+                   OptionParser.parse!(argv, switches: [bad: :integer])
+                 end
+  end
+
+  test "parse!/2 lists all supported options and aliases" do
+    expected_suggestion =
+      """
+      1 error found!
+      --verbos : Unknown option. Did you mean --verbose?
+
+      Supported options:
+        --count INTEGER (alias: -c)
+        --debug, --no-debug (alias: -d)
+        --files STRING (alias: -f) (may be given more than once)
+        --name STRING (alias: -n)
+        --verbose, --no-verbose (alias: -v)\
+      """
+
+    assert_raise OptionParser.ParseError, expected_suggestion, fn ->
+      OptionParser.parse!(["--verbos"],
+        strict: [
+          name: :string,
+          count: :integer,
+          verbose: :boolean,
+          debug: :boolean,
+          files: :keep
+        ],
+        aliases: [n: :name, c: :count, v: :verbose, d: :debug, f: :files]
+      )
     end
   end
 
   test "parse_head!/2 raises an exception when an option is of the wrong type" do
-    message = "1 error found!\n--number : Expected type integer, got \"lib\""
+    message =
+      """
+      1 error found!
+      --number : Expected type integer, got "lib"
+
+      Supported options:
+        --number INTEGER\
+      """
 
     assert_raise OptionParser.ParseError, message, fn ->
       argv = ["--number", "lib", "test/enum_test.exs"]
@@ -350,6 +418,80 @@ defmodule OptionParserTest do
       assert OptionParser.parse(["arg1", "--option", "-43.2"], opts) ==
                {[option: -43.2], ["arg1"], []}
     end
+
+    test "parses configured regexes" do
+      assert {[pattern: regex], ["foo"], []} =
+               OptionParser.parse(["--pattern", "a.*b", "foo"], switches: [pattern: :regex])
+
+      assert Regex.match?(regex, "aXXXb")
+      refute Regex.match?(regex, "xyz")
+
+      assert {[pattern: regex], ["foo"], []} =
+               OptionParser.parse(["--pattern=a.*b", "foo"], switches: [pattern: :regex])
+
+      assert Regex.match?(regex, "aXXXb")
+
+      # Test Unicode support
+      assert {[pattern: regex], ["foo"], []} =
+               OptionParser.parse(["--pattern", "café.*résumé", "foo"],
+                 switches: [pattern: :regex]
+               )
+
+      assert Regex.match?(regex, "café test résumé")
+      refute Regex.match?(regex, "ascii only")
+
+      # Test invalid regex
+      assert OptionParser.parse(["--pattern", "[invalid", "foo"], switches: [pattern: :regex]) ==
+               {[], ["foo"], [{"--pattern", "[invalid"}]}
+    end
+
+    test "parses configured regexes with keep" do
+      argv = ["--pattern", "a.*", "--pattern", "b.*", "foo"]
+
+      assert {[pattern: regex1, pattern: regex2], ["foo"], []} =
+               OptionParser.parse(argv, switches: [pattern: [:regex, :keep]])
+
+      assert Regex.match?(regex1, "aXXX")
+      assert Regex.match?(regex2, "bXXX")
+      refute Regex.match?(regex1, "bXXX")
+      refute Regex.match?(regex2, "aXXX")
+
+      argv = ["--pattern=a.*", "foo", "--pattern=b.*", "bar"]
+
+      assert {[pattern: regex1, pattern: regex2], ["foo", "bar"], []} =
+               OptionParser.parse(argv, switches: [pattern: [:regex, :keep]])
+
+      assert Regex.match?(regex1, "aXXX")
+      assert Regex.match?(regex2, "bXXX")
+    end
+
+    test "correctly handles regex compilation errors" do
+      opts = [switches: [pattern: :regex]]
+
+      # Invalid regex patterns should be treated as errors
+      assert OptionParser.parse(["--pattern", "*invalid"], opts) ==
+               {[], [], [{"--pattern", "*invalid"}]}
+
+      assert OptionParser.parse(["--pattern", "[unclosed"], opts) ==
+               {[], [], [{"--pattern", "[unclosed"}]}
+
+      assert OptionParser.parse(["--pattern", "(?invalid)"], opts) ==
+               {[], [], [{"--pattern", "(?invalid)"}]}
+    end
+
+    test "parse! raises an exception for invalid regex patterns" do
+      assert_raise OptionParser.ParseError,
+                   ~r/Invalid regular expression \"\[invalid\": missing terminating \] for character class at position \d/,
+                   fn ->
+                     OptionParser.parse!(["--pattern", "[invalid"], switches: [pattern: :regex])
+                   end
+
+      assert_raise OptionParser.ParseError,
+                   ~r/Invalid regular expression \"\(\?invalid\)\": unrecognized character after \(\? or \(\?\- at position \d/,
+                   fn ->
+                     OptionParser.parse!(["--pattern", "(?invalid)"], switches: [pattern: :regex])
+                   end
+    end
   end
 
   describe "next" do
diff --git a/lib/elixir/test/elixir/protocol/consolidation_test.exs b/lib/elixir/test/elixir/protocol/consolidation_test.exs
index 48ab0d6a328..fc03f8d31da 100644
--- a/lib/elixir/test/elixir/protocol/consolidation_test.exs
+++ b/lib/elixir/test/elixir/protocol/consolidation_test.exs
@@ -165,7 +165,7 @@ defmodule Protocol.ConsolidationTest do
 
     defp exports(binary) do
       {:ok, {_, [{~c"ExCk", check_bin}]}} = :beam_lib.chunks(binary, [~c"ExCk"])
-      assert {:elixir_checker_v1, contents} = :erlang.binary_to_term(check_bin)
+      assert {:elixir_checker_v2, contents} = :erlang.binary_to_term(check_bin)
       Map.new(contents.exports)
     end
 
@@ -176,31 +176,6 @@ defmodule Protocol.ConsolidationTest do
       assert %{{:ok, 1} => %{deprecated: "Reason", sig: _}} = exports(sample_binary())
     end
 
-    test "defines signatures without fallback to any" do
-      exports = exports(sample_binary())
-
-      assert %{{:impl_for, 1} => %{sig: {:strong, domain, clauses}}} = exports
-      assert domain == [term()]
-
-      assert clauses == [
-               {[Of.impl(ImplStruct)], atom([Sample.Protocol.ConsolidationTest.ImplStruct])},
-               {[negation(Of.impl(ImplStruct))], atom([nil])}
-             ]
-
-      assert %{{:impl_for!, 1} => %{sig: {:strong, domain, clauses}}} = exports
-      assert domain == [Of.impl(ImplStruct)]
-
-      assert clauses == [
-               {[Of.impl(ImplStruct)], atom([Sample.Protocol.ConsolidationTest.ImplStruct])}
-             ]
-
-      assert %{{:ok, 1} => %{sig: {:strong, nil, clauses}}} = exports
-
-      assert clauses == [
-               {[Of.impl(ImplStruct)], dynamic()}
-             ]
-    end
-
     test "defines signatures with fallback to any" do
       exports = exports(with_any_binary())
 
diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs
index e64897b6e7c..166a3281360 100644
--- a/lib/elixir/test/elixir/regex_test.exs
+++ b/lib/elixir/test/elixir/regex_test.exs
@@ -9,22 +9,22 @@ 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"
+  test "module attribute" do
+    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
+
+    if System.otp_release() >= "28" do
+      assert ModAttr.regex().re_pattern != ModAttr.bare_regex().re_pattern
+    else
+      assert ModAttr.regex().re_pattern == ModAttr.bare_regex().re_pattern
     end
   end
 
diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex
index bc2ea55fac1..b323e0f58e5 100644
--- a/lib/ex_unit/lib/ex_unit.ex
+++ b/lib/ex_unit/lib/ex_unit.ex
@@ -93,6 +93,50 @@ defmodule ExUnit do
 
   @type test_id :: {module, name :: atom}
 
+  @typedoc """
+  Configuration options for ExUnit.
+
+  See `configure/1` for detailed documentation of each option.
+  """
+  @type configure_opts :: [
+          {:assert_receive_timeout, non_neg_integer()}
+          | {:autorun, boolean()}
+          | {:capture_log, boolean() | [level: Logger.level()]}
+          | {:colors,
+             [
+               enabled: boolean(),
+               success: atom(),
+               invalid: atom(),
+               skipped: atom(),
+               failure: atom(),
+               error_info: atom(),
+               extra_info: atom(),
+               location_info: [atom()],
+               diff_insert: atom(),
+               diff_insert_whitespace: IO.ANSI.ansidata(),
+               diff_delete: atom(),
+               diff_delete_whitespace: IO.ANSI.ansidata()
+             ]}
+          | {:exclude, keyword()}
+          | {:exit_status, non_neg_integer()}
+          | {:failures_manifest_path, String.t()}
+          | {:formatters, [module()]}
+          | {:include, keyword()}
+          | {:max_cases, pos_integer()}
+          | {:max_failures, pos_integer() | :infinity}
+          | {:only_test_ids, [test_id()]}
+          | {:rand_algorithm, atom()}
+          | {:refute_receive_timeout, non_neg_integer()}
+          | {:seed, non_neg_integer()}
+          | {:slowest, non_neg_integer()}
+          | {:slowest_modules, non_neg_integer()}
+          | {:stacktrace_depth, non_neg_integer()}
+          | {:timeout, pos_integer()}
+          | {:trace, boolean()}
+          | {:test_location_relative_path, String.t()}
+          | {atom(), term()}
+        ]
+
   defmodule Test do
     @moduledoc """
     A struct that keeps information about the test.
@@ -218,7 +262,7 @@ defmodule ExUnit do
   If you want to run tests manually, you can set the `:autorun` option
   to `false` and use `run/0` to run tests.
   """
-  @spec start(Keyword.t()) :: :ok
+  @spec start(configure_opts()) :: :ok
   def start(options \\ []) do
     {:ok, _} = Application.ensure_all_started(:ex_unit)
 
@@ -287,7 +331,7 @@ defmodule ExUnit do
       filter. See the "Filters" section in the documentation for `ExUnit.Case`;
 
     * `:exit_status` - specifies an alternate exit status to use when the test
-      suite fails. Defaults to 2;
+      suite fails. Defaults to `2`;
 
     * `:failures_manifest_path` - specifies a path to the file used to store failures
       between runs;
@@ -357,7 +401,7 @@ defmodule ExUnit do
   and these options can then be used in places such as custom formatters. These
   other options will be ignored by ExUnit itself.
   """
-  @spec configure(Keyword.t()) :: :ok
+  @spec configure(configure_opts()) :: :ok
   def configure(options) when is_list(options) do
     Enum.each(options, fn {k, v} ->
       Application.put_env(:ex_unit, k, v)
diff --git a/lib/ex_unit/lib/ex_unit/callbacks.ex b/lib/ex_unit/lib/ex_unit/callbacks.ex
index 622a02658cc..5fad529b552 100644
--- a/lib/ex_unit/lib/ex_unit/callbacks.ex
+++ b/lib/ex_unit/lib/ex_unit/callbacks.ex
@@ -169,6 +169,12 @@ defmodule ExUnit.Callbacks do
       end
   """
 
+  @type child_spec_overrides :: [
+          restart: :permanent | :transient | :temporary,
+          shutdown: :brutal_kill | timeout(),
+          type: :worker | :supervisor
+        ]
+
   @doc false
   def __register__(module) do
     Module.put_attribute(module, :ex_unit_describe, nil)
@@ -560,7 +566,7 @@ defmodule ExUnit.Callbacks do
   more about these keys in [the `Task` module](`Task#module-ancestor-and-caller-tracking`).
   """
   @doc since: "1.5.0"
-  @spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) ::
+  @spec start_supervised(Supervisor.child_spec() | module | {module, term}, child_spec_overrides) ::
           Supervisor.on_start_child()
   def start_supervised(child_spec_or_module, opts \\ []) do
     sup =
@@ -581,7 +587,8 @@ defmodule ExUnit.Callbacks do
   not started properly.
   """
   @doc since: "1.6.0"
-  @spec start_supervised!(Supervisor.child_spec() | module | {module, term}, keyword) :: pid
+  @spec start_supervised!(Supervisor.child_spec() | module | {module, term}, child_spec_overrides) ::
+          pid
   def start_supervised!(child_spec_or_module, opts \\ []) do
     case start_supervised(child_spec_or_module, opts) do
       {:ok, pid} ->
@@ -627,7 +634,10 @@ defmodule ExUnit.Callbacks do
   > *by the test supervisor* in reverse order, ensuring graceful termination.
   """
   @doc since: "1.14.0"
-  @spec start_link_supervised!(Supervisor.child_spec() | module | {module, term}, keyword) ::
+  @spec start_link_supervised!(
+          Supervisor.child_spec() | module | {module, term},
+          child_spec_overrides
+        ) ::
           pid
   def start_link_supervised!(child_spec_or_module, opts \\ []) do
     pid = start_supervised!(child_spec_or_module, opts)
diff --git a/lib/ex_unit/lib/ex_unit/capture_io.ex b/lib/ex_unit/lib/ex_unit/capture_io.ex
index 30e437a43ff..f506b6a4bf5 100644
--- a/lib/ex_unit/lib/ex_unit/capture_io.ex
+++ b/lib/ex_unit/lib/ex_unit/capture_io.ex
@@ -28,6 +28,12 @@ defmodule ExUnit.CaptureIO do
 
   """
 
+  @type capture_io_opts :: [
+          input: String.t(),
+          capture_prompt: boolean(),
+          encoding: :unicode | :latin1
+        ]
+
   @doc """
   Captures IO generated when evaluating `fun`.
 
@@ -152,7 +158,7 @@ defmodule ExUnit.CaptureIO do
 
   See `capture_io/1` for more information.
   """
-  @spec capture_io(atom() | pid() | String.t() | keyword(), (-> any())) :: String.t()
+  @spec capture_io(atom() | pid() | String.t() | capture_io_opts, (-> any())) :: String.t()
   def capture_io(device_pid_input_or_options, fun)
 
   def capture_io(device_or_pid, fun)
@@ -172,7 +178,7 @@ defmodule ExUnit.CaptureIO do
 
   See `capture_io/1` for more information.
   """
-  @spec capture_io(atom() | pid(), String.t() | keyword(), (-> any())) :: String.t()
+  @spec capture_io(atom() | pid(), String.t() | capture_io_opts, (-> any())) :: String.t()
   def capture_io(device_or_pid, input_or_options, fun)
       when (is_atom(device_or_pid) or is_pid(device_or_pid)) and
              (is_binary(input_or_options) or is_list(input_or_options)) and is_function(fun, 0) do
@@ -209,7 +215,7 @@ defmodule ExUnit.CaptureIO do
   See `with_io/1` for more information.
   """
   @doc since: "1.13.0"
-  @spec with_io(atom() | pid() | String.t() | keyword(), (-> any())) :: {any(), String.t()}
+  @spec with_io(atom() | pid() | String.t() | capture_io_opts, (-> any())) :: {any(), String.t()}
   def with_io(device_pid_input_or_options, fun)
 
   def with_io(device, fun) when is_atom(device) and is_function(fun, 0) do
@@ -234,7 +240,7 @@ defmodule ExUnit.CaptureIO do
   See `with_io/1` for more information.
   """
   @doc since: "1.13.0"
-  @spec with_io(atom() | pid(), String.t() | keyword(), (-> any())) :: {any(), String.t()}
+  @spec with_io(atom() | pid(), String.t() | capture_io_opts, (-> any())) :: {any(), String.t()}
   def with_io(device_or_pid, input_or_options, fun)
 
   def with_io(device, input, fun)
diff --git a/lib/ex_unit/lib/ex_unit/capture_log.ex b/lib/ex_unit/lib/ex_unit/capture_log.ex
index e885a10df91..e8178a9b7b4 100644
--- a/lib/ex_unit/lib/ex_unit/capture_log.ex
+++ b/lib/ex_unit/lib/ex_unit/capture_log.ex
@@ -43,6 +43,10 @@ defmodule ExUnit.CaptureLog do
 
   @compile {:no_warn_undefined, Logger}
 
+  @type capture_log_opts :: [
+          level: Logger.level() | nil
+        ]
+
   @doc """
   Captures Logger messages generated when evaluating `fun`.
 
@@ -74,7 +78,7 @@ defmodule ExUnit.CaptureLog do
   To get the result of the evaluation along with the captured log,
   use `with_log/2`.
   """
-  @spec capture_log(keyword, (-> any)) :: String.t()
+  @spec capture_log(capture_log_opts, (-> any)) :: String.t()
   def capture_log(opts \\ [], fun) do
     {_, log} = with_log(opts, fun)
     log
@@ -98,7 +102,7 @@ defmodule ExUnit.CaptureLog do
 
   """
   @doc since: "1.13.0"
-  @spec with_log(keyword, (-> result)) :: {result, log :: String.t()} when result: any
+  @spec with_log(capture_log_opts, (-> result)) :: {result, log :: String.t()} when result: any
   def with_log(opts \\ [], fun) when is_list(opts) do
     opts =
       if opts[:level] == :warn do
diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex
index 38b59cf2e5c..f72160711c9 100644
--- a/lib/ex_unit/lib/ex_unit/case.ex
+++ b/lib/ex_unit/lib/ex_unit/case.ex
@@ -168,18 +168,22 @@ defmodule ExUnit.Case do
 
     * `:test_pid` - the PID of the testing process
 
-    * `:test_type` - the test type used when printing test results.
-      It is set by ExUnit to `:test`, `:doctest` and so on, but is customizable.
+    * `:test_type` - the test type used when printing test results. It can also
+      be used for filtering. It is set by ExUnit to `:test`, `:doctest`, or
+      the equivalent type given to `register_test/6`
 
     * `:describe` - the describe block the test belongs to (if in a describe)
 
-    * `:describe_line` - the line the describe block begins on (if in a describe)
+    * `:describe_line` - the line the describe block begins on (if in a describe).
+      It can be used to run all tests in the describe block given by line
 
     * `:doctest` - the module or the file being doctested (if a doctest)
 
-    * `:doctest_data` - additional metadata about doctests (if a doctest)
+    * `:doctest_data` - additional metadata about doctests stored in a map,
+      such as the `:end_line`, available for reflection purposes (if a doctest)
 
-    * `:doctest_line` - the line the doctest was defined (if a doctest)
+    * `:doctest_line` - the line the doctest was defined (if a doctest).
+      It can be used to run a doctest defined at the given source line
 
   The following tags customize how tests behave:
 
@@ -187,7 +191,7 @@ defmodule ExUnit.Case do
 
     * `:skip` - skips the test with the given reason
 
-    * `:timeout` - customizes the test timeout in milliseconds (defaults to 60000).
+    * `:timeout` - customizes the test timeout in milliseconds (defaults to `60_000`).
       Accepts `:infinity` as a timeout value.
 
     * `:tmp_dir` - (since v1.11.0) see the "Tmp Dir" section below
@@ -338,6 +342,11 @@ defmodule ExUnit.Case do
   """
 
   @type env :: module() | Macro.Env.t()
+
+  @type register_attribute_opts :: [
+          accumulate: boolean(),
+          persist: boolean()
+        ]
   @compile {:no_warn_undefined, [IEx.Pry]}
   @reserved [:module, :file, :line, :test, :async, :registered, :describe]
 
@@ -653,12 +662,11 @@ defmodule ExUnit.Case do
   end
 
   @doc """
-  Registers a function to run as part of this case.
+  Registers a function to run as a test for this module.
 
-  This is used by third-party projects, like QuickCheck, to
-  implement macros like `property/3` that works like `test`
-  but instead defines a property. See `test/3` implementation
-  for an example of invoking this function.
+  This is used by third-party projects to implement macros like
+  `property/3` that works like `test` but instead defines a property.
+  See `test/3` implementation for an example of invoking this function.
 
   The test type will be converted to a string and pluralized for
   display. You can use `ExUnit.plural_rule/2` to set a custom
@@ -767,7 +775,7 @@ defmodule ExUnit.Case do
 
   """
   @doc since: "1.3.0"
-  @spec register_attribute(env, atom, keyword) :: :ok
+  @spec register_attribute(env, atom, register_attribute_opts) :: :ok
   def register_attribute(env, name, opts \\ [])
   def register_attribute(%{module: mod}, name, opts), do: register_attribute(mod, name, opts)
 
@@ -809,7 +817,7 @@ defmodule ExUnit.Case do
 
   """
   @doc since: "1.10.0"
-  @spec register_describe_attribute(env, atom, keyword) :: :ok
+  @spec register_describe_attribute(env, atom, register_attribute_opts) :: :ok
   def register_describe_attribute(env, name, opts \\ [])
 
   def register_describe_attribute(%{module: mod}, name, opts) do
@@ -849,7 +857,7 @@ defmodule ExUnit.Case do
 
   """
   @doc since: "1.10.0"
-  @spec register_module_attribute(env, atom, keyword) :: :ok
+  @spec register_module_attribute(env, atom, register_attribute_opts) :: :ok
   def register_module_attribute(env, name, opts \\ [])
 
   def register_module_attribute(%{module: mod}, name, opts) do
diff --git a/lib/ex_unit/lib/ex_unit/filters.ex b/lib/ex_unit/lib/ex_unit/filters.ex
index 106cd2d56d4..69f9ac71d51 100644
--- a/lib/ex_unit/lib/ex_unit/filters.ex
+++ b/lib/ex_unit/lib/ex_unit/filters.ex
@@ -196,11 +196,8 @@ defmodule ExUnit.Filters do
   Tests are first excluded, then included, and then skipped (if any left).
 
   If a `:skip` tag is found in `tags`, `{:skipped, message}` is returned if the test
-  has been left after the `exclude` and `include` filters. Otherwise `{:exclude, message}`
-  is returned.
-
-  The only exception to this rule is that `:skip` is found in the `include` filter,
-  `:ok` is returned regardless of whether the test was excluded or not.
+  remains after the `exclude` and `include` filters. However, if skipped tests are
+  specifically included, then they will always run.
 
   ## Examples
 
diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex
index 046d648a416..fc5cf9b7b91 100644
--- a/lib/iex/lib/iex.ex
+++ b/lib/iex/lib/iex.ex
@@ -390,6 +390,78 @@ defmodule IEx do
 
   """
 
+  @typedoc """
+  Color settings used by the IEx shell.
+  """
+  @type colors_opts :: [
+          enabled: boolean(),
+          eval_result: IO.ANSI.ansidata(),
+          eval_info: IO.ANSI.ansidata(),
+          eval_error: IO.ANSI.ansidata(),
+          eval_interrupt: IO.ANSI.ansidata(),
+          stack_info: IO.ANSI.ansidata(),
+          blame_diff: IO.ANSI.ansidata(),
+          ls_directory: IO.ANSI.ansidata(),
+          ls_device: IO.ANSI.ansidata(),
+          doc_code: IO.ANSI.ansidata(),
+          doc_inline_code: IO.ANSI.ansidata(),
+          doc_headings: IO.ANSI.ansidata(),
+          doc_title: IO.ANSI.ansidata(),
+          doc_bold: IO.ANSI.ansidata(),
+          doc_underline: IO.ANSI.ansidata(),
+          syntax_colors: syntax_colors_opts() | false
+        ]
+
+  @typedoc """
+  Syntax coloring settings for inspected expressions.
+  """
+  @type syntax_colors_opts :: [
+          atom: IO.ANSI.ansidata(),
+          binary: IO.ANSI.ansidata(),
+          boolean: IO.ANSI.ansidata(),
+          charlist: IO.ANSI.ansidata(),
+          list: IO.ANSI.ansidata(),
+          map: IO.ANSI.ansidata(),
+          nil: IO.ANSI.ansidata(),
+          number: IO.ANSI.ansidata(),
+          string: IO.ANSI.ansidata(),
+          tuple: IO.ANSI.ansidata(),
+          variable: IO.ANSI.ansidata(),
+          call: IO.ANSI.ansidata(),
+          operator: IO.ANSI.ansidata(),
+          reset: IO.ANSI.ansidata()
+        ]
+
+  @typedoc """
+  Inspect settings used by the IEx shell.
+  """
+  @type inspect_opts :: [
+          base: :binary | :decimal | :octal | :hex,
+          binaries: :infer | :as_binaries | :as_strings,
+          charlists: :infer | :as_charlists | :as_lists,
+          custom_options: keyword(),
+          inspect_fun: (term(), Inspect.Opts.t() -> Inspect.Algebra.t()),
+          limit: pos_integer() | :infinity,
+          pretty: boolean(),
+          printable_limit: pos_integer() | :infinity,
+          safe: boolean(),
+          structs: boolean(),
+          syntax_colors: syntax_colors_opts(),
+          width: pos_integer() | :infinity
+        ]
+
+  @type configure_opts :: [
+          auto_reload: boolean(),
+          alive_prompt: String.t(),
+          colors: colors_opts(),
+          default_prompt: String.t(),
+          dot_iex: String.t() | nil,
+          history_size: integer(),
+          inspect: inspect_opts(),
+          parser: {module(), atom(), [any()]},
+          width: pos_integer()
+        ]
+
   @doc """
   Configures IEx.
 
@@ -521,7 +593,7 @@ defmodule IEx do
   in-memory modules when they get invalidated by a concurrent compilation
   happening in the Operating System.
   """
-  @spec configure(keyword()) :: :ok
+  @spec configure(configure_opts) :: :ok
   def configure(options) do
     IEx.Config.configure(options)
   end
diff --git a/lib/iex/lib/iex/broker.ex b/lib/iex/lib/iex/broker.ex
index 56eba38da04..d2d9ed57140 100644
--- a/lib/iex/lib/iex/broker.ex
+++ b/lib/iex/lib/iex/broker.ex
@@ -158,7 +158,10 @@ defmodule IEx.Broker do
   defp local_or_remote_shell() do
     Enum.find_value([node() | Node.list()], fn node ->
       try do
-        :erpc.call(node, IEx.Broker, :shell, [])
+        case :erpc.call(node, :shell, :whereis, []) do
+          pid when is_pid(pid) -> pid
+          :undefined -> nil
+        end
       catch
         _, _ -> nil
       end
diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex
index 80860f60bb4..cf470754feb 100644
--- a/lib/iex/lib/iex/helpers.ex
+++ b/lib/iex/lib/iex/helpers.ex
@@ -113,6 +113,7 @@ defmodule IEx.Helpers do
     * `:force` - when `true`, forces the application to recompile
 
   """
+  @spec recompile(force: boolean()) :: :ok | :error | :noop
   def recompile(options \\ []) do
     cond do
       not mix_started?() ->
@@ -1034,6 +1035,9 @@ defmodule IEx.Helpers do
 
       {:error, :enoent} ->
         IO.puts(IEx.color(:eval_error, "No directory #{directory}"))
+
+      {:error, reason} ->
+        IO.puts(IEx.color(:eval_error, :file.format_error(reason)))
     end
 
     dont_display_result()
@@ -1078,6 +1082,9 @@ defmodule IEx.Helpers do
 
       {:error, :enotdir} ->
         IO.puts(IEx.color(:eval_info, Path.absname(path)))
+
+      {:error, reason} ->
+        IO.puts(IEx.color(:eval_error, :file.format_error(reason)))
     end
 
     dont_display_result()
diff --git a/lib/iex/lib/iex/info.ex b/lib/iex/lib/iex/info.ex
index aaf05a57b0a..332d7ade11a 100644
--- a/lib/iex/lib/iex/info.ex
+++ b/lib/iex/lib/iex/info.ex
@@ -458,7 +458,7 @@ defimpl IEx.Info, for: Range do
     description = """
     This is a struct representing a range of numbers. It is commonly
     defined using the `first..last//step` syntax. The step is not
-    required and defaults to 1.
+    required and defaults to `1`.
     """
 
     [
diff --git a/lib/iex/lib/iex/pry.ex b/lib/iex/lib/iex/pry.ex
index 463aa35e9c0..333365f65b6 100644
--- a/lib/iex/lib/iex/pry.ex
+++ b/lib/iex/lib/iex/pry.ex
@@ -704,7 +704,7 @@ defmodule IEx.Pry do
   end
 
   def dbg(ast, options, %Macro.Env{} = env) when is_list(options) do
-    options = quote(do: Keyword.put(unquote(options), :print_location, false))
+    options = Keyword.put(options, :print_location, false)
 
     quote do
       IEx.Pry.pry(binding(), __ENV__)
diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex
index 5ab6d8eff04..fe0d938ee03 100644
--- a/lib/iex/lib/iex/server.ex
+++ b/lib/iex/lib/iex/server.ex
@@ -14,6 +14,14 @@ defmodule IEx.Server do
 
   """
 
+  @type run_opts :: [
+          prefix: String.t(),
+          env: Macro.Env.t(),
+          binding: keyword(),
+          on_eof: :stop_evaluator | :halt,
+          register: boolean()
+        ]
+
   @doc false
   defstruct parser_state: [],
             counter: 1,
@@ -35,7 +43,7 @@ defmodule IEx.Server do
 
   """
   @doc since: "1.8.0"
-  @spec run(keyword) :: :ok
+  @spec run(run_opts) :: :ok
   def run(opts) when is_list(opts) do
     if Keyword.get(opts, :register, true) do
       IEx.Broker.register(self())
diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs
index 99b526f7efa..39d454f50ef 100644
--- a/lib/iex/test/iex/autocomplete_test.exs
+++ b/lib/iex/test/iex/autocomplete_test.exs
@@ -62,7 +62,16 @@ defmodule IEx.AutocompleteTest do
     assert expand(~c"t String") == {:yes, ~c"", [~c"String", ~c"StringIO"]}
 
     assert expand(~c"t String.") ==
-             {:yes, ~c"", [~c"codepoint/0", ~c"grapheme/0", ~c"pattern/0", ~c"t/0"]}
+             {:yes, ~c"",
+              [
+                ~c"codepoint/0",
+                ~c"grapheme/0",
+                ~c"pattern/0",
+                ~c"replace_opts/0",
+                ~c"split_opts/0",
+                ~c"splitter_opts/0",
+                ~c"t/0"
+              ]}
 
     assert expand(~c"t String.grap") == {:yes, ~c"heme", []}
     assert expand(~c"t  String.grap") == {:yes, ~c"heme", []}
@@ -85,7 +94,16 @@ defmodule IEx.AutocompleteTest do
     assert expand(~c"t(String") == {:yes, ~c"", [~c"String", ~c"StringIO"]}
 
     assert expand(~c"t(String.") ==
-             {:yes, ~c"", [~c"codepoint/0", ~c"grapheme/0", ~c"pattern/0", ~c"t/0"]}
+             {:yes, ~c"",
+              [
+                ~c"codepoint/0",
+                ~c"grapheme/0",
+                ~c"pattern/0",
+                ~c"replace_opts/0",
+                ~c"split_opts/0",
+                ~c"splitter_opts/0",
+                ~c"t/0"
+              ]}
 
     assert expand(~c"t(String.grap") == {:yes, ~c"heme", []}
   end
diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs
index 10d9ecf5735..77f35f5529e 100644
--- a/lib/iex/test/iex/helpers_test.exs
+++ b/lib/iex/test/iex/helpers_test.exs
@@ -967,7 +967,7 @@ defmodule IEx.HelpersTest do
 
   describe "t" do
     test "prints when there is no type information or the type is private" do
-      assert capture_io(fn -> t(IEx) end) == "No type information for IEx was found\n"
+      assert capture_io(fn -> t(List) end) == "No type information for List was found\n"
 
       assert capture_io(fn -> t(Enum.doesnt_exist()) end) ==
                "No type information for Enum.doesnt_exist was found or " <>
diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex
index c240ab85578..5591671e8c8 100644
--- a/lib/logger/lib/logger.ex
+++ b/lib/logger/lib/logger.ex
@@ -12,7 +12,7 @@ defmodule Logger do
 
   Overall, you will find that `Logger`:
 
-    * Provides all 7 syslog levels
+    * Provides all 8 syslog levels
       (although debug, info, warning, and error are the most commonly used).
 
     * Supports both message-based and structural logging.
@@ -507,6 +507,30 @@ defmodule Logger do
   @type report :: map() | keyword()
   @type message :: :unicode.chardata() | String.Chars.t() | report()
   @type metadata :: keyword()
+
+  @type configure_opts :: [
+          level: level(),
+          translator_inspect_opts: Inspect.Opts.t(),
+          sync_threshold: non_neg_integer(),
+          discard_threshold: non_neg_integer(),
+          truncate: non_neg_integer() | :infinity,
+          utc_log: boolean()
+        ]
+
+  @type formatter_opts :: [
+          colors: [
+            enabled: boolean(),
+            debug: atom(),
+            info: atom(),
+            warning: atom(),
+            error: atom()
+          ],
+          format: String.t() | {module(), atom()},
+          metadata: :all | [atom()],
+          truncate: pos_integer() | :infinity,
+          utc_log: boolean()
+        ]
+
   @new_erlang_levels [:emergency, :alert, :critical, :warning, :notice]
   @levels [:error, :info, :debug] ++ @new_erlang_levels
   @metadata :logger_level
@@ -552,7 +576,7 @@ defmodule Logger do
   instead.
   """
   @doc since: "1.15.0"
-  @spec default_formatter(keyword) :: {module, :logger.formatter_config()}
+  @spec default_formatter(formatter_opts) :: {module, :logger.formatter_config()}
   def default_formatter(overrides \\ []) when is_list(overrides) do
     Application.get_env(:logger, :default_formatter, [])
     |> Keyword.merge(overrides)
@@ -712,7 +736,7 @@ defmodule Logger do
     :translator_inspect_opts
   ]
   @backend_options [:sync_threshold, :discard_threshold, :truncate, :utc_log]
-  @spec configure(keyword) :: :ok
+  @spec configure(configure_opts) :: :ok
   def configure(options) do
     for {k, v} <- options do
       cond do
@@ -973,7 +997,7 @@ defmodule Logger do
   anonymous functions to `bare_log/3` and they will only be evaluated
   if there is something to be logged.
   """
-  @spec bare_log(level, message | (-> message | {message, keyword}), keyword) :: :ok
+  @spec bare_log(level, message | (-> message | {message, keyword}), metadata) :: :ok
   def bare_log(level, message_or_fun, metadata \\ []) do
     level = elixir_level_to_erlang_level(level)
 
diff --git a/lib/logger/lib/logger/backends/internal.ex b/lib/logger/lib/logger/backends/internal.ex
index afef580a6e5..4af6c2e117b 100644
--- a/lib/logger/lib/logger/backends/internal.ex
+++ b/lib/logger/lib/logger/backends/internal.ex
@@ -8,13 +8,32 @@ defmodule Logger.Backends.Internal do
   @name __MODULE__
   @type backend :: :gen_event.handler()
 
+  @typedoc """
+  Configuration options for Logger backends.
+
+  These are the built-in backend options that can be configured at runtime.
+  """
+  @type backend_config_opts :: [
+          sync_threshold: pos_integer(),
+          discard_threshold: pos_integer() | :infinity,
+          truncate: pos_integer() | :infinity,
+          utc_log: boolean()
+        ]
+
+  @typedoc """
+  Options for `add/2` and `remove/2` operations.
+  """
+  @type add_remove_opts :: [
+          flush: boolean()
+        ]
+
   @doc """
   Apply runtime configuration to all backends.
 
   See the module doc for more information.
   """
   @backend_options [:sync_threshold, :discard_threshold, :truncate, :utc_log]
-  @spec configure(keyword) :: :ok
+  @spec configure(backend_config_opts) :: :ok
   def configure(options) do
     ensure_started()
     Logger.Backends.Config.configure(Keyword.take(options, @backend_options))
@@ -65,7 +84,7 @@ defmodule Logger.Backends.Internal do
       {:ok, _pid} = Logger.add_backend(MyBackend, flush: true)
 
   """
-  @spec add(backend, keyword) :: Supervisor.on_start_child()
+  @spec add(backend, add_remove_opts) :: Supervisor.on_start_child()
   def add(backend, opts \\ []) do
     ensure_started()
     _ = if opts[:flush], do: Logger.flush()
@@ -91,7 +110,7 @@ defmodule Logger.Backends.Internal do
       to `Logger` are processed before the backend is removed
 
   """
-  @spec remove(backend, keyword) :: :ok | {:error, term}
+  @spec remove(backend, add_remove_opts) :: :ok | {:error, term}
   def remove(backend, opts \\ []) do
     ensure_started()
     _ = if opts[:flush], do: Logger.flush()
diff --git a/lib/logger/lib/logger/formatter.ex b/lib/logger/lib/logger/formatter.ex
index 858449bc27c..2ed22d88af3 100644
--- a/lib/logger/lib/logger/formatter.ex
+++ b/lib/logger/lib/logger/formatter.ex
@@ -8,7 +8,7 @@ defmodule Logger.Formatter do
   @moduledoc ~S"""
   Conveniences and built-in formatter for logs.
 
-  This modules defines a suitable `:logger` formatter which formats
+  This module defines a suitable `:logger` formatter which formats
   messages and reports as Elixir terms and also provides additional
   functionality, such as timezone conversion, truncation, and coloring.
   This formatter is used by default by `Logger` and you can configure it
@@ -99,6 +99,20 @@ defmodule Logger.Formatter do
   @type date_time_ms :: {date, time_ms}
 
   @type pattern :: :date | :level | :levelpad | :message | :metadata | :node | :time
+
+  @type new_opts :: [
+          colors: [
+            enabled: boolean(),
+            debug: atom(),
+            info: atom(),
+            warning: atom(),
+            error: atom()
+          ],
+          format: String.t() | {module(), atom()},
+          metadata: :all | [atom()],
+          truncate: pos_integer() | :infinity,
+          utc_log: boolean()
+        ]
   @valid_patterns [:time, :date, :message, :level, :node, :metadata, :levelpad]
   @default_pattern "\n$time $metadata[$level] $message\n"
   @replacement "�"
@@ -152,7 +166,7 @@ defmodule Logger.Formatter do
   The color of the message can also be configured per message via
   the `:ansi_color` metadata.
   """
-  @spec new(keyword) :: formatter when formatter: term
+  @spec new(new_opts) :: formatter when formatter: term
   def new(options \\ []) do
     template = compile(options[:format])
     colors = colors(options[:colors] || [])
diff --git a/lib/logger/lib/logger/utils.ex b/lib/logger/lib/logger/utils.ex
index 5492740714d..302e4829e8c 100644
--- a/lib/logger/lib/logger/utils.ex
+++ b/lib/logger/lib/logger/utils.ex
@@ -127,6 +127,12 @@ defmodule Logger.Utils do
   For information about format scanning and how to consume them,
   check `:io_lib.scan_format/2`.
   """
+  @spec scan_inspect(
+          atom() | binary() | charlist(),
+          list(),
+          non_neg_integer() | :infinity,
+          Inspect.Opts.t()
+        ) :: [:io_lib.format_spec()]
   def scan_inspect(format, args, truncate, opts \\ %Inspect.Opts{})
 
   def scan_inspect(format, args, truncate, opts) when is_atom(format) do
diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex
index a4b97a21f3a..52a169aa02d 100644
--- a/lib/mix/lib/mix.ex
+++ b/lib/mix/lib/mix.ex
@@ -424,6 +424,25 @@ defmodule Mix do
 
   """
 
+  @typedoc """
+  Options for `install/2`.
+
+  See the `install/2` function documentation for detailed information about each option.
+  """
+  @type install_opts :: [
+          force: boolean(),
+          verbose: boolean(),
+          consolidate_protocols: boolean(),
+          elixir: String.t(),
+          system_env: Enum.t(),
+          config: keyword(),
+          config_path: String.t() | atom(),
+          lockfile: String.t() | atom(),
+          start_applications: boolean(),
+          compilers: [atom()],
+          runtime_config: keyword()
+        ]
+
   @mix_install_project Mix.InstallProject
   @mix_install_app :mix_install
   @mix_install_app_string Atom.to_string(@mix_install_app)
@@ -827,7 +846,7 @@ defmodule Mix do
   ## Environment variables
 
   The `MIX_INSTALL_DIR` environment variable configures the directory that
-  caches all `Mix.install/2`. It defaults to the "mix/install" folder in the
+  caches all `Mix.install/2`. It defaults to the `mix/install` folder in the
   default user cache of your operating system. You can use `install_project_dir/0`
   to access the directory of an existing install (alongside other installs):
 
@@ -845,6 +864,10 @@ defmodule Mix do
   if `:force` is enabled.
   """
   @doc since: "1.12.0"
+  @spec install(
+          [atom() | {atom(), String.t()} | {atom(), String.t(), keyword()} | {atom(), keyword()}],
+          install_opts()
+        ) :: :ok
   def install(deps, opts \\ [])
 
   def install(deps, opts) when is_list(deps) and is_list(opts) do
@@ -884,6 +907,7 @@ defmodule Mix do
         config: [],
         config_path: nil,
         consolidate_protocols: true,
+        compilers: [:elixir],
         elixir: nil,
         force: false,
         lockfile: nil,
@@ -898,6 +922,7 @@ defmodule Mix do
     system_env = Keyword.fetch!(opts, :system_env)
     consolidate_protocols? = Keyword.fetch!(opts, :consolidate_protocols)
     start_applications? = Keyword.fetch!(opts, :start_applications)
+    compilers = Keyword.fetch!(opts, :compilers)
 
     id =
       {deps, config, system_env, consolidate_protocols?}
@@ -923,7 +948,8 @@ defmodule Mix do
         dynamic_config = [
           deps: deps,
           consolidate_protocols: consolidate_protocols?,
-          config_path: config_path
+          config_path: config_path,
+          compilers: compilers
         ]
 
         :ok =
@@ -1093,7 +1119,6 @@ defmodule Mix do
       app: @mix_install_app,
       erlc_paths: [],
       elixirc_paths: [],
-      compilers: [:elixir],
       prune_code_paths: false
     ] ++ dynamic_config
   end
diff --git a/lib/mix/lib/mix/compilers/erlang.ex b/lib/mix/lib/mix/compilers/erlang.ex
index ab94b0a03de..84af2218ea4 100644
--- a/lib/mix/lib/mix/compilers/erlang.ex
+++ b/lib/mix/lib/mix/compilers/erlang.ex
@@ -7,6 +7,17 @@ defmodule Mix.Compilers.Erlang do
 
   @manifest_vsn 1
 
+  @typedoc """
+  Options for `compile/6`.
+  """
+  @type compile_opts :: [
+          all_warnings: boolean(),
+          force: boolean(),
+          parallel: boolean() | MapSet.t(Path.t()),
+          preload: (-> term()),
+          verbose: boolean()
+        ]
+
   @doc """
   Compiles the given `mappings`.
 
@@ -18,6 +29,8 @@ defmodule Mix.Compilers.Erlang do
 
   ## Options
 
+    * `:all_warnings` - a flag to disable showing all previous warnings
+
     * `:force` - forces compilation regardless of modification times
 
     * `:parallel` - a mapset of files to compile in parallel
@@ -25,6 +38,8 @@ defmodule Mix.Compilers.Erlang do
     * `:preload` - any code that must be preloaded if any pending
       entry needs to be compiled
 
+    * `:verbose` - a flag to enable verbose printing
+
   ## Examples
 
   For example, a simple compiler for Lisp Flavored Erlang
@@ -58,6 +73,14 @@ defmodule Mix.Compilers.Erlang do
   `{:error, errors, warnings}` in case of error. This function returns
   `{status, diagnostics}` as specified in `Mix.Task.Compiler`.
   """
+  @spec compile(
+          Path.t(),
+          [{Path.t(), Path.t()}],
+          atom(),
+          atom(),
+          compile_opts,
+          (Path.t(), Path.t() -> {:ok, term(), [term()]} | {:error, [term()], [term()]})
+        ) :: {Mix.Task.Compiler.status(), [Mix.Task.Compiler.Diagnostic.t()]}
   def compile(manifest, mappings, src_ext, dest_ext, opts, callback)
       when is_atom(src_ext) and is_atom(dest_ext) and is_list(opts) do
     force = opts[:force]
diff --git a/lib/mix/lib/mix/dep.ex b/lib/mix/lib/mix/dep.ex
index 1dd953b4cd9..1ebb3150df1 100644
--- a/lib/mix/lib/mix/dep.ex
+++ b/lib/mix/lib/mix/dep.ex
@@ -415,7 +415,7 @@ defmodule Mix.Dep do
       |> Kernel.++(if manager, do: [manager: manager], else: [])
       |> Kernel.++(if system_env != [], do: [system_env: system_env], else: [])
       |> Kernel.++(opts)
-      |> Keyword.drop([:dest, :build, :lock, :manager, :checkout])
+      |> Keyword.drop([:dest, :build, :lock, :manager, :checkout, :app_properties])
 
     info = if req, do: {app, req, opts}, else: {app, opts}
     "\n  > In #{Path.relative_to_cwd(from)}:\n    #{inspect(info)}\n"
diff --git a/lib/mix/lib/mix/dep/lock.ex b/lib/mix/lib/mix/dep/lock.ex
index 9f031024782..a8ec7192f56 100644
--- a/lib/mix/lib/mix/dep/lock.ex
+++ b/lib/mix/lib/mix/dep/lock.ex
@@ -9,6 +9,14 @@
 defmodule Mix.Dep.Lock do
   @moduledoc false
 
+  @typedoc """
+  Options for `write/2`.
+  """
+  @type write_opts :: [
+          file: Path.t(),
+          check_locked: boolean()
+        ]
+
   @doc """
   Reads the lockfile, returns a map containing
   each app name and its current lock information.
@@ -30,7 +38,7 @@ defmodule Mix.Dep.Lock do
   @doc """
   Receives a map and writes it as the latest lock.
   """
-  @spec write(map(), keyword) :: :ok
+  @spec write(map(), write_opts) :: :ok
   def write(map, opts \\ []) do
     lockfile = opts[:file] || lockfile()
 
diff --git a/lib/mix/lib/mix/generator.ex b/lib/mix/lib/mix/generator.ex
index 1e8af77ef60..e04e4ca30dd 100644
--- a/lib/mix/lib/mix/generator.ex
+++ b/lib/mix/lib/mix/generator.ex
@@ -7,6 +7,12 @@ defmodule Mix.Generator do
   Conveniences for working with paths and generating content.
   """
 
+  @type generator_opts :: [
+          force: boolean(),
+          quiet: boolean(),
+          format_elixir: boolean()
+        ]
+
   @doc ~S"""
   Creates a file with the given contents.
 
@@ -17,6 +23,7 @@ defmodule Mix.Generator do
 
     * `:force` - forces creation without a shell prompt
     * `:quiet` - does not log command output
+    * `:format_elixir` (since v1.18.0) - if `true`, apply formatter to the generated file
 
   ## Examples
 
@@ -25,7 +32,7 @@ defmodule Mix.Generator do
       true
 
   """
-  @spec create_file(Path.t(), iodata, keyword) :: boolean()
+  @spec create_file(Path.t(), iodata, generator_opts) :: boolean()
   def create_file(path, contents, opts \\ []) when is_binary(path) do
     log(:green, :creating, Path.relative_to_cwd(path), opts)
 
@@ -62,7 +69,7 @@ defmodule Mix.Generator do
       true
 
   """
-  @spec create_directory(Path.t(), keyword) :: true
+  @spec create_directory(Path.t(), quiet: boolean()) :: true
   def create_directory(path, options \\ []) when is_binary(path) do
     log(:green, "creating", Path.relative_to_cwd(path), options)
     File.mkdir_p!(path)
@@ -79,6 +86,7 @@ defmodule Mix.Generator do
 
     * `:force` - forces copying without a shell prompt
     * `:quiet` - does not log command output
+    * `:format_elixir` (since v1.18.0) - if `true`, apply formatter to the generated file
 
   ## Examples
 
@@ -88,7 +96,7 @@ defmodule Mix.Generator do
 
   """
   @doc since: "1.9.0"
-  @spec copy_file(Path.t(), Path.t(), keyword) :: boolean()
+  @spec copy_file(Path.t(), Path.t(), generator_opts) :: boolean()
   def copy_file(source, target, options \\ []) do
     create_file(target, File.read!(source), options)
   end
@@ -116,7 +124,7 @@ defmodule Mix.Generator do
 
   """
   @doc since: "1.9.0"
-  @spec copy_template(Path.t(), Path.t(), keyword, keyword) :: boolean()
+  @spec copy_template(Path.t(), Path.t(), keyword, generator_opts) :: boolean()
   def copy_template(source, target, assigns, options \\ []) do
     create_file(target, EEx.eval_file(source, assigns: assigns), options)
   end
diff --git a/lib/mix/lib/mix/local/installer.ex b/lib/mix/lib/mix/local/installer.ex
index b5180d87168..10d8d0938ce 100644
--- a/lib/mix/lib/mix/local/installer.ex
+++ b/lib/mix/lib/mix/local/installer.ex
@@ -23,10 +23,27 @@ defmodule Mix.Local.Installer do
           | {:url, url :: binary}
           | {:fetcher, dep_spec :: tuple}
 
+  @typedoc """
+  Options for installer functions.
+
+  These options are used by various installer functions to control behavior
+  during installation, parsing, and uninstallation.
+  """
+  @type installer_opts :: [
+          app: String.t(),
+          submodules: boolean(),
+          sparse: String.t(),
+          force: boolean(),
+          sha512: String.t(),
+          organization: String.t(),
+          repo: String.t(),
+          timeout: pos_integer()
+        ]
+
   @doc """
   Checks that the `install_spec` and `opts` are supported by the respective module.
   """
-  @callback check_install_spec(install_spec, opts :: keyword) :: :ok | {:error, String.t()}
+  @callback check_install_spec(install_spec, opts :: installer_opts) :: :ok | {:error, String.t()}
 
   @doc """
   Returns a list of already installed version of the same artifact.
@@ -37,7 +54,7 @@ defmodule Mix.Local.Installer do
   Builds a local artifact either from a remote dependency or for
   the current project.
   """
-  @callback build(install_spec, opts :: Keyword.t()) :: Path.t()
+  @callback build(install_spec, opts :: installer_opts) :: Path.t()
 
   @doc """
   The installation itself.
@@ -175,7 +192,7 @@ defmodule Mix.Local.Installer do
   @doc """
   Receives `argv` and `opts` from options parsing and returns an `install_spec`.
   """
-  @spec parse_args([String.t()], keyword) :: install_spec | {:error, String.t()}
+  @spec parse_args([String.t()], installer_opts) :: install_spec | {:error, String.t()}
   def parse_args(argv, opts)
 
   def parse_args([], _opts) do
diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex
index 72f648967ec..060c533c885 100644
--- a/lib/mix/lib/mix/project.ex
+++ b/lib/mix/lib/mix/project.ex
@@ -130,6 +130,67 @@ defmodule Mix.Project do
   makes sure Elixir is not added as a dependency to the generated `.app` file or
   to the escript generated with `mix escript.build`, and so on.
 
+  ## Umbrella projects
+
+  Umbrella projects are a convenience to help you organize and manage multiple
+  applications. While it provides a degree of separation between applications,
+  those applications are not fully decoupled, as they share the same configuration
+  and the same dependencies.
+
+  In an umbrella project, you have an `apps/` folder where you store each application.
+  Then, instead of each app in the umbrella having its own configuration, build cache,
+  lockfile and so, they all point to the parent project by specifying the following
+  configuration in their `mix.exs`:
+
+      build_path: "../../_build",
+      config_path: "../../config/config.exs",
+      deps_path: "../../deps",
+      lockfile: "../../mix.lock",
+
+  The pattern of keeping multiple applications in the same repository is known as
+  [monorepo](https://en.wikipedia.org/wiki/Monorepo). Umbrella projects maximize
+  this pattern by providing conveniences to compile, test and run multiple
+  applications at once. When an umbrella application needs to depend on another
+  one, it can be done by passing the `in_umbrella: true` option to your dependency.
+  If an umbrella application `:foo` depends on its sibling `:bar`, you can specify
+  this dependency in `foo`'s `mix.exs` file as:
+
+      {:bar, in_umbrella: true}
+
+  ### Undoing umbrellas
+
+  Using umbrella projects can impact how you design and write your software and,
+  as time passes, they may turn out to be the wrong choice.
+  If you find yourself in a position where you want to use different configurations
+  in each application for the same dependency or use different dependency versions,
+  then it is likely your codebase has grown beyond what umbrellas can provide.
+
+  If you find yourself in this situation, you have two options:
+
+    1. Convert everything into a single Mix project, which can be done in steps.
+       First move all files in `lib`, `test`, `priv`, and friends into a single
+       application, while still keeping the overall umbrella structure and
+       `mix.exs` files. For example, if your umbrellas has three applications,
+       `foo`, `bar` and `baz`, where `baz` depends on both `foo` and `bar`,
+       move all source to `baz`. Then remove `foo` and `bar` one by one,
+       updating any configuration and removing references to the `:foo` and
+      `:bar` application names. Until you have only a single application.
+
+    2. Remove umbrella structure while keeping them as distinct applications.
+       This is done by moving applications outside of the umbrella
+       project's `apps/` directory and updating the projects' `mix.exs` files
+       to no longer set the `build_path`, `config_path`, `deps_path`, and
+       `lockfile` configurations, guaranteeing each of them have their own
+       build and dependency structure.
+
+  Keep in mind that umbrellas are one of many options for managing private
+  packages within your organization. You might:
+
+    1. Have multiple directories inside the same repository and using `:path`
+       dependencies (which is essentially the monorepo pattern)
+    2. Use private Git repositories and Mix' ability to fetch Git dependencies
+    3. Publishing packages to a private [Hex.pm](https://hex.pm/) organization
+
   ## Invoking this module
 
   This module contains many functions that return project information and
@@ -158,6 +219,22 @@ defmodule Mix.Project do
 
   """
 
+  @type build_structure_opts :: [
+          symlink_ebin: boolean(),
+          source: String.t()
+        ]
+
+  @typedoc """
+  Options for dependency traversal functions.
+
+  These options control how dependency trees are traversed and filtered
+  in functions like `deps_scms/1`, `deps_paths/1`, and `deps_tree/1`.
+  """
+  @type deps_traversal_opts :: [
+          depth: pos_integer(),
+          parents: [atom()]
+        ]
+
   @doc false
   defmacro __using__(_) do
     quote do
@@ -540,7 +617,7 @@ defmodule Mix.Project do
 
   """
   @doc since: "1.10.0"
-  @spec deps_scms(keyword) :: %{optional(atom) => Mix.SCM.t()}
+  @spec deps_scms(deps_traversal_opts) :: %{optional(atom) => Mix.SCM.t()}
   def deps_scms(opts \\ []) when is_list(opts) do
     traverse_deps(opts, fn %{scm: scm} -> scm end)
   end
@@ -561,7 +638,7 @@ defmodule Mix.Project do
       #=> %{foo: "deps/foo", bar: "custom/path/dep"}
 
   """
-  @spec deps_paths(keyword) :: %{optional(atom) => Path.t()}
+  @spec deps_paths(deps_traversal_opts) :: %{optional(atom) => Path.t()}
   def deps_paths(opts \\ []) when is_list(opts) do
     traverse_deps(opts, fn %{opts: opts} -> opts[:dest] end)
   end
@@ -583,7 +660,7 @@ defmodule Mix.Project do
 
   """
   @doc since: "1.15.0"
-  @spec deps_tree(keyword) :: %{optional(atom) => [atom]}
+  @spec deps_tree(deps_traversal_opts) :: %{optional(atom) => [atom]}
   def deps_tree(opts \\ []) when is_list(opts) do
     traverse_deps(opts, fn %{deps: deps} -> Enum.map(deps, & &1.app) end)
   end
@@ -843,8 +920,11 @@ defmodule Mix.Project do
 
     * `:symlink_ebin` - symlink ebin instead of copying it
 
+    * `:source` - the source directory to copy from.
+      Defaults to the current working directory.
+
   """
-  @spec build_structure(keyword, keyword) :: :ok
+  @spec build_structure(keyword, build_structure_opts) :: :ok
   def build_structure(config \\ config(), opts \\ []) do
     source = opts[:source] || File.cwd!()
     target = app_path(config)
@@ -878,7 +958,7 @@ defmodule Mix.Project do
 
   `opts` are the same options that can be passed to `build_structure/2`.
   """
-  @spec ensure_structure(keyword, keyword) :: :ok
+  @spec ensure_structure(keyword, build_structure_opts) :: :ok
   def ensure_structure(config \\ config(), opts \\ []) do
     if File.exists?(app_path(config)) do
       :ok
diff --git a/lib/mix/lib/mix/release.ex b/lib/mix/lib/mix/release.ex
index dc066d64f89..d31a9b9b1f9 100644
--- a/lib/mix/lib/mix/release.ex
+++ b/lib/mix/lib/mix/release.ex
@@ -68,6 +68,23 @@ defmodule Mix.Release do
           steps: [(t -> t) | :assemble, ...]
         }
 
+  @typedoc """
+  Options for stripping BEAM files.
+  """
+  @type strip_beam_opts :: [
+          keep: [String.t()],
+          compress: boolean()
+        ]
+
+  @typedoc """
+  Erlang/OTP sys.config structure.
+
+  A list of tuples where each tuple contains an application name and its
+  configuration as a keyword list. This is the standard format for Erlang
+  application configuration.
+  """
+  @type sys_config :: [{application(), keyword()}]
+
   @default_apps [kernel: :permanent, stdlib: :permanent, elixir: :permanent, sasl: :permanent]
   @safe_modes [:permanent, :temporary, :transient]
   @unsafe_modes [:load, :none]
@@ -445,7 +462,7 @@ defmodule Mix.Release do
 
   In case there are no config providers, it doesn't change `sys_config`.
   """
-  @spec make_sys_config(t, keyword(), Config.Provider.config_path()) ::
+  @spec make_sys_config(t, sys_config(), Config.Provider.config_path()) ::
           :ok | {:error, String.t()}
   def make_sys_config(release, sys_config, config_provider_path) do
     {sys_config, runtime_config?} =
@@ -900,8 +917,25 @@ defmodule Mix.Release do
 
   The exact chunks that are kept are not documented and may change in
   future versions.
+
+  ## Options
+
+    * `:keep` - a list of additional chunk names (as strings) to keep in the
+      stripped BEAM file beyond those required by Erlang/Elixir
+
+    * `:compress` - when `true`, the resulting BEAM file will be compressed
+      using gzip. Defaults to `false`
+
+  ## Examples
+
+      # Strip with default options
+      Mix.Release.strip_beam(beam_binary)
+
+      # Keep additional chunks and compress
+      Mix.Release.strip_beam(beam_binary, keep: ["Docs", "ChunkName"], compress: true)
+
   """
-  @spec strip_beam(binary(), keyword()) :: {:ok, binary()} | {:error, :beam_lib, term()}
+  @spec strip_beam(binary(), strip_beam_opts()) :: {:ok, binary()} | {:error, :beam_lib, term()}
   def strip_beam(binary, options \\ []) when is_list(options) do
     chunks_to_keep = options[:keep] |> List.wrap() |> Enum.map(&String.to_charlist/1)
     all_chunks = Enum.uniq(@additional_chunks ++ :beam_lib.significant_chunks() ++ chunks_to_keep)
diff --git a/lib/mix/lib/mix/shell.ex b/lib/mix/lib/mix/shell.ex
index 75770ef7a8a..d3e544b8c2d 100644
--- a/lib/mix/lib/mix/shell.ex
+++ b/lib/mix/lib/mix/shell.ex
@@ -7,6 +7,29 @@ defmodule Mix.Shell do
   Defines `Mix.Shell` contract.
   """
 
+  @type cmd_opts :: [
+          {:print_app, boolean()}
+          | {:stderr_to_stdout, boolean()}
+          | {:quiet, boolean()}
+          | {:env, [{String.t(), String.t()}]}
+          | {:cd, String.t()}
+          | {atom(), term()}
+        ]
+
+  @type yes_opts :: [
+          {:default, :yes | :no}
+          | {atom(), term()}
+        ]
+
+  @type stream_cmd_opts :: [
+          {:cd, String.t()}
+          | {:stderr_to_stdout, boolean()}
+          | {:use_stdio, boolean()}
+          | {:env, [{String.t(), String.t()}]}
+          | {:quiet, boolean()}
+          | {atom(), term()}
+        ]
+
   @doc false
   defstruct [:callback]
 
@@ -48,7 +71,7 @@ defmodule Mix.Shell do
 
   All the built-in shells support these.
   """
-  @callback cmd(command :: String.t(), options :: keyword) :: integer
+  @callback cmd(command :: String.t(), options :: cmd_opts) :: integer
 
   @doc """
   Prompts the user for input.
@@ -69,7 +92,7 @@ defmodule Mix.Shell do
 
     * `:default` - `:yes` or `:no` (the default is `:yes`)
   """
-  @callback yes?(message :: binary, options :: keyword) :: boolean
+  @callback yes?(message :: binary, options :: yes_opts) :: boolean
 
   @doc """
   Prints the current application to the shell if
@@ -108,16 +131,16 @@ defmodule Mix.Shell do
 
     * `:cd` *(since v1.11.0)* - the directory to run the command in
 
-    * `:stderr_to_stdout` - redirects stderr to stdout, defaults to true, unless use_stdio is set to false
+    * `:stderr_to_stdout` - redirects stderr to stdout, defaults to `true`, unless `:use_stdio` is set to `false`
 
-    * `:use_stdio` - controls whether the command should use stdin / stdout / stdrr, defaults to true
+    * `:use_stdio` - controls whether the command should use `stdin` / `stdout` / `stdrr`, defaults to `true`
 
     * `:env` - a list of environment variables, defaults to `[]`
 
     * `:quiet` - overrides the callback to no-op
 
   """
-  @spec cmd(String.t() | {String.t(), [String.t()]}, keyword, (binary -> term)) ::
+  @spec cmd(String.t() | {String.t(), [String.t()]}, stream_cmd_opts, (binary -> term)) ::
           exit_status :: non_neg_integer
   def cmd(command, options \\ [], callback) when is_function(callback, 1) do
     callback =
diff --git a/lib/mix/lib/mix/shell/io.ex b/lib/mix/lib/mix/shell/io.ex
index 57aabfc5524..0988272954c 100644
--- a/lib/mix/lib/mix/shell/io.ex
+++ b/lib/mix/lib/mix/shell/io.ex
@@ -11,6 +11,11 @@ defmodule Mix.Shell.IO do
 
   @behaviour Mix.Shell
 
+  @typedoc """
+  Options for `yes?/2`.
+  """
+  @type yes_opts :: [default: :yes | :no]
+
   @doc """
   Prints the current application to the shell if it
   was not printed yet.
@@ -72,6 +77,7 @@ defmodule Mix.Shell.IO do
       end
 
   """
+  @spec yes?(String.t(), yes_opts) :: boolean()
   def yes?(message, options \\ []) do
     default = Keyword.get(options, :default, :yes)
 
diff --git a/lib/mix/lib/mix/shell/quiet.ex b/lib/mix/lib/mix/shell/quiet.ex
index 6c5f0c0dbbf..22fec04943f 100644
--- a/lib/mix/lib/mix/shell/quiet.ex
+++ b/lib/mix/lib/mix/shell/quiet.ex
@@ -52,11 +52,14 @@ defmodule Mix.Shell.Quiet do
       the prompt instead. Defaults to `:yes`.
 
   """
+  @spec yes?(String.t(), Mix.Shell.IO.yes_opts()) :: boolean()
   defdelegate yes?(message, options \\ []), to: Mix.Shell.IO
 
   @doc """
   Executes the given command quietly without outputting anything.
   """
+  @spec cmd(String.t() | {String.t(), [String.t()]}, Mix.Shell.stream_cmd_opts()) ::
+          exit_status :: non_neg_integer
   def cmd(command, opts \\ []) do
     Mix.Shell.cmd(command, opts, fn data -> data end)
   end
diff --git a/lib/mix/lib/mix/sync/lock.ex b/lib/mix/lib/mix/sync/lock.ex
index 4705f7602e9..9f13b853435 100644
--- a/lib/mix/lib/mix/sync/lock.ex
+++ b/lib/mix/lib/mix/sync/lock.ex
@@ -73,6 +73,13 @@ defmodule Mix.Sync.Lock do
   @probe_data_size byte_size(@probe_data)
   @probe_timeout_ms 5_000
 
+  @typedoc """
+  Options for `with_lock/3`.
+  """
+  @type with_lock_opts :: [
+          on_taken: (String.t() -> any())
+        ]
+
   @doc """
   Acquires a lock identified by the given key.
 
@@ -95,7 +102,7 @@ defmodule Mix.Sync.Lock do
       is successfully acquired by this process.
 
   """
-  @spec with_lock(iodata(), (-> term()), keyword()) :: term()
+  @spec with_lock(iodata(), (-> term()), with_lock_opts()) :: term()
   def with_lock(key, fun, opts \\ []) do
     opts = Keyword.validate!(opts, [:on_taken])
 
diff --git a/lib/mix/lib/mix/tasks/archive.build.ex b/lib/mix/lib/mix/tasks/archive.build.ex
index 2330889e564..9d9a763a7b8 100644
--- a/lib/mix/lib/mix/tasks/archive.build.ex
+++ b/lib/mix/lib/mix/tasks/archive.build.ex
@@ -36,7 +36,7 @@ defmodule Mix.Tasks.Archive.Build do
   ## Command line options
 
     * `-o` - specifies output file name.
-      If there is a `mix.exs`, defaults to "APP-VERSION.ez".
+      If there is a `mix.exs`, defaults to `APP-VERSION.ez`.
 
     * `-i` - specifies the input directory to archive.
       If there is a `mix.exs`, defaults to the current application build.
diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex
index 92b272e04b2..7383b4a8802 100644
--- a/lib/mix/lib/mix/tasks/compile.app.ex
+++ b/lib/mix/lib/mix/tasks/compile.app.ex
@@ -100,8 +100,8 @@ defmodule Mix.Tasks.Compile.App do
 
     * `:reliable_dir_mtime` - this task relies on the operating system
       changing the mtime on a directory whenever a file is added or removed.
-      You can set this option to false if your system does not provide
-      reliable mtimes. Defaults to false on Windows.
+      You can set this option to `false` if your system does not provide
+      reliable mtimes. Defaults to `false` on Windows.
 
   ## Phases
 
diff --git a/lib/mix/lib/mix/tasks/compile.erlang.ex b/lib/mix/lib/mix/tasks/compile.erlang.ex
index 2140b4090ac..2261c180cb0 100644
--- a/lib/mix/lib/mix/tasks/compile.erlang.ex
+++ b/lib/mix/lib/mix/tasks/compile.erlang.ex
@@ -8,7 +8,7 @@ defmodule Mix.Tasks.Compile.Erlang do
 
   @recursive true
   @manifest "compile.erlang"
-  @switches [force: :boolean, all_warnings: :boolean]
+  @switches [force: :boolean, verbose: :boolean, all_warnings: :boolean]
 
   @moduledoc """
   Compiles Erlang source files.
@@ -23,6 +23,7 @@ defmodule Mix.Tasks.Compile.Erlang do
     * `--all-warnings` (`--no-all-warnings`) - prints all warnings, including previous compilations
       (default is true except on errors)
     * `--force` - forces compilation regardless of modification times
+    * `--verbose` - prints verbose output
 
   ## Configuration
 
diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex
index 88f296937e2..edf2f6880c4 100644
--- a/lib/mix/lib/mix/tasks/compile.ex
+++ b/lib/mix/lib/mix/tasks/compile.ex
@@ -61,6 +61,8 @@ defmodule Mix.Tasks.Compile do
     * `--erl-config` - path to an Erlang term file that will be loaded as Mix config
     * `--force` - forces compilation
     * `--list` - lists all enabled compilers
+    * `--listeners` - starts Mix listeners (they are started by default,
+      unless `--no-listeners` or `--no-deps-check` are given)
     * `--no-app-loading` - does not load .app resource file after compilation
     * `--no-archives-check` - skips checking of archives
     * `--no-compile` - does not actually compile, only loads code and perform checks
diff --git a/lib/mix/lib/mix/tasks/compile.leex.ex b/lib/mix/lib/mix/tasks/compile.leex.ex
index 3eebff22444..deef8dec163 100644
--- a/lib/mix/lib/mix/tasks/compile.leex.ex
+++ b/lib/mix/lib/mix/tasks/compile.leex.ex
@@ -30,6 +30,7 @@ defmodule Mix.Tasks.Compile.Leex do
     * `--all-warnings` (`--no-all-warnings`) - prints all warnings, including previous compilations
       (default is true except on errors)
     * `--force` - forces compilation regardless of modification times
+    * `--verbose` - prints verbose output
 
   ## Configuration
 
diff --git a/lib/mix/lib/mix/tasks/compile.yecc.ex b/lib/mix/lib/mix/tasks/compile.yecc.ex
index 373565e1152..46650336ea0 100644
--- a/lib/mix/lib/mix/tasks/compile.yecc.ex
+++ b/lib/mix/lib/mix/tasks/compile.yecc.ex
@@ -8,7 +8,7 @@ defmodule Mix.Tasks.Compile.Yecc do
 
   @recursive true
   @manifest "compile.yecc"
-  @switches [force: :boolean, all_warnings: :boolean]
+  @switches [force: :boolean, verbose: :boolean, all_warnings: :boolean]
 
   # These options can't be controlled with :yecc_options.
   @forced_opts [report: true, return: true]
@@ -30,6 +30,7 @@ defmodule Mix.Tasks.Compile.Yecc do
     * `--all-warnings` (`--no-all-warnings`) - prints all warnings, including previous compilations
       (default is true except on errors)
     * `--force` - forces compilation regardless of modification times
+    * `--verbose` - prints verbose output
 
   ## Configuration
 
diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex
index 3be40eade8f..2e91535b199 100644
--- a/lib/mix/lib/mix/tasks/deps.compile.ex
+++ b/lib/mix/lib/mix/tasks/deps.compile.ex
@@ -227,9 +227,9 @@ defmodule Mix.Tasks.Deps.Compile do
     end)
   end
 
-  defp do_rebar3(%Mix.Dep{opts: opts} = dep, config) do
-    if not Mix.Rebar.available?(:rebar3) do
-      handle_rebar_not_found(dep)
+  defp do_rebar3(%Mix.Dep{opts: opts, manager: manager} = dep, config) do
+    if not Mix.Rebar.available?(manager) do
+      Mix.Tasks.Local.Rebar.run(["--force"])
     end
 
     dep_path = opts[:dest]
@@ -285,32 +285,6 @@ defmodule Mix.Tasks.Deps.Compile do
     |> Mix.Rebar.serialize_config()
   end
 
-  defp handle_rebar_not_found(%Mix.Dep{app: app, manager: manager}) do
-    shell = Mix.shell()
-
-    shell.info(
-      "Could not find \"#{manager}\", which is needed to build dependency #{inspect(app)}"
-    )
-
-    install_question =
-      "Shall I install #{manager}? (if running non-interactively, " <>
-        "use \"mix local.rebar --force\")"
-
-    if not shell.yes?(install_question) do
-      error_message =
-        "Could not find \"#{manager}\" to compile " <>
-          "dependency #{inspect(app)}, please ensure \"#{manager}\" is available"
-
-      Mix.raise(error_message)
-    end
-
-    Mix.Tasks.Local.Rebar.run(["--force"])
-
-    if not Mix.Rebar.available?(manager) do
-      Mix.raise("\"#{manager}\" installation failed")
-    end
-  end
-
   defp do_make(dep, config) do
     command = make_command(dep)
     shell_cmd!(dep, config, command, [{"IS_DEP", "1"}])
diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex
index ce7080eac42..f17cd078cc2 100644
--- a/lib/mix/lib/mix/tasks/deps.ex
+++ b/lib/mix/lib/mix/tasks/deps.ex
@@ -114,7 +114,7 @@ defmodule Mix.Tasks.Deps do
     * `:runtime` - whether the dependency is part of runtime applications.
       If the `:applications` key is not provided in `def application` in your
       `mix.exs` file, Mix will automatically include all dependencies as a runtime
-      application, except if `runtime: false` is given. Defaults to true.
+      application, except if `runtime: false` is given. Defaults to `true`.
 
     * `:system_env` - an enumerable of key-value tuples of binaries to be set
       as environment variables when loading or compiling the dependency
diff --git a/lib/mix/lib/mix/tasks/deps.loadpaths.ex b/lib/mix/lib/mix/tasks/deps.loadpaths.ex
index ccd084e5bea..8df70169ed4 100644
--- a/lib/mix/lib/mix/tasks/deps.loadpaths.ex
+++ b/lib/mix/lib/mix/tasks/deps.loadpaths.ex
@@ -75,9 +75,11 @@ defmodule Mix.Tasks.Deps.Loadpaths do
 
     Code.prepend_paths(Enum.flat_map(all, &Mix.Dep.load_paths/1), cache: true)
 
-    # For now we only allow listeners defined in dependencies, so
-    # we start them right after adding adding deps to the path
-    if "--no-listeners" not in args do
+    # For now we only allow listeners defined in dependencies,
+    # so we start them right after adding adding deps to the path,
+    # as long as we are sure they have been compiled
+    if "--listeners" in args or
+         ("--no-listeners" not in args and "--no-deps-check" not in args) do
       Mix.PubSub.start_listeners()
     end
 
diff --git a/lib/mix/lib/mix/tasks/deps.partition.ex b/lib/mix/lib/mix/tasks/deps.partition.ex
index 176bfec17f5..73fd291a07f 100644
--- a/lib/mix/lib/mix/tasks/deps.partition.ex
+++ b/lib/mix/lib/mix/tasks/deps.partition.ex
@@ -67,6 +67,7 @@ defmodule Mix.Tasks.Deps.Partition do
     ]
 
     options = [
+      :exit_status,
       :binary,
       :hide,
       :use_stdio,
@@ -164,12 +165,10 @@ defmodule Mix.Tasks.Deps.Partition do
         send_deps_and_server_loop([client | available], busy, deps, status)
 
       {:tcp_closed, socket} ->
-        shutdown_clients(available ++ busy)
-        Mix.raise("mix deps.partition #{inspect(socket)} closed unexpectedly")
+        tcp_failed!("closed unexpectedly", socket, available, busy)
 
       {:tcp_error, socket, error} ->
-        shutdown_clients(available ++ busy)
-        Mix.raise("mix deps.partition #{inspect(socket)} errored: #{inspect(error)}")
+        tcp_failed!("errored: #{inspect(error)}", socket, available, busy)
 
       {port, {:data, {eol, data}}} ->
         with %{index: index} <-
@@ -189,6 +188,26 @@ defmodule Mix.Tasks.Deps.Partition do
     end
   end
 
+  defp tcp_failed!(message, socket, available, busy) do
+    {%{port: port} = client, busy} = pop_with(busy, &(&1.socket == socket))
+
+    # Let's make sure it has all been written out
+    # but don't wait for more than 5 seconds if it
+    # gets stuck for some unknown reason
+    receive do
+      {^port, {:exit_status, _}} -> :ok
+    after
+      5_000 -> Mix.shell().error("Timed out waiting for port exit #{inspect(port)}")
+    end
+
+    shutdown_clients(available ++ busy ++ [client])
+
+    Mix.raise(
+      "mix deps.partition #{inspect(socket)} #{message} " <>
+        "(set MIX_OS_DEPS_COMPILE_PARTITION_COUNT=1 to run in serial)"
+    )
+  end
+
   defp shutdown_clients(clients) do
     Enum.each(clients, fn %{socket: socket, port: port, index: index} ->
       if Mix.debug?() do
@@ -201,18 +220,22 @@ defmodule Mix.Tasks.Deps.Partition do
   end
 
   defp close_port(port, prefix) do
+    try do
+      Port.close(port)
+    catch
+      _, _ -> :ok
+    end
+
+    loop_close_port(port, prefix)
+  end
+
+  defp loop_close_port(port, prefix) do
     receive do
-      {^port, {:data, {:eol, data}}} -> [prefix, data, ?\n | close_port(port, prefix)]
-      {^port, {:data, {:noeol, data}}} -> [data | close_port(port, prefix)]
+      {^port, {:data, {:eol, data}}} -> [prefix, data, ?\n | loop_close_port(port, prefix)]
+      {^port, {:data, {:noeol, data}}} -> [data | loop_close_port(port, prefix)]
+      {^port, {:exit_status, _}} -> loop_close_port(port, prefix)
     after
-      0 ->
-        try do
-          Port.close(port)
-        catch
-          _, _ -> :ok
-        end
-
-        []
+      0 -> []
     end
   end
 
diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex
index db05232bcb3..cbcddaea8f8 100644
--- a/lib/mix/lib/mix/tasks/format.ex
+++ b/lib/mix/lib/mix/tasks/format.ex
@@ -80,7 +80,7 @@ defmodule Mix.Tasks.Format do
     * `--stdin-filename` - path to the file being formatted on stdin.
       This is useful if you are using plugins to support custom filetypes such
       as `.heex`. Without passing this flag, it is assumed that the code being
-      passed via stdin is valid Elixir code. Defaults to "stdin.exs".
+      passed via stdin is valid Elixir code. Defaults to `stdin.exs`.
 
     * `--migrate` - enables the `:migrate` option, which should be able to
       automatically fix some deprecation warnings but changes the AST.
@@ -225,13 +225,17 @@ defmodule Mix.Tasks.Format do
 
   @doc """
   Returns which features this plugin should plug into.
+
+  It receives all options specified in `.formatter.exs`.
   """
-  @callback features(Keyword.t()) :: [sigils: [atom()], extensions: [binary()]]
+  @callback features(keyword) :: [sigils: [atom()], extensions: [binary()]]
 
   @doc """
   Receives a string to be formatted with options and returns said string.
+
+  It receives all options specified in `.formatter.exs`.
   """
-  @callback format(String.t(), Keyword.t()) :: String.t()
+  @callback format(String.t(), keyword) :: String.t()
 
   @impl true
   def run(all_args) do
@@ -356,6 +360,16 @@ defmodule Mix.Tasks.Format do
     plugins
   end
 
+  @typedoc """
+  Options for `formatter_for_file/2`.
+  """
+  @type formatter_for_file_opts :: [
+          deps_paths: %{atom() => String.t()},
+          dot_formatter: String.t(),
+          plugin_loader: ([module()] -> [module()]),
+          root: String.t()
+        ]
+
   @doc """
   Returns a formatter function and the formatter options to
   be used for the given file.
@@ -389,6 +403,8 @@ defmodule Mix.Tasks.Format do
     * `:root` - use the given root as the current working directory.
   """
   @doc since: "1.13.0"
+  @spec formatter_for_file(String.t(), formatter_for_file_opts) ::
+          {(String.t() -> String.t()), keyword()}
   def formatter_for_file(file, opts \\ []) do
     cwd = Keyword.get_lazy(opts, :root, &File.cwd!/0)
     {dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts)
@@ -502,6 +518,12 @@ defmodule Mix.Tasks.Format do
   defp eval_deps_opts(deps, opts) do
     deps_paths = opts[:deps_paths] || Mix.Project.deps_paths()
 
+    if not is_map(deps_paths) do
+      Mix.raise(
+        "Expected :deps_paths to return a map of dependency paths, got: #{inspect(deps_paths)}"
+      )
+    end
+
     for dep <- deps,
         dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths),
         dep_dot_formatter = Path.join(dep_path, ".formatter.exs"),
@@ -810,7 +832,6 @@ defmodule Mix.Tasks.Format do
   end
 
   @doc false
-  @spec text_diff_format(String.t(), String.t()) :: iolist()
   def text_diff_format(old, new, opts \\ [])
 
   def text_diff_format(code, code, _opts), do: []
diff --git a/lib/mix/lib/mix/tasks/loadpaths.ex b/lib/mix/lib/mix/tasks/loadpaths.ex
index 2e67bd2e5f5..a44eac90930 100644
--- a/lib/mix/lib/mix/tasks/loadpaths.ex
+++ b/lib/mix/lib/mix/tasks/loadpaths.ex
@@ -21,6 +21,8 @@ defmodule Mix.Tasks.Loadpaths do
 
   ## Command line options
 
+    * `--listeners` - starts Mix listeners (they are started by default,
+      unless `--no-listeners` or `--no-deps-check` are given)
     * `--no-archives-check` - does not check archives
     * `--no-compile` - does not compile dependencies, only check and load them
     * `--no-deps-check` - does not check dependencies, only load available ones
diff --git a/lib/mix/lib/mix/tasks/profile.cprof.ex b/lib/mix/lib/mix/tasks/profile.cprof.ex
index 62abd3489a1..5eaf752d1c9 100644
--- a/lib/mix/lib/mix/tasks/profile.cprof.ex
+++ b/lib/mix/lib/mix/tasks/profile.cprof.ex
@@ -95,6 +95,15 @@ defmodule Mix.Tasks.Profile.Cprof do
   (for example, 2147483647 in a 32-bit host).
   """
 
+  @typedoc """
+  Options for the cprof profiler.
+  """
+  @type profile_opts :: [
+          matching: {module() | :_, atom() | :_, arity() | :_},
+          limit: non_neg_integer(),
+          module: module()
+        ]
+
   @switches [
     parallel: :boolean,
     require: :keep,
@@ -172,7 +181,7 @@ defmodule Mix.Tasks.Profile.Cprof do
     * `:module` - filters out any results not pertaining to the given module
 
   """
-  @spec profile((-> result), keyword()) :: result when result: any()
+  @spec profile((-> result), profile_opts()) :: result when result: any()
   def profile(fun, opts \\ []) when is_function(fun, 0) do
     Mix.ensure_application!(:tools)
     {return_value, num_matched_functions, analysis_result} = profile_and_analyse(fun, opts)
diff --git a/lib/mix/lib/mix/tasks/profile.eprof.ex b/lib/mix/lib/mix/tasks/profile.eprof.ex
index 52099b7247a..e4c134a01de 100644
--- a/lib/mix/lib/mix/tasks/profile.eprof.ex
+++ b/lib/mix/lib/mix/tasks/profile.eprof.ex
@@ -101,6 +101,18 @@ defmodule Mix.Tasks.Profile.Eprof do
   `Mix.Tasks.Profile.Cprof` that uses [`:cprof`](`:cprof`) and has a low performance degradation effect.
   """
 
+  @typedoc """
+  Options for the eprof profiler.
+  """
+  @type profile_opts :: [
+          matching: {module() | :_, atom() | :_, arity() | :_},
+          calls: non_neg_integer(),
+          time: non_neg_integer(),
+          sort: :time | :calls,
+          warmup: boolean(),
+          set_on_spawn: boolean()
+        ]
+
   @switches [
     parallel: :boolean,
     require: :keep,
@@ -189,7 +201,7 @@ defmodule Mix.Tasks.Profile.Eprof do
     * `:set_on_spawn` - if newly spawned processes should be measured (default: `true`)
 
   """
-  @spec profile((-> result), keyword()) :: result when result: any()
+  @spec profile((-> result), profile_opts()) :: result when result: any()
   def profile(fun, opts \\ []) when is_function(fun, 0) do
     Mix.ensure_application!(:tools)
     {return_value, results} = profile_and_analyse(fun, opts)
diff --git a/lib/mix/lib/mix/tasks/profile.fprof.ex b/lib/mix/lib/mix/tasks/profile.fprof.ex
index 8a7f4cd4cbc..5fdf1840ac1 100644
--- a/lib/mix/lib/mix/tasks/profile.fprof.ex
+++ b/lib/mix/lib/mix/tasks/profile.fprof.ex
@@ -114,6 +114,16 @@ defmodule Mix.Tasks.Profile.Fprof do
   this should provide more realistic insights into bottlenecks.
   """
 
+  @typedoc """
+  Options for the fprof profiler.
+  """
+  @type profile_opts :: [
+          callers: boolean(),
+          details: boolean(),
+          sort: :acc | :own,
+          trace_to_file: boolean()
+        ]
+
   @switches [
     parallel: :boolean,
     require: :keep,
@@ -184,7 +194,7 @@ defmodule Mix.Tasks.Profile.Fprof do
       usage for larger workloads.
 
   """
-  @spec profile((-> result), keyword()) :: result when result: any()
+  @spec profile((-> result), profile_opts()) :: result when result: any()
   def profile(fun, opts \\ []) when is_function(fun, 0) do
     Mix.ensure_application!(:runtime_tools)
     Mix.ensure_application!(:tools)
diff --git a/lib/mix/lib/mix/tasks/profile.tprof.ex b/lib/mix/lib/mix/tasks/profile.tprof.ex
index 163c9448cf1..1a266621589 100644
--- a/lib/mix/lib/mix/tasks/profile.tprof.ex
+++ b/lib/mix/lib/mix/tasks/profile.tprof.ex
@@ -144,6 +144,21 @@ defmodule Mix.Tasks.Profile.Tprof do
   which is more limited but has a lower footprint.
   """
 
+  @typedoc """
+  Options for the tprof profiler.
+  """
+  @type profile_opts :: [
+          matching: {module() | :_, atom() | :_, arity() | :_},
+          type: :time | :memory | :calls,
+          calls: non_neg_integer(),
+          time: non_neg_integer(),
+          memory: non_neg_integer(),
+          sort: :calls | :per_call | :time | :memory,
+          report: :process | :total,
+          warmup: boolean(),
+          set_on_spawn: boolean()
+        ]
+
   @switches [
     parallel: :boolean,
     require: :keep,
@@ -260,7 +275,7 @@ defmodule Mix.Tasks.Profile.Tprof do
     * `:set_on_spawn` - if newly spawned processes should be measured (default: `true`)
 
   """
-  @spec profile((-> result), keyword()) :: result when result: any()
+  @spec profile((-> result), profile_opts()) :: result when result: any()
   def profile(fun, opts \\ []) when is_function(fun, 0) do
     Mix.ensure_application!(:tools)
     {type, return_value, results} = profile_and_analyse(fun, opts)
diff --git a/lib/mix/lib/mix/tasks/release.ex b/lib/mix/lib/mix/tasks/release.ex
index 57fed44b5f8..b9cc0caa4bf 100644
--- a/lib/mix/lib/mix/tasks/release.ex
+++ b/lib/mix/lib/mix/tasks/release.ex
@@ -176,11 +176,11 @@ defmodule Mix.Tasks.Release do
   in daemon mode so it automatically restarts the system in case
   of crashes. See the generated `releases/RELEASE_VSN/env.sh` file.
 
-  The daemon will write all of its standard output to the "tmp/log/"
+  The daemon will write all of its standard output to the `tmp/log/`
   directory in the release root. You can watch the log file by doing
   `tail -f tmp/log/erlang.log.1` or similar. Once files get too large,
   the index suffix will be incremented. A developer can also attach
-  to the standard input of the daemon by invoking "to_erl tmp/pipe/"
+  to the standard input of the daemon by invoking `to_erl tmp/pipe/`
   from the release root. However, note that attaching to the system
   should be done with extreme care, since the usual commands for
   exiting an Elixir system, such as hitting Ctrl+C twice or Ctrl+\\,
@@ -441,12 +441,12 @@ defmodule Mix.Tasks.Release do
           ]
 
     * `:rel_templates_path` - the path to find template files that are copied to
-      the release, such as "vm.args.eex", "remote.vm.args.eex", "env.sh.eex"
-      (or "env.bat.eex"), and "overlays". Defaults to "rel" in the project root.
+      the release, such as `vm.args.eex`, `remote.vm.args.eex`, `env.sh.eex`
+      (or `env.bat.eex`), and `overlays`. Defaults to `"rel"` in the project root.
 
     * `:overlays` - a list of directories with extra files to be copied
       as is to the release. The "overlays" directory at `:rel_templates_path`
-      is always included in this list by default (typically at "rel/overlays").
+      is always included in this list by default (typically at `"rel/overlays"`).
       See the "Overlays" section for more information.
 
     * `:steps` - a list of steps to execute when assembling the release. See
@@ -479,7 +479,7 @@ defmodule Mix.Tasks.Release do
   the release is assembled. This can be easily done by placing such
   files in the `rel/overlays` directory. Any file in there is copied
   as is to the release root. For example, if you have placed a
-  "rel/overlays/Dockerfile" file, the "Dockerfile" will be copied as
+  `rel/overlays/Dockerfile` file, the "Dockerfile" will be copied as
   is to the release root.
 
   If you want to specify extra overlay directories, you can do so
diff --git a/lib/mix/lib/mix/tasks/run.ex b/lib/mix/lib/mix/tasks/run.ex
index 2553c0cefd0..043b18a74f9 100644
--- a/lib/mix/lib/mix/tasks/run.ex
+++ b/lib/mix/lib/mix/tasks/run.ex
@@ -49,6 +49,8 @@ defmodule Mix.Tasks.Run do
   ## Command-line options
 
     * `--eval`, `-e` - evaluates the given code
+    * `--listeners` - starts Mix listeners (they are started by default,
+      unless `--no-listeners` or `--no-deps-check` are given)
     * `--require`, `-r` - executes the given pattern/file
     * `--parallel`, `-p` - makes all requires parallel
     * `--preload-modules` - preloads all modules defined in applications
@@ -63,6 +65,19 @@ defmodule Mix.Tasks.Run do
 
   """
 
+  @typedoc """
+  Options for the internal `run/5` function.
+
+  These options are typically parsed from command-line arguments.
+  """
+  @type run_opts :: [
+          {:parallel_require, String.t()}
+          | {:eval, String.t()}
+          | {:require, String.t()}
+          | {:parallel, boolean()}
+          | {:config, String.t()}
+        ]
+
   @impl true
   def run(args) do
     {opts, head} =
@@ -97,7 +112,7 @@ defmodule Mix.Tasks.Run do
   @doc false
   @spec run(
           OptionParser.argv(),
-          keyword,
+          run_opts,
           OptionParser.argv(),
           (String.t() -> term()),
           (String.t() -> term())
diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex
index ad32028f915..0b1fa23e06d 100644
--- a/lib/mix/lib/mix/tasks/test.ex
+++ b/lib/mix/lib/mix/tasks/test.ex
@@ -153,6 +153,9 @@ defmodule Mix.Tasks.Test do
     * `--max-requires` - sets the maximum number of test files to compile in parallel.
       Setting this to 1 will compile test files sequentially.
 
+    * `-n`, `--name-pattern` *(since v1.19.0)* - only run tests with names that match
+      the given regular expression
+
     * `--no-archives-check` - does not check archives
 
     * `--no-color` - disables color in the output
@@ -212,7 +215,7 @@ defmodule Mix.Tasks.Test do
       test suite) as errors and returns an exit status of 1 if the test suite would otherwise
       pass. If the test suite fails and also include warnings as errors, the exit
       status returned will be the value of the `--exit-status` option, which
-      defaults to 2, plus one. Therefore in the default case, this will be exit status 3.
+      defaults to `2`, plus one. Therefore in the default case, this will be exit status `3`.
 
       Note that failures reported by `--warnings-as-errors` cannot be retried with the
       `--failed` flag.
@@ -237,7 +240,7 @@ defmodule Mix.Tasks.Test do
       It is expected that all test paths contain a `test_helper.exs` file
 
     * `:test_pattern` - a pattern to find potential test files.
-      Defaults to `*.{ex,exs}`.
+      Defaults to `"*.{ex,exs}"`.
 
       In Elixir versions earlier than 1.19.0, this option defaulted to `*_test.exs`,
       but to allow better warnings for misnamed test files, it since matches any
@@ -475,6 +478,7 @@ defmodule Mix.Tasks.Test do
     include: :keep,
     exclude: :keep,
     seed: :integer,
+    name_pattern: :regex,
     only: :keep,
     compile: :boolean,
     start: :boolean,
@@ -497,12 +501,13 @@ defmodule Mix.Tasks.Test do
     repeat_until_failure: :integer
   ]
 
+  @aliases [b: :breakpoints, n: :name_pattern]
+
   @cover [output: "cover", tool: Mix.Tasks.Test.Coverage]
 
   @impl true
   def run(args) do
-    {opts, files} = OptionParser.parse!(args, strict: @switches, aliases: [b: :breakpoints])
-    opts = put_manifest_file(opts)
+    {opts, files} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
 
     if not Mix.Task.recursing?() do
       do_run(opts, args, files)
@@ -576,6 +581,7 @@ defmodule Mix.Tasks.Test do
     Mix.Task.run("compile", args -- ["--warnings-as-errors"])
 
     project = Mix.Project.config()
+
     {partitions, opts} = Keyword.pop(opts, :partitions)
     partitioned? = is_integer(partitions) and partitions > 1
 
@@ -667,7 +673,7 @@ defmodule Mix.Tasks.Test do
     catch
       kind, reason ->
         # Also mark the whole suite as failed
-        file = Keyword.fetch!(opts, :failures_manifest_path)
+        file = get_manifest_path(opts)
         ExUnit.Filters.fail_all!(file)
         :erlang.raise(kind, reason, __STACKTRACE__)
     else
@@ -700,8 +706,10 @@ defmodule Mix.Tasks.Test do
             end)
 
           excluded == total and Keyword.has_key?(opts, :only) ->
-            message = "The --only option was given to \"mix test\" but no test was executed"
-            raise_or_error_at_exit(shell, message, opts)
+            nothing_executed(shell, "--only", opts)
+
+          excluded == total and Keyword.has_key?(opts, :name_pattern) ->
+            nothing_executed(shell, "--name-pattern", opts)
 
           true ->
             :ok
@@ -756,6 +764,11 @@ defmodule Mix.Tasks.Test do
     Mix.raise(message)
   end
 
+  defp nothing_executed(shell, option, opts) do
+    message = "The #{option} option was given to \"mix test\" but no test was executed"
+    raise_or_error_at_exit(shell, message, opts)
+  end
+
   defp raise_or_error_at_exit(shell, message, opts) do
     cond do
       opts[:raise] ->
@@ -858,7 +871,7 @@ defmodule Mix.Tasks.Test do
       opts
       |> filter_opts(:include)
       |> filter_opts(:exclude)
-      |> filter_opts(:only)
+      |> filter_only_and_name_pattern()
       |> formatter_opts()
       |> color_opts()
       |> exit_status_opts()
@@ -871,7 +884,10 @@ defmodule Mix.Tasks.Test do
   defp merge_helper_opts(opts) do
     # The only options that are additive from app env are the excludes
     value = List.wrap(Application.get_env(:ex_unit, :exclude, []))
-    Keyword.update(opts, :exclude, value, &Enum.uniq(&1 ++ value))
+
+    opts
+    |> Keyword.update(:exclude, value, &Enum.uniq(&1 ++ value))
+    |> Keyword.put_new_lazy(:failures_manifest_path, fn -> get_manifest_path([]) end)
   end
 
   defp default_opts(opts) do
@@ -882,24 +898,30 @@ defmodule Mix.Tasks.Test do
   defp parse_filters(opts, key) do
     if Keyword.has_key?(opts, key) do
       ExUnit.Filters.parse(Keyword.get_values(opts, key))
+    else
+      []
     end
   end
 
-  defp filter_opts(opts, :only) do
-    if filters = parse_filters(opts, :only) do
-      opts
-      |> Keyword.update(:include, filters, &(filters ++ &1))
-      |> Keyword.update(:exclude, [:test], &[:test | &1])
-    else
-      opts
+  defp filter_only_and_name_pattern(opts) do
+    only = parse_filters(opts, :only)
+    name_patterns = opts |> Keyword.get_values(:name_pattern) |> Enum.map(&{:test, &1})
+
+    case only ++ name_patterns do
+      [] ->
+        opts
+
+      filters ->
+        opts
+        |> Keyword.update(:include, filters, &(filters ++ &1))
+        |> Keyword.update(:exclude, [:test], &[:test | &1])
     end
   end
 
   defp filter_opts(opts, key) do
-    if filters = parse_filters(opts, key) do
-      Keyword.put(opts, key, filters)
-    else
-      opts
+    case parse_filters(opts, key) do
+      [] -> opts
+      filters -> Keyword.put(opts, key, filters)
     end
   end
 
@@ -918,16 +940,14 @@ defmodule Mix.Tasks.Test do
 
   @manifest_file_name ".mix_test_failures"
 
-  defp put_manifest_file(opts) do
-    Keyword.put_new_lazy(
-      opts,
-      :failures_manifest_path,
-      fn -> Path.join(Mix.Project.manifest_path(), @manifest_file_name) end
-    )
+  defp get_manifest_path(opts) do
+    opts[:failures_manifest_path] ||
+      Application.get_env(:ex_unit, :failures_manifest_path) ||
+      Path.join(Mix.Project.manifest_path(), @manifest_file_name)
   end
 
   defp manifest_opts(opts) do
-    manifest_file = Keyword.fetch!(opts, :failures_manifest_path)
+    manifest_file = get_manifest_path(opts)
 
     if opts[:failed] do
       if opts[:stale] do
diff --git a/lib/mix/lib/mix/utils.ex b/lib/mix/lib/mix/utils.ex
index edf04d21221..3e95e7207c0 100644
--- a/lib/mix/lib/mix/utils.ex
+++ b/lib/mix/lib/mix/utils.ex
@@ -276,7 +276,7 @@ defmodule Mix.Utils do
   Returns the name of the file written to, or "-" if the output was to STDOUT.
   This function is made public mostly for testing.
   """
-  @spec write_according_to_opts!(Path.t(), iodata(), keyword) :: Path.t()
+  @spec write_according_to_opts!(Path.t(), iodata(), write_opts) :: Path.t()
   def write_according_to_opts!(default_file_spec, contents, opts) do
     file_spec = Keyword.get(opts, :output, default_file_spec)
 
@@ -305,7 +305,7 @@ defmodule Mix.Utils do
   If the `:output` option is `-` then prints to standard output,
   see write_according_to_opts!/3 for details.
   """
-  @spec write_json_tree!(Path.t(), [node], (node -> {formatted_node, [node]}), keyword) ::
+  @spec write_json_tree!(Path.t(), [node], (node -> {formatted_node, [node]}), write_opts) ::
           Path.t()
         when node: term()
   def write_json_tree!(default_file_spec, nodes, callback, opts \\ []) do
@@ -336,13 +336,36 @@ defmodule Mix.Utils do
 
   @type formatted_node :: {name :: String.Chars.t(), edge_info :: String.Chars.t()}
 
+  @typedoc """
+  Options for `write_according_to_opts!/3`, `write_json_tree!/4`, and `write_dot_graph!/5`.
+  """
+  @type write_opts :: [
+          output: String.t()
+        ]
+
+  @typedoc """
+  Options for `print_tree/3`.
+  """
+  @type print_tree_opts :: [
+          format: String.t()
+        ]
+
+  @typedoc """
+  Options for `read_path/2`.
+  """
+  @type read_path_opts :: [
+          timeout: pos_integer(),
+          sha512: String.t()
+        ]
+
   @doc """
   Prints the given tree according to the callback.
 
   The callback will be invoked for each node and it
   must return a `{printed, children}` tuple.
   """
-  @spec print_tree([node], (node -> {formatted_node, [node]}), keyword) :: :ok when node: term()
+  @spec print_tree([node], (node -> {formatted_node, [node]}), print_tree_opts) :: :ok
+        when node: term()
   def print_tree(nodes, callback, opts \\ []) do
     pretty? =
       case Keyword.get(opts, :format) do
@@ -414,7 +437,7 @@ defmodule Mix.Utils do
           String.t(),
           [node],
           (node -> {formatted_node, [node]}),
-          keyword
+          write_opts
         ) :: Path.t()
         when node: term()
   def write_dot_graph!(default_file_spec, title, nodes, callback, opts \\ []) do
@@ -632,7 +655,7 @@ defmodule Mix.Utils do
       seconds
 
   """
-  @spec read_path(String.t(), keyword) ::
+  @spec read_path(String.t(), read_path_opts) ::
           {:ok, binary}
           | :badpath
           | {:remote, String.t()}
diff --git a/lib/mix/test/mix/task_test.exs b/lib/mix/test/mix/task_test.exs
index d0e7315aa54..3cdb9a1e299 100644
--- a/lib/mix/test/mix/task_test.exs
+++ b/lib/mix/test/mix/task_test.exs
@@ -43,7 +43,7 @@ defmodule Mix.TaskTest do
   end
 
   test "run/2 converts OptionParser.ParseError into Mix errors" do
-    message = "Could not invoke task \"hello\": 1 error found!\n--unknown : Unknown option"
+    message = ~r"Could not invoke task \"hello\": 1 error found!\n--unknown : Unknown option"
 
     assert_raise Mix.Error, message, fn ->
       Mix.Task.run("hello", ["--parser", "--unknown"])
@@ -52,7 +52,7 @@ defmodule Mix.TaskTest do
     Mix.Task.clear()
 
     message =
-      "Could not invoke task \"hello\": 1 error found!\n--int : Expected type integer, got \"foo\""
+      ~r"Could not invoke task \"hello\": 1 error found!\n--int : Expected type integer, got \"foo\""
 
     assert_raise Mix.Error, message, fn ->
       Mix.Task.run("hello", ["--parser", "--int", "foo"])
diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs
index cc44e77d2f4..d202102790e 100644
--- a/lib/mix/test/mix/tasks/compile_test.exs
+++ b/lib/mix/test/mix/tasks/compile_test.exs
@@ -425,9 +425,12 @@ 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
+      # Ensure we can boot without compilation and listeners if desired
       assert mix(["loadpaths", "--no-compile", "--no-listeners"]) == ""
 
+      # Ensure we can boot only with --no-deps-check if desired
+      assert mix(["loadpaths", "--no-deps-check"]) == ""
+
       # Now setup dependencies
       mix(["deps.compile"])
 
diff --git a/lib/mix/test/mix/tasks/profile.tprof_test.exs b/lib/mix/test/mix/tasks/profile.tprof_test.exs
index ddc5190b4b7..41973151083 100644
--- a/lib/mix/test/mix/tasks/profile.tprof_test.exs
+++ b/lib/mix/test/mix/tasks/profile.tprof_test.exs
@@ -84,8 +84,8 @@ defmodule Mix.Tasks.Profile.TprofTest do
   test "sorts based on memory per call", context do
     in_tmp(context.test, fn ->
       assert capture_io(fn ->
-               Tprof.run(["--type", "memory", "--sort", "per_call", "-e", @expr])
-             end) =~ ~r/\n:erlang\.integer_to_binary\/1.*\nEnum\.each\/2/s
+               Tprof.run(["--type", "memory", "--sort", "calls", "-e", @expr])
+             end) =~ ~r/Enum\.each\/2\s+1\s+.*\n:erlang\.integer_to_binary\/1\s+5\s+/s
     end)
   end
 
diff --git a/lib/mix/test/mix/tasks/test_test.exs b/lib/mix/test/mix/tasks/test_test.exs
index f5d792ce473..e1ca04b14f5 100644
--- a/lib/mix/test/mix/tasks/test_test.exs
+++ b/lib/mix/test/mix/tasks/test_test.exs
@@ -9,31 +9,41 @@ defmodule Mix.Tasks.TestTest do
 
   describe "ex_unit_opts/1" do
     test "returns ex unit options" do
-      assert ex_unit_opts_from_given(unknown: "ok", seed: 13) == [seed: 13]
+      assert filtered_ex_unit_opts(unknown: "ok", seed: 13) == [seed: 13]
     end
 
     test "returns includes and excludes" do
       included = [include: [:focus, key: "val"]]
-      assert ex_unit_opts_from_given(include: "focus", include: "key:val") == included
+      assert filtered_ex_unit_opts(include: "focus", include: "key:val") == included
 
       excluded = [exclude: [:focus, key: "val"]]
-      assert ex_unit_opts_from_given(exclude: "focus", exclude: "key:val") == excluded
+      assert filtered_ex_unit_opts(exclude: "focus", exclude: "key:val") == excluded
     end
 
     test "translates :only into includes and excludes" do
-      assert ex_unit_opts_from_given(only: "focus") == [include: [:focus], exclude: [:test]]
+      assert filtered_ex_unit_opts(only: "focus") == [include: [:focus], exclude: [:test]]
 
       only = [include: [:focus, :special], exclude: [:test]]
-      assert ex_unit_opts_from_given(only: "focus", include: "special") == only
+      assert filtered_ex_unit_opts(only: "focus", include: "special") == only
+    end
+
+    test "translates :name_pattern into includes and excludes" do
+      assert [include: [test: hello_regex, test: world_regex], exclude: [:test]] =
+               filtered_ex_unit_opts(name_pattern: ~r/hello/, name_pattern: ~r/world/)
+
+      assert Regex.match?(hello_regex, "hello")
+      refute Regex.match?(hello_regex, "world")
+      refute Regex.match?(world_regex, "hello")
+      assert Regex.match?(world_regex, "world")
     end
 
     test "translates :color into list containing an enabled key-value pair" do
-      assert ex_unit_opts_from_given(color: false) == [colors: [enabled: false]]
-      assert ex_unit_opts_from_given(color: true) == [colors: [enabled: true]]
+      assert filtered_ex_unit_opts(color: false) == [colors: [enabled: false]]
+      assert filtered_ex_unit_opts(color: true) == [colors: [enabled: true]]
     end
 
     test "translates :formatter into list of modules" do
-      assert ex_unit_opts_from_given(formatter: "A.B") == [formatters: [A.B]]
+      assert filtered_ex_unit_opts(formatter: "A.B") == [formatters: [A.B]]
     end
 
     test "accepts custom :exit_status" do
@@ -53,8 +63,8 @@ defmodule Mix.Tasks.TestTest do
       ex_unit_opts
     end
 
-    defp ex_unit_opts_from_given(passed) do
-      passed
+    defp filtered_ex_unit_opts(opts) do
+      opts
       |> Keyword.put(:failures_manifest_path, "foo.bar")
       |> ex_unit_opts()
       |> Keyword.drop([:failures_manifest_path, :autorun, :exit_status])
diff --git a/lib/mix/test/mix_test.exs b/lib/mix/test/mix_test.exs
index 15471b5928d..0cd19896b03 100644
--- a/lib/mix/test/mix_test.exs
+++ b/lib/mix/test/mix_test.exs
@@ -430,6 +430,43 @@ defmodule MixTest do
       System.delete_env("MIX_INSTALL_RESTORE_PROJECT_DIR")
     end
 
+    test "custom compilers", %{tmp_dir: tmp_dir} do
+      File.mkdir_p!("#{tmp_dir}/install_test/lib/mix/tasks/compile/")
+
+      File.write!("#{tmp_dir}/install_test/lib/mix/tasks/compile/install_test.ex", """
+      defmodule Mix.Tasks.Compile.InstallTest do
+        use Mix.Task.Compiler
+
+        def run(_args) do
+          Mix.shell().info("Hello from custom compiler!")
+
+          :noop
+        end
+      end
+      """)
+
+      Mix.install(
+        [
+          {:install_test, path: Path.join(tmp_dir, "install_test")}
+        ],
+        compilers: [:elixir, :install_test]
+      )
+
+      assert File.dir?(Path.join(tmp_dir, "installs"))
+
+      assert Protocol.consolidated?(InstallTest.Protocol)
+
+      assert_received {:mix_shell, :info, ["==> install_test"]}
+      assert_received {:mix_shell, :info, ["Compiling 3 files (.ex)"]}
+      assert_received {:mix_shell, :info, ["Generated install_test app"]}
+      assert_received {:mix_shell, :info, ["==> mix_install"]}
+      assert_received {:mix_shell, :info, ["Hello from custom compiler!"]}
+      refute_received _
+
+      assert List.keyfind(Application.started_applications(), :install_test, 0)
+      assert apply(InstallTest, :hello, []) == :world
+    end
+
     test "installed?", %{tmp_dir: tmp_dir} do
       refute Mix.installed?()
 
