Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package elixir for openSUSE:Factory checked 
in at 2026-08-05 17:49:15
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/elixir (Old)
 and      /work/SRC/openSUSE:Factory/.elixir.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "elixir"

Wed Aug  5 17:49:15 2026 rev:50 rq:1369576 version:1.20.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/elixir/elixir.changes    2026-06-25 
10:55:55.602726376 +0200
+++ /work/SRC/openSUSE:Factory/.elixir.new.16738/elixir.changes 2026-08-05 
17:50:12.621659869 +0200
@@ -1,0 +2,6 @@
+Tue Aug  4 17:24:18 UTC 2026 - Alessio Biancalana <[email protected]>
+
+- Upgrade to Elixir 1.20.3:
+  * Changelog available at https://hexdocs.pm/elixir/1.20.3/changelog.html
+
+-------------------------------------------------------------------

Old:
----
  elixir-1.20.2-doc.zip
  elixir-1.20.2.tar.gz

New:
----
  elixir-1.20.3-doc.zip
  elixir-1.20.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ elixir.spec ++++++
--- /var/tmp/diff_new_pack.leQfw5/_old  2026-08-05 17:50:13.253682045 +0200
+++ /var/tmp/diff_new_pack.leQfw5/_new  2026-08-05 17:50:13.257682186 +0200
@@ -18,7 +18,7 @@
 
 %define elixirdir %{_prefix}/lib/elixir
 Name:           elixir
-Version:        1.20.2
+Version:        1.20.3
 Release:        0
 Summary:        Functional meta-programming aware language built atop Erlang
 License:        Apache-2.0

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.leQfw5/_old  2026-08-05 17:50:13.321684431 +0200
+++ /var/tmp/diff_new_pack.leQfw5/_new  2026-08-05 17:50:13.325684572 +0200
@@ -1,6 +1,6 @@
-mtime: 1782219391
-commit: a42d22d3dba9e95b74fb0c4beaa627e08b93c424a4a63efc9f730fe7c668661f
+mtime: 1785864304
+commit: c98851904b7aba54535399b4a316f798e58292e7abc79600154d6d4138afac9d
 url: https://src.opensuse.org/erlang/elixir
-revision: a42d22d3dba9e95b74fb0c4beaa627e08b93c424a4a63efc9f730fe7c668661f
+revision: c98851904b7aba54535399b4a316f798e58292e7abc79600154d6d4138afac9d
 projectscmsync: https://src.opensuse.org/erlang/_ObsPrj.git
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-08-04 19:25:04.000000000 +0200
@@ -0,0 +1 @@
+.osc

++++++ elixir-1.20.2-doc.zip -> elixir-1.20.3-doc.zip ++++++
/work/SRC/openSUSE:Factory/elixir/elixir-1.20.2-doc.zip 
/work/SRC/openSUSE:Factory/.elixir.new.16738/elixir-1.20.3-doc.zip differ: char 
11, line 1

++++++ elixir-1.20.2.tar.gz -> elixir-1.20.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/CHANGELOG.md 
new/elixir-1.20.3/CHANGELOG.md
--- old/elixir-1.20.2/CHANGELOG.md      2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/CHANGELOG.md      2026-08-04 16:31:51.000000000 +0200
@@ -192,6 +192,34 @@
 
 You can enable it by setting `elixirc_options: [module_definition: 
:interpreted]` in your `mix.exs`.
 
+## v1.20.3 (2026-08-04)
+
+### 1. Enhancements
+
+#### Elixir
+
+  * [Kernel.ParallelCompiler] Batch type checker cache operations to improve 
compilation times
+
+### 2. Bug fixes
+
+#### Elixir
+
+  * [Kernel] Improve type precision of the `++/2` operator
+  * [Kernel] Improve type precision of the `send/2` return type
+  * [Kernel] Improve type refinement for `:erlang.andalso/2` and 
`:erlang.orelse/2`
+  * [Kernel] Fix type checking of `Map.put/3` on empty map types
+  * [Kernel] Fix type checking of tuple insertion on equivalent tuple types
+  * [Kernel] Fix type checking of tuple insertion on gradual types with empty 
static parts
+  * [Kernel] Fix variable refinements leaking from invalid `receive/after` 
timeout expressions
+  * [Kernel] Fix rendering inferred types containing unions of static and 
dynamic functions
+  * [Kernel] Ensure typing errors in `:maps.values/1` reference the correct 
function
+  * [Kernel] Mark `%_{}` patterns as precise so subsequent redundant struct 
clauses are detected
+  * [elixirc] Fix parsing of the `--profile time` option
+
+#### IEx
+
+  * [IEx.Helpers] Fix `break!/1` with unknown expressions
+
 ## v1.20.2 (2026-06-23)
 
 ### 1. Enhancements
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/VERSION new/elixir-1.20.3/VERSION
--- old/elixir-1.20.2/VERSION   2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/VERSION   2026-08-04 16:31:51.000000000 +0200
@@ -1 +1 @@
-1.20.2
+1.20.3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/bin/elixir new/elixir-1.20.3/bin/elixir
--- old/elixir-1.20.2/bin/elixir        2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/bin/elixir        2026-08-04 16:31:51.000000000 +0200
@@ -6,7 +6,7 @@
 
 set -e
 
-ELIXIR_VERSION=1.20.2
+ELIXIR_VERSION=1.20.3
 
 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; 
}; }; then
   cat <<USAGE >&2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/bin/elixir.bat 
new/elixir-1.20.3/bin/elixir.bat
--- old/elixir-1.20.2/bin/elixir.bat    2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/bin/elixir.bat    2026-08-04 16:31:51.000000000 +0200
@@ -4,7 +4,7 @@
 :: SPDX-FileCopyrightText: 2021 The Elixir Team
 :: SPDX-FileCopyrightText: 2012 Plataformatec
 
-set ELIXIR_VERSION=1.20.2
+set ELIXIR_VERSION=1.20.3
 
 if    ""%1""==""""                if ""%2""=="""" goto documentation
 if /I ""%1""==""--help""          if ""%2""=="""" goto documentation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/lib/elixir/lib/kernel/cli.ex 
new/elixir-1.20.3/lib/elixir/lib/kernel/cli.ex
--- old/elixir-1.20.2/lib/elixir/lib/kernel/cli.ex      2026-06-23 
14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/lib/kernel/cli.ex      2026-08-04 
16:31:51.000000000 +0200
@@ -338,7 +338,7 @@
     parse_argv(t, %{config | verbose_compile: true})
   end
 
-  defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
+  defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do
     parse_argv(t, %{config | profile: :time})
   end
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elixir-1.20.2/lib/elixir/lib/module/parallel_checker.ex 
new/elixir-1.20.3/lib/elixir/lib/module/parallel_checker.ex
--- old/elixir-1.20.2/lib/elixir/lib/module/parallel_checker.ex 2026-06-23 
14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/lib/module/parallel_checker.ex 2026-08-04 
16:31:51.000000000 +0200
@@ -68,17 +68,37 @@
   def spawn({pid, {checker, table}}, module, module_map, signatures, 
beam_location, log?) do
     # Protocols may have been consolidated. So if we know their beam location,
     # we discard their module map on purpose and start from file.
-    info =
-      if beam_location != [] and Keyword.has_key?(module_map.attributes, 
:__protocol__) do
-        List.to_string(beam_location)
-      else
-        cache_from_module_map(table, module_map, signatures)
-      end
+    if beam_location != [] and Keyword.has_key?(module_map.attributes, 
:__protocol__) do
+      spawn_and_register_cache(pid, checker, table, module, 
List.to_string(beam_location), log?)
+    else
+      {mode, module_tuple} = cache_from_module_map(table, module_map, 
signatures)
+      ref = make_ref()
+      spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, 
log?)
+      register_cache_and_checker(checker, mode, module, spawned, ref)
+      :ok
+    end
+  end
 
-    inner_spawn(pid, checker, table, module, info, log?)
+  defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
+    spawn(fn ->
+      mon_ref = Process.monitor(pid)
+
+      receive do
+        {^ref, :check, profile} ->
+          # Set the compiler info so we can collect warnings
+          Process.link(pid)
+          :erlang.put(:elixir_compiler_info, {pid, self()})
+          {warnings, errors} = check_module(module_tuple, {checker, table}, 
log?, profile)
+          send(pid, {__MODULE__, module, warnings, errors})
+          send(checker, {__MODULE__, :checked, ref})
+
+        {:DOWN, ^mon_ref, _, _, _} ->
+          :ok
+      end
+    end)
   end
 
-  defp inner_spawn(pid, checker, table, module, info, log?) do
+  defp spawn_and_register_cache(pid, checker, table, module, info, log?) do
     ref = make_ref()
 
     spawned =
@@ -89,48 +109,28 @@
           {^ref, :cache} ->
             Process.link(pid)
 
-            {mode, module_tuple} =
-              cond do
-                is_binary(info) ->
-                  location =
-                    case :code.which(module) do
-                      [_ | _] = path -> path
-                      _ -> info
-                    end
-
-                  with {:ok, binary} <- File.read(location),
-                       {:ok,
-                        {_, [{:debug_info, {:debug_info_v1, backend, data}}, 
{~c"ExCk", checker}]}} <-
-                         :beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
-                       {:ok, module_map} <- backend.debug_info(:elixir_v1, 
module, data, []),
-                       {@elixir_checker_version, contents} <- 
:erlang.binary_to_term(checker) do
-                    {cache_chunk(table, module, contents), 
module_map_to_module_tuple(module_map)}
-                  else
-                    _ -> {:uncached, nil}
-                  end
-
-                is_tuple(info) ->
-                  info
+            location =
+              case :code.which(module) do
+                [_ | _] = path -> path
+                _ -> info
               end
 
-            # We only make the module available now, so they are not visible 
during inference
-            :ets.insert(table, {module, mode})
-            send(checker, {ref, :cached})
-
-            receive do
-              {^ref, :check, profile} ->
-                # Set the compiler info so we can collect warnings
-                :erlang.put(:elixir_compiler_info, {pid, self()})
-
-                {warnings, errors} =
-                  if module_tuple do
-                    check_module(module_tuple, {checker, table}, log?, profile)
-                  else
-                    {[], []}
-                  end
-
-                send(pid, {__MODULE__, module, warnings, errors})
-                send(checker, {__MODULE__, :done, module})
+            with {:ok, binary} <- File.read(location),
+                 {:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, 
{~c"ExCk", exck}]}} <-
+                   :beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
+                 {:ok, module_map} <- backend.debug_info(:elixir_v1, module, 
data, []),
+                 {@elixir_checker_version, contents} <- 
:erlang.binary_to_term(exck) do
+              mode = cache_chunk(table, module, contents)
+              module_tuple = module_map_to_module_tuple(module_map)
+              spawned = spawn_checker(ref, pid, checker, table, module, 
module_tuple, log?)
+              :ets.insert(table, {module, mode})
+              send(checker, {__MODULE__, :cached, module, spawned, ref})
+            else
+              _ ->
+                # Nothing to check, so we notify everyone we are done
+                :ets.insert(table, {module, :uncached})
+                send(checker, {__MODULE__, :cached, module, nil, ref})
+                send(pid, {__MODULE__, module, [], []})
             end
 
           {:DOWN, ^mon_ref, _, _, _} ->
@@ -138,7 +138,7 @@
         end
       end)
 
-    register(checker, module, spawned, ref)
+    register_cache(checker, module, spawned, ref)
     :ok
   end
 
@@ -192,7 +192,7 @@
     log? = not match?({_, false}, value)
 
     for {module, file} <- runtime_files do
-      inner_spawn(self(), checker, table, module, file, log?)
+      spawn_and_register_cache(self(), checker, table, module, file, log?)
     end
 
     count = :gen_server.call(checker, :start, :infinity)
@@ -533,8 +533,12 @@
     :gen_server.call(server, {:unlock, module, mode}, :infinity)
   end
 
-  defp register(server, module, pid, ref) do
-    :gen_server.cast(server, {:register, module, pid, ref})
+  defp register_cache_and_checker(server, mode, module, pid, ref) do
+    :gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, 
ref})
+  end
+
+  defp register_cache(server, module, pid, ref) do
+    :gen_server.cast(server, {:register_cache, module, pid, ref})
   end
 
   ## Server callbacks
@@ -577,7 +581,8 @@
 
     state = %{
       waiting: %{},
-      modules: [],
+      caches: [],
+      checkers: [],
       spawned: %{},
       schedulers: schedulers,
       threshold: threshold,
@@ -590,20 +595,9 @@
     :gen_server.enter_loop(__MODULE__, [], state)
   end
 
-  def handle_call(:start, _from, %{modules: modules, protocols: protocols, 
table: table} = state) do
+  def handle_call(:start, _from, %{caches: caches, protocols: protocols, 
table: table} = state) do
     :ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
-
-    for {_module, pid, ref} <- modules do
-      send(pid, {ref, :cache})
-    end
-
-    for {_module, _pid, ref} <- modules do
-      receive do
-        {^ref, :cached} -> :ok
-      end
-    end
-
-    {:reply, length(modules), run_checkers(%{state | protocols: []})}
+    {:reply, length(caches), run_caches(%{state | protocols: []})}
   end
 
   def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
@@ -633,10 +627,21 @@
     {:noreply, state}
   end
 
-  def handle_info({__MODULE__, :done, module}, state) do
-    # Unfortunately we cannot assume uniqueness because the same module
-    # may be defined by mistake several times
-    {timer, spawned} = Map.pop(state.spawned, module)
+  def handle_info({__MODULE__, :cached, module, pid, ref}, state) do
+    {_nil, spawned} = Map.pop(state.spawned, ref)
+
+    state =
+      if pid do
+        %{state | spawned: spawned, checkers: [{module, pid, ref} | 
state.checkers]}
+      else
+        %{state | spawned: spawned}
+      end
+
+    {:noreply, run_caches(state)}
+  end
+
+  def handle_info({__MODULE__, :checked, ref}, state) do
+    {timer, spawned} = Map.pop(state.spawned, ref)
     timer && Process.cancel_timer(timer)
     {:noreply, run_checkers(%{state | spawned: spawned})}
   end
@@ -645,11 +650,41 @@
     {:stop, :normal, state}
   end
 
-  def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) 
do
-    {:noreply, %{state | modules: [{module, pid, ref} | modules]}}
+  def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = 
state) do
+    {:noreply, %{state | caches: [{module, pid, ref} | caches]}}
+  end
+
+  def handle_cast(
+        {:register_cache_and_checker, mode, module, pid, ref},
+        %{caches: caches, checkers: checkers} = state
+      ) do
+    {:noreply,
+     %{state | caches: [{module, mode} | caches], checkers: [{module, pid, 
ref} | checkers]}}
+  end
+
+  defp run_caches(%{caches: [], spawned: spawned} = state) do
+    if spawned == %{}, do: run_checkers(state), else: state
+  end
+
+  defp run_caches(%{spawned: spawned, schedulers: schedulers} = state)
+       when map_size(spawned) >= schedulers do
+    state
+  end
+
+  defp run_caches(%{caches: [cache | caches]} = state) do
+    case cache do
+      {_module, pid, ref} ->
+        send(pid, {ref, :cache})
+        spawned = Map.put(state.spawned, ref, nil)
+        run_caches(%{state | caches: caches, spawned: spawned})
+
+      {module, mode} ->
+        :ets.insert(state.table, {module, mode})
+        run_caches(%{state | caches: caches})
+    end
   end
 
-  defp run_checkers(%{modules: []} = state) do
+  defp run_checkers(%{checkers: []} = state) do
     state
   end
 
@@ -658,11 +693,11 @@
     state
   end
 
-  defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
+  defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do
     send(pid, {ref, :check, state.profile})
     timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, 
module, pid})
-    spawned = Map.put(state.spawned, module, timer)
-    run_checkers(%{state | modules: modules, spawned: spawned})
+    spawned = Map.put(state.spawned, ref, timer)
+    run_checkers(%{state | checkers: checkers, spawned: spawned})
   end
 
   defp profile(module, :time, fun) do
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/lib/elixir/lib/module/types/apply.ex 
new/elixir-1.20.3/lib/elixir/lib/module/types/apply.ex
--- old/elixir-1.20.2/lib/elixir/lib/module/types/apply.ex      2026-06-23 
14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/lib/module/types/apply.ex      2026-08-04 
16:31:51.000000000 +0200
@@ -1087,6 +1087,13 @@
     end
   end
 
+  defp remote_apply(:erlang, :send, info, [_dest, message] = args_types, 
stack) do
+    case remote_apply(info, args_types, stack) do
+      {:ok, _type} -> {:ok, return(message, args_types, stack)}
+      other -> other
+    end
+  end
+
   defp remote_apply(:erlang, :tl, _info, [list], stack) do
     case list_tl(list) do
       {:ok, value_type} -> {:ok, return(value_type, [list], stack)}
@@ -1094,6 +1101,33 @@
     end
   end
 
+  defp remote_apply(:erlang, :++, _info, [left, right], stack) do
+    # TODO: remove once we add parametric types, this will just be:
+    # empty_list(), a -> a
+    # non_empty_list(elem), a -> non_empty_list(elem, a)
+    case list_of(left) do
+      {empty_list?, list_of} ->
+        left_result =
+          if empty_list? do
+            right
+          else
+            none()
+          end
+
+        right_result =
+          if list_of do
+            non_empty_list(list_of, right)
+          else
+            none()
+          end
+
+        {:ok, return(union(left_result, right_result), [left, right], stack)}
+
+      :badproperlist ->
+        {:error, badremote(:erlang, :++, [left, right])}
+    end
+  end
+
   @struct_key atom([:__struct__])
   @nil_atom atom([nil])
 
@@ -1409,7 +1443,7 @@
   defp remote_apply(:maps, :values, _info, [map], stack) do
     case map_to_list(map, fn _key, value -> value end) do
       {:ok, list_type} -> {:ok, return(list_type, [map], stack)}
-      :badmap -> {:error, badremote(:maps, :keys, [map])}
+      :badmap -> {:error, badremote(:maps, :values, [map])}
     end
   end
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/lib/elixir/lib/module/types/descr.ex 
new/elixir-1.20.3/lib/elixir/lib/module/types/descr.ex
--- old/elixir-1.20.2/lib/elixir/lib/module/types/descr.ex      2026-06-23 
14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/lib/module/types/descr.ex      2026-08-04 
16:31:51.000000000 +0200
@@ -409,6 +409,11 @@
   defp pop_dynamic(:term), do: {:term, :term}
   defp pop_dynamic(descr), do: Map.pop(descr, :dynamic, descr)
 
+  defp put_dynamic(:term, dynamic), do: optional_to_term(%{dynamic: dynamic})
+  defp put_dynamic(static, dynamic) when static == dynamic, do: static
+  defp put_dynamic(_static, dynamic) when dynamic == @none, do: @none
+  defp put_dynamic(static, dynamic), do: Map.put(static, :dynamic, dynamic)
+
   @compile {:inline, maybe_union: 2}
   defp maybe_union(nil, _fun), do: nil
   defp maybe_union(descr, fun), do: union(descr, fun.())
@@ -508,7 +513,8 @@
       {right_dynamic, right_static} = pop_dynamic(right)
       dynamic_part = difference_static(left_dynamic, right_static)
 
-      Map.put(difference_static(left_static, right_dynamic), :dynamic, 
dynamic_part)
+      difference_static(left_static, right_dynamic)
+      |> put_dynamic(dynamic_part)
     else
       difference_static(left, right)
     end
@@ -1407,10 +1413,10 @@
       {:ok, atom()}
 
       iex> fun_apply(fun([integer()], atom()), [float()])
-      :badarg
+      {:badarg, [integer()], false}
 
-      iex> fun_apply(fun([dynamic()], atom()), [dynamic()])
-      {:ok, atom()}
+      iex> fun_apply(dynamic(fun([integer()], atom())), [integer()])
+      {:ok, dynamic(atom())}
   """
   def fun_apply(:term, _arguments), do: :badfun
 
@@ -1626,7 +1632,7 @@
 
   defp fun_normalize(%{fun: {:union, bdds}}, arity) do
     case :maps.take(arity, bdds) do
-      {bdd, _rest} ->
+      {bdd, rest} ->
         {domain, arrows} =
           Enum.reduce(fun_bdd_to_pos_dnf(arity, bdd), {term(), []}, fn 
pos_funs,
                                                                        
{domain, arrows} ->
@@ -1634,7 +1640,13 @@
           end)
 
         if arrows == [] do
-          {:badarity, :maps.keys(bdds)}
+          # The function is empty at the requested arity. Report the *other*
+          # arities (never the called one, which would be self-contradictory),
+          # or :badfun when there are none, i.e. the function is empty.
+          case :maps.keys(rest) do
+            [] -> :badfun
+            other -> {:badarity, other}
+          end
         else
           {:ok, domain, arrows}
         end
@@ -1952,25 +1964,33 @@
   # representation. The goal here is to do the opposite of fun_descr
   # and put static and dynamic parts back together to improve
   # pretty printing.
-  defp fun_denormalize(%{fun: {:union, static_repr}}, %{fun: {:union, 
dynamic_repr}}, opts) do
-    # Denormalize each arity
-    for {arity, static_bdd} <- static_repr,
-        {^arity, dynamic_bdd} <- dynamic_repr,
-        reduce: {static_repr, dynamic_repr, []} do
-      {statics, dynamics, acc} ->
-        with {:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, 
dynamic_bdd, opts) do
-          {Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted | 
acc]}
-        else
-          _ -> {statics, dynamics, acc}
-        end
-    end
+  defp fun_denormalize(
+         %{fun: {:union, static_repr}} = static,
+         %{fun: {:union, dynamic_repr}} = dynamic,
+         opts
+       ) do
+    {static_repr, dynamic_repr, acc} =
+      Enum.reduce(static_repr, {static_repr, dynamic_repr, []}, fn
+        {arity, static_bdd}, {statics, dynamics, acc} ->
+          with %{^arity => dynamic_bdd} <- dynamics,
+               {:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, 
dynamic_bdd, opts) do
+            {Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted 
| acc]}
+          else
+            _ -> {statics, dynamics, acc}
+          end
+      end)
+
+    {fun_replace_arities(static, static_repr), fun_replace_arities(dynamic, 
dynamic_repr), acc}
   end
 
   # If not unions of functions, do not try to denormalize.
-  defp fun_denormalize(static_repr, dynamic_repr, _opts) do
-    {static_repr, dynamic_repr, []}
+  defp fun_denormalize(static, dynamic, _opts) do
+    {static, dynamic, []}
   end
 
+  defp fun_replace_arities(descr, arities) when arities == %{}, do: 
Map.delete(descr, :fun)
+  defp fun_replace_arities(descr, arities), do: %{descr | fun: {:union, 
arities}}
+
   defp fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do
     static_pos = fun_bdd_to_pos_dnf(arity, static_bdd)
     dynamic_pos = fun_bdd_to_pos_dnf(arity, dynamic_bdd)
@@ -2272,7 +2292,7 @@
   It returns a two-element tuple. The first element dictates the
   empty list type. The second element returns the value type.
 
-      {boolean(), t() or nil}
+      {true, nil} or {boolean(), t()}
 
   If the value is `nil`, it means that component is missing.
   Note `{false, nil}` is not a valid return type, instead it
@@ -2284,10 +2304,10 @@
     case :maps.take(:dynamic, descr) do
       :error ->
         with {empty_list?, value} <- list_of_static(descr) do
-          if empty?(value) and empty_list? == false do
-            :badproperlist
-          else
-            {empty_list?, value}
+          cond do
+            not empty?(value) -> {empty_list?, value}
+            empty_list? -> {true, nil}
+            true -> :badproperlist
           end
         end
 
@@ -2307,7 +2327,7 @@
 
               %{list: bdd} ->
                 Enum.reduce(list_bdd_to_pos_dnf(bdd), none(), fn {list, last, 
_negs}, acc ->
-                  if last == @empty_list or subtype?(last, @empty_list) do
+                  if empty_list_type?(last) do
                     union(acc, list)
                   else
                     acc
@@ -4175,7 +4195,7 @@
   defp map_put_static_value(descr, split_keys, type) do
     case :maps.take(:dynamic, descr) do
       :error ->
-        if descr_key?(descr, :map) and map_only?(descr) do
+        if non_empty_map_only?(descr) do
           {:ok, map_put_static(descr, split_keys, type)}
         else
           :badmap
@@ -4497,11 +4517,22 @@
   end
 
   defp map_line_meet_empty?(key, type, neg_type, t1, t2, tag, neg_tag, 
acc_meet, negs) do
-    diff = difference(type, neg_type)
     meet = intersection(type, neg_type)
 
-    (empty?(diff) or map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} 
| t1]), negs)) and
-      (empty?(meet) or map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} 
| acc_meet], negs))
+    if empty?(meet) do
+      # This negative map is disjoint from the current line at this field.
+      map_line_empty?(tag, Enum.reverse(acc_meet, [{key, type} | t1]), negs)
+    else
+      diff = difference(type, neg_type)
+
+      if empty?(diff) do
+        # The field is a subtype of the negative field, so their intersection 
is type.
+        map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, type} | acc_meet], 
negs)
+      else
+        map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} | t1]), negs) 
and
+          map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} | acc_meet], 
negs)
+      end
+    end
   end
 
   defp map_line_fields_empty?([{k1, v1} | t1], [{k2, _} | _] = l2, tag, 
neg_tag, fields, negs)
@@ -5203,13 +5234,22 @@
   defp tuple_elements_empty?(acc_meet, 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)
     meet = intersection(ty, neg_type)
 
-    # In this case, there is no intersection between the positive and this 
negative.
-    # So we should just "go next"
-    (empty?(diff) or tuple_line_empty?(tag, Enum.reverse(acc_meet, [diff | 
elements]), negs)) and
-      (empty?(meet) or tuple_elements_empty?([meet | acc_meet], tag, elements, 
neg_elements, negs))
+    if empty?(meet) do
+      # This negative tuple is disjoint from the current line at this element.
+      tuple_line_empty?(tag, Enum.reverse(acc_meet, [ty | elements]), negs)
+    else
+      diff = difference(ty, neg_type)
+
+      if empty?(diff) do
+        # The element is a subtype of the negative element, so their 
intersection is ty.
+        tuple_elements_empty?([ty | acc_meet], tag, elements, neg_elements, 
negs)
+      else
+        tuple_line_empty?(tag, Enum.reverse(acc_meet, [diff | elements]), 
negs) and
+          tuple_elements_empty?([meet | acc_meet], tag, elements, 
neg_elements, negs)
+      end
+    end
   end
 
   # Determines if the set difference is empty when:
@@ -5538,12 +5578,15 @@
       iex> tuple_fetch(tuple([integer(), atom()]), 0)
       {false, integer()}
 
-      iex> tuple_fetch(union(tuple([integer()]), tuple([integer(), atom()])), 
1)
-      {true, atom()}
+      iex> tuple_fetch(union(tuple([integer()]), tuple([atom()])), 0)
+      {false, union(integer(), atom())}
 
       iex> tuple_fetch(dynamic(), 0)
       {true, dynamic()}
 
+      iex> tuple_fetch(tuple([integer(), atom()]), 2)
+      :badindex
+
       iex> tuple_fetch(integer(), 0)
       :badtuple
 
@@ -5955,27 +5998,57 @@
     end
   end
 
-  defp tuple_insert_static(descr, _, _) when descr == @none, do: none()
-
-  defp tuple_insert_static(descr, index, type) do
-    Map.update!(descr, :tuple, fn bdd ->
-      bdd_map(bdd, fn bdd_leaf(tag, elements) ->
-        # If the tuple is open, then we want List.insert_at to put the new 
element at the correct
-        # index, which requires filling the tuple with `term()` values first.
-        # Closed tuples of an incorrect size will be ignored (they are 
cancelled by the earlier
-        # intersection with `tuple_of_size_at_least`).
-        elements =
-          if tag == :open and length(elements) < index do
-            tuple_fill(elements, index)
+  defp tuple_insert_static(%{tuple: bdd} = descr, index, type) do
+    %{
+      descr
+      | tuple:
+          if tuple_bdd_positive?(bdd) do
+            # A pure disjunction of leaves: the insert distributes over the 
union, so
+            # we rewrite each leaf in place (preserving the structure callers 
assert on).
+            bdd_map(bdd, fn bdd_leaf(tag, elements) ->
+              tuple_insert_leaf(tag, elements, index, type)
+            end)
           else
-            elements
+            # The bdd carries negations and/or implicit `:bdd_top` positive 
paths
+            # (e.g. from `tuple_difference(open_tuple([]), _) -> 
bdd_negation`).
+            # `bdd_map` rewrites only explicit leaves, so it would skip the 
implicit
+            # top (losing the insert) and wrongly transform negated leaves. 
Expand to
+            # the exact negation-free positive DNF first, then insert into 
each leaf.
+            bdd
+            |> tuple_bdd_to_dnf_no_negations()
+            |> Enum.reduce(:bdd_bot, fn {tag, elements}, acc ->
+              tuple_union(tuple_insert_leaf(tag, elements, index, type), acc)
+            end)
           end
+    }
+  end
 
-        bdd_leaf_new(tag, List.insert_at(elements, index, type))
-      end)
-    end)
+  defp tuple_insert_static(_descr, _index, _type), do: none()
+
+  # Inserts `type` at `index` into a single tuple literal. If the tuple is 
open,
+  # `List.insert_at` needs the tuple filled with `term()` up to `index` first.
+  # Closed tuples of an incorrect size are cancelled before reaching here (the
+  # input is intersected/guarded with `tuple_of_size_at_least`).
+  defp tuple_insert_leaf(tag, elements, index, type) do
+    elements =
+      if tag == :open and length(elements) < index do
+        tuple_fill(elements, index)
+      else
+        elements
+      end
+
+    bdd_leaf_new(tag, List.insert_at(elements, index, type))
   end
 
+  # `bdd_map` rewrites a tuple insert exactly only on a pure positive 
disjunction
+  # of leaves (every node keeps its literal on the constrained-top branch with 
no
+  # dual branch). Any dual branch (or implicit `:bdd_top` in negated position)
+  # would be transformed incorrectly or skipped.
+  defp tuple_bdd_positive?(:bdd_bot), do: true
+  defp tuple_bdd_positive?(bdd_leaf(_, _)), do: true
+  defp tuple_bdd_positive?({_, _lit, :bdd_top, u, :bdd_bot}), do: 
tuple_bdd_positive?(u)
+  defp tuple_bdd_positive?(_), do: false
+
   @doc """
   Replace an element in the tuple at the given (0-based) index.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/lib/elixir/lib/module/types/expr.ex 
new/elixir-1.20.3/lib/elixir/lib/module/types/expr.ex
--- old/elixir-1.20.2/lib/elixir/lib/module/types/expr.ex       2026-06-23 
14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/lib/module/types/expr.ex       2026-08-04 
16:31:51.000000000 +0200
@@ -584,7 +584,8 @@
             {union(body_type, acc), Of.reset_vars(context, original)}
           else
             error = {:badtimeout, timeout_type, timeout, context}
-            {union(body_type, acc), error(__MODULE__, error, meta, stack, 
context)}
+            context = error(__MODULE__, error, meta, stack, context)
+            {union(body_type, acc), Of.reset_vars(context, original)}
           end
       end)
       |> dynamic_unless_static(stack)
@@ -687,6 +688,38 @@
     end
   end
 
+  def of_expr(
+        {{:., _, [:erlang, fun]}, _meta, [left, right]} = call,
+        expected,
+        _expr,
+        stack,
+        context
+      )
+      when fun in [:andalso, :orelse] do
+    polarity = fun == :andalso
+    polarity_type = atom([polarity])
+
+    left_expected =
+      case booleaness(expected) do
+        {^polarity, _} -> polarity_type
+        _ -> boolean()
+      end
+
+    {left_type, context} = of_expr(left, left_expected, call, stack, context)
+    {right_type, right_context} = of_expr(right, expected, call, stack, 
context)
+
+    if subtype?(left_type, polarity_type) do
+      {right_type, right_context}
+    else
+      context =
+        right_context
+        |> Of.reset_vars(context)
+        |> reset_warnings(context)
+
+      {union(atom([not polarity]), right_type), context}
+    end
+  end
+
   def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, 
stack, context) do
     {remote_type, context} = of_expr(remote, atom(), call, stack, context)
     {mods, context} = Of.modules(remote_type, name, length(args), call, meta, 
stack, context)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/lib/elixir/lib/module/types/pattern.ex 
new/elixir-1.20.3/lib/elixir/lib/module/types/pattern.ex
--- old/elixir-1.20.2/lib/elixir/lib/module/types/pattern.ex    2026-06-23 
14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/lib/module/types/pattern.ex    2026-08-04 
16:31:51.000000000 +0200
@@ -772,7 +772,8 @@
     {refined, context} = of_match_var(var, atom(), expr, stack, context)
 
     if compatible?(refined, atom()) do
-      of_open_map(args, singleton?(refined), [__struct__: refined], [], path, 
stack, context)
+      precise? = match?({:_, _, _}, var) or singleton?(refined)
+      of_open_map(args, precise?, [__struct__: refined], [], path, stack, 
context)
     else
       error = {:badstruct, refined, expr, context}
       {error_type(), false, error(__MODULE__, error, meta, stack, context)}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elixir-1.20.2/lib/elixir/pages/getting-started/structs.md 
new/elixir-1.20.3/lib/elixir/pages/getting-started/structs.md
--- old/elixir-1.20.2/lib/elixir/pages/getting-started/structs.md       
2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/pages/getting-started/structs.md       
2026-08-04 16:31:51.000000000 +0200
@@ -109,19 +109,18 @@
 User
 ```
 
-However, structs do not inherit any of the protocols that maps do. For 
example, you can neither enumerate nor access a struct:
+However, structs do not inherit any of the built-in features that maps do. For 
example, you can neither enumerate nor access a struct:
 
 ```elixir
 iex> john = %User{}
 %User{age: 27, name: "John"}
 iex> john[:name]
 ** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not 
implement the Access behaviour)
-             User.fetch(%User{age: 27, name: "John"}, :name)
 iex> Enum.each(john, fn {field, value} -> IO.puts(value) end)
 ** (Protocol.UndefinedError) protocol Enumerable not implemented for 
%User{age: 27, name: "John"} of type User (a struct)
 ```
 
-Structs alongside protocols provide one of the most important features for 
Elixir developers: data polymorphism. That's what we will explore in the next 
chapter.
+As we will learn in future chapters, Elixir does allow developers to attach 
custom behaviours to structs based on their names, enabling custom data 
enumeration, pretty printing, and more. This dispatch mechanism is called 
[protocols](protocols.md) and enables extensible data polymorphism in Elixir.
 
 ## Default values and required keys
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elixir-1.20.2/lib/elixir/test/elixir/module/types/descr_test.exs 
new/elixir-1.20.3/lib/elixir/test/elixir/module/types/descr_test.exs
--- old/elixir-1.20.2/lib/elixir/test/elixir/module/types/descr_test.exs        
2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/test/elixir/module/types/descr_test.exs        
2026-08-04 16:31:51.000000000 +0200
@@ -15,6 +15,7 @@
   use ExUnit.Case, async: true
 
   import Module.Types.Descr
+  doctest Module.Types.Descr, import: true
   defmacro domain_key(arg) when is_atom(arg), do: [arg]
 
   defp number(), do: union(integer(), float())
@@ -590,7 +591,10 @@
       assert equal?(dynamic(), difference(term(), dynamic()))
       assert empty?(difference(dynamic(), term()))
       assert empty?(difference(none(), dynamic()))
-      assert empty?(difference(dynamic(integer()), integer()))
+      assert difference(dynamic(integer()), integer()) == none()
+
+      # Covers assembling a result with static :term and dynamic unfolded term.
+      assert difference(term(), %{dynamic: none(), optional: 1}) == term()
     end
 
     test "optional" do
@@ -1311,6 +1315,28 @@
       assert fun_apply(fun_mixed, [integer()]) == {:badarity, [1, 2]}
       assert fun_apply(fun_mixed, [integer(), atom()]) == {:badarity, [2, 1]}
 
+      # A function that is empty at the called arity is :badfun, not a
+      # self-contradictory {:badarity, [called_arity]}.
+      empty_fun =
+        difference(fun([union(integer(), float())], atom()), fun([integer()], 
atom()))
+
+      assert empty?(empty_fun)
+      assert fun_apply(empty_fun, [integer()]) == :badfun
+
+      # When the function is empty at the called arity but usable at another, 
the
+      # badarity must list only the *other* arity, never the (empty) called 
one.
+      # `empty_fun` keeps a structured empty arity-1 entry, which union 
preserves,
+      # so `usable_at_2` is equal? to a plain 2-arity function yet carries it.
+      usable_at_2 = union(empty_fun, fun([integer(), atom()], boolean()))
+
+      refute empty?(usable_at_2)
+      assert equal?(usable_at_2, fun([integer(), atom()], boolean()))
+
+      # Congruent with the normalized form, and excludes the empty arity 1.
+      assert fun_apply(usable_at_2, [integer()]) == {:badarity, [2]}
+      assert fun_apply(fun([integer(), atom()], boolean()), [integer()]) == 
{:badarity, [2]}
+      assert fun_apply(usable_at_2, [integer(), atom()]) == {:ok, boolean()}
+
       # Function intersection tests (no overlap)
       fun0 = intersection(fun([integer()], atom()), fun([float()], binary()))
       assert fun_apply(fun0, [integer()]) == {:ok, atom()}
@@ -1709,7 +1735,7 @@
     test "list_of" do
       assert list_of(term()) == :badproperlist
       assert list_of(none()) == :badproperlist
-      assert list_of(empty_list()) == {true, none()}
+      assert list_of(empty_list()) == {true, nil}
       assert list_of(union(empty_list(), integer())) == :badproperlist
       assert list_of(non_empty_list(integer())) == {false, integer()}
       assert list_of(non_empty_list(integer(), atom())) == :badproperlist
@@ -1720,7 +1746,9 @@
       assert list_of(dynamic()) == {true, dynamic()}
       assert list_of(dynamic(list(integer()))) == {true, dynamic(integer())}
       assert list_of(dynamic(list(integer(), atom()))) == {true, nil}
+      assert list_of(dynamic(list(integer(), term()))) == {true, dynamic()}
       assert list_of(dynamic(non_empty_list(integer(), atom()))) == 
:badproperlist
+      assert list_of(dynamic(non_empty_list(integer(), term()))) == {false, 
dynamic()}
       assert list_of(dynamic(union(empty_list(), integer()))) == {true, nil}
 
       # A list that the difference resolves to nothing
@@ -1955,6 +1983,18 @@
              |> tuple_insert_at(1, float())
              |> equal?(tuple([integer(), float(), atom(), boolean()]))
 
+      # Inserting must be a congruence wrt equal?, even when the positive side 
is
+      # the implicit top (a negation with :bdd_top branches) rather than an
+      # explicit open leaf. Here t2 == open_tuple([term(), term()]).
+      t2 = difference(open_tuple([]), union(tuple([]), tuple([term()])))
+      assert equal?(t2, open_tuple([term(), term()]))
+
+      assert tuple_insert_at(t2, 2, float())
+             |> equal?(tuple_insert_at(open_tuple([term(), term()]), 2, 
float()))
+
+      # The inserted index is actually constrained to float().
+      refute subtype?(tuple([integer(), integer(), atom()]), 
tuple_insert_at(t2, 2, float()))
+
       # Test inserting into a complex union involving dynamic
       assert union(tuple([integer(), atom()]), dynamic(tuple([float(), 
binary()])))
              |> tuple_insert_at(1, boolean())
@@ -1974,6 +2014,20 @@
       # Errors must propagate even when the inserted value is dynamic
       assert tuple_insert_at(integer(), 0, dynamic()) == :badtuple
       assert tuple_insert_at(tuple([atom([:ok])]), 2, dynamic()) == :badindex
+
+      # Must not crash when a gradual descr's static part is a non-normalized
+      # empty (semantically empty but syntactically present) non-tuple 
component.
+      a1 = union(dynamic(), non_empty_list(integer(), none()))
+      assert equal?(a1, dynamic())
+
+      assert tuple_insert_at(a1, 1, atom([:x]))
+             |> equal?(tuple_insert_at(dynamic(), 1, atom([:x])))
+
+      a2 = union(tuple([dynamic()]), non_empty_list(none()))
+      assert equal?(a2, tuple([dynamic()]))
+
+      assert tuple_insert_at(a2, 1, atom([:x]))
+             |> equal?(tuple_insert_at(tuple([dynamic()]), 1, atom([:x])))
     end
 
     test "tuple_replace_at" do
@@ -3162,6 +3216,16 @@
 
       assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), 
x: term())}
     end
+
+    test "is consistent across representations of an empty type" do
+      # An empty map component that survives syntactically (open_map(c: none())
+      # is a non-normalized empty, equal to none()) must report :badmap like
+      # none(), not {:ok, <inhabited>}.
+      a2 = open_map(c: none())
+      assert equal?(none(), a2)
+      assert map_put(none(), atom([:a]), integer()) == :badmap
+      assert map_put(a2, atom([:a]), integer()) == :badmap
+    end
   end
 
   describe "disjoint" do
@@ -3258,6 +3322,8 @@
 
     test "dynamic (negation)" do
       assert dynamic(negation(integer())) |> to_quoted_string() == 
"dynamic(not integer())"
+      assert negation(dynamic(none())) == term()
+
       assert negation(dynamic(integer())) |> to_quoted_string() == "dynamic() 
or not integer()"
 
       assert union(atom(), dynamic(integer())) |> negation() |> 
to_quoted_string() ==
@@ -3534,6 +3600,34 @@
                """
     end
 
+    test "fun union of static and dynamic keeps non-fun components" do
+      # Denormalizing a static/dynamic fun union must not drop the other parts 
of
+      # the type, nor crash when an arity fails to denormalize.
+
+      # (a) must not raise when denormalization fails for an arity
+      assert difference(fun([term()], atom()), fun([integer()], 
dynamic(atom())))
+             |> to_quoted_string() ==
+               "dynamic((term() -> atom()) and (integer() -> atom()))"
+
+      # (b) non-fun components must be preserved alongside the denormalized fun
+      assert union(
+               integer(),
+               union(fun([integer()], atom()), dynamic(fun([integer()], 
atom())))
+             )
+             |> to_quoted_string() == "(integer() -> atom()) or integer()"
+
+      # several components, with a leftover non-denormalized dynamic arity
+      assert union(
+               atom([:tag]),
+               union(
+                 fun([integer()], atom()),
+                 dynamic(union(fun([integer()], atom()), fun([integer(), 
integer()], atom())))
+               )
+             )
+             |> to_quoted_string() ==
+               "dynamic((integer(), integer() -> atom())) or :tag or 
(integer() -> atom())"
+    end
+
     test "fun (negation)" do
       assert fun([integer()], atom()) |> negation() |> to_quoted_string() ==
                "not (integer() -> atom())"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elixir-1.20.2/lib/elixir/test/elixir/module/types/expr_test.exs 
new/elixir-1.20.3/lib/elixir/test/elixir/module/types/expr_test.exs
--- old/elixir-1.20.2/lib/elixir/test/elixir/module/types/expr_test.exs 
2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/test/elixir/module/types/expr_test.exs 
2026-08-04 16:31:51.000000000 +0200
@@ -148,6 +148,30 @@
                    non_empty_list(term(), term())
                """
     end
+
+    test "++" do
+      assert typecheck!([x], [] ++ String.to_integer(x)) == integer()
+      assert typecheck!([x], [x] ++ []) == non_empty_list(dynamic())
+
+      assert typeerror!([x], String.to_integer(x) ++ []) |> strip_ansi() =~
+               ~l"""
+               incompatible types given to Kernel.++/2:
+
+                   String.to_integer(x) ++ []
+
+               given types:
+
+                   integer(), empty_list()
+
+               but expected one of:
+
+                   #1
+                   empty_list(), term()
+
+                   #2
+                   non_empty_list(term()), term()
+               """
+    end
   end
 
   describe "funs" do
@@ -419,6 +443,13 @@
              ) == dynamic(tuple([integer(), integer(), binary()]))
     end
 
+    test "send returns the message" do
+      assert typecheck!(send(self(), {:msg, 1})) == tuple([atom([:msg]), 
integer()])
+
+      assert typeerror!(send(123, {:msg, 1})) =~
+               "incompatible types given to Kernel.send/2"
+    end
+
     test "undefined function warnings" do
       assert typewarn!(URI.unknown("foo")) ==
                {dynamic(), "URI.unknown/1 is undefined or private"}
@@ -2276,6 +2307,42 @@
       assert typecheck!(false or true) == atom([true])
     end
 
+    test "andalso/orelse do not leak right-hand side refinements" do
+      assert typecheck!(
+               [x],
+               cond do
+                 :erlang.andalso(is_tuple(x), tuple_size(x) == 3) -> {:tuple, 
x}
+                 is_atom(x) -> {:atom, x}
+                 true -> {:other, x}
+               end
+             )
+             |> equal?(
+               dynamic(
+                 tuple([atom([:tuple]), tuple([term(), term(), term()])])
+                 |> union(tuple([atom([:atom]), atom()]))
+                 |> union(tuple([atom([:other]), negation(atom())]))
+               )
+             )
+
+      assert typecheck!(
+               [x],
+               cond do
+                 :erlang.orelse(is_atom(x), is_tuple(x)) -> {:atom_or_tuple, x}
+                 is_binary(x) -> {:binary, x}
+                 true -> {:other, x}
+               end
+             )
+             |> equal?(
+               dynamic(
+                 tuple([atom([:atom_or_tuple]), term()])
+                 |> union(tuple([atom([:binary]), binary()]))
+                 |> union(
+                   tuple([atom([:other]), negation(union(binary(), 
union(atom(), tuple())))])
+                 )
+               )
+             )
+    end
+
     test "and reports violations" do
       assert typeerror!([x = 123], x and true) =~ """
              the following conditional expression will always fail:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elixir-1.20.2/lib/elixir/test/elixir/module/types/infer_test.exs 
new/elixir-1.20.3/lib/elixir/test/elixir/module/types/infer_test.exs
--- old/elixir-1.20.2/lib/elixir/test/elixir/module/types/infer_test.exs        
2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/test/elixir/module/types/infer_test.exs        
2026-08-04 16:31:51.000000000 +0200
@@ -320,7 +320,7 @@
            ]
   end
 
-  test "from defguard (regression with large code generation)", config do
+  test "from single defguard (regression with large code generation)", config 
do
     # As long as it type checks in time, we are fine,
     # but it should infer Macro.t in the future.
     infer config do
@@ -334,6 +334,32 @@
     end
   end
 
+  test "from multiple defguard (regression with large code generation)", 
config do
+    infer config do
+      defguardp is_a(meta) when meta.m != :ppppp or meta.t != :nnnnn
+      defguardp is_b(meta) when is_nil(meta.s) or not meta.config.s.tunable
+      defguardp is_c(meta) when is_nil(meta.k) or not meta.config.k.tunable
+      defguardp is_d(meta) when is_nil(meta.t) or not meta.config.t.tunable
+      defguardp is_e(meta) when not meta.config.m.tunable
+      defguardp is_f(meta) when not meta.config.v.tunable
+
+      defguardp is_all_1(meta)
+                when is_a(meta) and is_b(meta) and is_c(meta) and is_d(meta) 
and
+                       is_e(meta) and is_f(meta)
+
+      defguardp is_all_2(meta)
+                when (meta.m != :ppppp or meta.t != :nnnnn) and
+                       (is_nil(meta.s) or not meta.config.s.tunable) and
+                       (is_nil(meta.k) or not meta.config.k.tunable) and
+                       (is_nil(meta.t) or not meta.config.t.tunable) and
+                       not meta.config.m.tunable and
+                       not meta.config.v.tunable
+
+      def fun_1(%{meta: meta} = _data) when is_all_1(meta), do: :something
+      def fun_2(%{meta: meta} = _data) when is_all_2(meta), do: :something
+    end
+  end
+
   test "from defaults (regression with multiple clauses)", config do
     types =
       infer config do
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elixir-1.20.2/lib/elixir/test/elixir/module/types/pattern_test.exs 
new/elixir-1.20.3/lib/elixir/test/elixir/module/types/pattern_test.exs
--- old/elixir-1.20.2/lib/elixir/test/elixir/module/types/pattern_test.exs      
2026-06-23 14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/elixir/test/elixir/module/types/pattern_test.exs      
2026-08-04 16:31:51.000000000 +0200
@@ -1386,7 +1386,11 @@
     end
 
     test "maps in patterns" do
+      assert precise?([%{}])
       assert precise?([%{ok: _}])
+      assert precise?([%_{}])
+      assert precise?([%x{}])
+      assert precise?([%URI{}])
       assert precise?([%URI{path: _}])
 
       refute precise?([%{ok: 123}])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/lib/iex/lib/iex.ex 
new/elixir-1.20.3/lib/iex/lib/iex.ex
--- old/elixir-1.20.2/lib/iex/lib/iex.ex        2026-06-23 14:02:47.000000000 
+0200
+++ new/elixir-1.20.3/lib/iex/lib/iex.ex        2026-08-04 16:31:51.000000000 
+0200
@@ -785,7 +785,7 @@
     __break__!(ast, module, fun, args, guards, stops, env)
   end
 
-  def __break__!(ast, _stops) do
+  def __break__!(ast, _stops, _env) do
     raise_unknown_break_ast!(ast)
   end
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elixir-1.20.2/lib/iex/test/iex/helpers_test.exs 
new/elixir-1.20.3/lib/iex/test/iex/helpers_test.exs
--- old/elixir-1.20.2/lib/iex/test/iex/helpers_test.exs 2026-06-23 
14:02:47.000000000 +0200
+++ new/elixir-1.20.3/lib/iex/test/iex/helpers_test.exs 2026-08-04 
16:31:51.000000000 +0200
@@ -102,6 +102,12 @@
                    fn -> break!(PryExampleModule, :unknown, 2) end
     end
 
+    test "errors when setting up a break for unknown expression" do
+      assert_raise ArgumentError, ~r"unknown expression to break on", fn ->
+        break!(123)
+      end
+    end
+
     test "errors for non-Elixir modules" do
       assert_raise RuntimeError,
                    "could not set breakpoint, module :maps was not written in 
Elixir",

Reply via email to