From 85338df6fc08facfda46b32f5abb3a914d73c7b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Valim?= <jose.valim@gmail.com>
Date: Tue, 25 Aug 2026 11:27:54 +0200
Subject: [PATCH] Fix recursion on charlist error path

---
 lib/elixir/lib/inspect.ex               | 30 ++++++++++++++++++++-----
 lib/elixir/lib/inspect/algebra.ex       |  4 ++--
 lib/elixir/lib/list.ex                  |  4 ++--
 lib/elixir/test/elixir/inspect_test.exs | 17 +++++++++++++-
 lib/elixir/test/elixir/list_test.exs    | 12 ++++++++++
 5 files changed, 57 insertions(+), 10 deletions(-)

diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex
index 637bca8a6..d5b876fcf 100644
--- a/lib/elixir/lib/inspect.ex
+++ b/lib/elixir/lib/inspect.ex
@@ -264,13 +264,20 @@ def inspect(term, opts) do
     close = color("]", :list, opts)
 
     cond do
-      lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
-        inspected =
-          case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
-            {escaped, ""} -> [?~, ?c, ?", escaped, ?"]
-            {escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
+      (lists == :as_charlists and unicode_list?(term, printable_limit)) or
+          (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
+        {split, tail} =
+          if is_integer(printable_limit) do
+            case Enum.split(term, printable_limit) do
+              {split, []} -> {split, []}
+              {split, _} -> {split, " ++ ..."}
+            end
+          else
+            {term, []}
           end
 
+        {escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
+        inspected = [?~, ?c, ?", escaped, ?" | tail]
         color(IO.iodata_to_binary(inspected), :charlist, opts)
 
       keyword?(term) ->
@@ -281,6 +288,19 @@ def inspect(term, opts) do
     end
   end
 
+  defp unicode_list?(_, 0), do: true
+
+  defp unicode_list?([char | rest], counter)
+       when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
+       do: unicode_list?(rest, decrement(counter))
+
+  defp unicode_list?([], _counter), do: true
+  defp unicode_list?(_, _counter), do: false
+
+  @compile {:inline, decrement: 1}
+  defp decrement(:infinity), do: :infinity
+  defp decrement(counter), do: counter - 1
+
   @doc false
   def keyword({key, value}, opts) do
     key = color(Macro.inspect_atom(:key, key), :atom, opts)
diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex
index 5e213b377..941aa5525 100644
--- a/lib/elixir/lib/inspect/algebra.ex
+++ b/lib/elixir/lib/inspect/algebra.ex
@@ -17,8 +17,8 @@ defmodule Inspect.Opts do
       is `:decimal` and if it is printable, otherwise in bit syntax. See
       `String.printable?/1` to learn when a string is printable.
 
-    * `:charlists` - when `:as_charlists` all lists will be printed as charlists,
-      non-printable elements will be escaped.
+    * `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
+      non-printable code points will be escaped. Other lists will be printed as lists.
 
       When `:as_lists` all lists will be printed as lists.
 
diff --git a/lib/elixir/lib/list.ex b/lib/elixir/lib/list.ex
index a8155deed..ea4a69abd 100644
--- a/lib/elixir/lib/list.ex
+++ b/lib/elixir/lib/list.ex
@@ -1093,7 +1093,7 @@ def to_string(list) when is_list(list) do
 
         Please check the given list or call inspect/1 to get the list representation, got:
 
-        #{inspect(list)}
+        #{inspect(list, charlists: :as_lists)}
         """
     else
       result when is_binary(result) ->
@@ -1145,7 +1145,7 @@ def to_charlist(list) when is_list(list) do
 
         Please check the given list or call inspect/1 to get the list representation, got:
 
-        #{inspect(list)}
+        #{inspect(list, charlists: :as_lists)}
         """
     else
       result when is_list(result) ->
diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs
index a8c12c36e..f90b69a2c 100644
--- a/lib/elixir/test/elixir/inspect_test.exs
+++ b/lib/elixir/test/elixir/inspect_test.exs
@@ -276,10 +276,15 @@ test "printable" do
 
   test "printable limit" do
     assert inspect(~c"hello world", printable_limit: 4) == ~s(~c"hell" ++ ...)
+    assert inspect(~c"hello", printable_limit: :infinity) == ~s(~c"hello")
     # Non printable characters after the limit don't matter
     assert inspect(~c"hello world" ++ [0], printable_limit: 4) == ~s(~c"hell" ++ ...)
     # Non printable strings aren't affected by printable limit
     assert inspect([0, 1, 2, 3, 4], printable_limit: 3) == ~s([0, 1, 2, 3, 4])
+    # Ensure a non-printable value after limit does not crash
+    assert inspect(~c"hello world" ++ [nil], printable_limit: 4) == ~s(~c"hell" ++ ...)
+    # Ensure a non-printable value after default limit does not hang
+    assert String.ends_with?(inspect(List.duplicate(?a, 5000) ++ [nil]), "\" ++ ...")
   end
 
   test "keyword" do
@@ -304,10 +309,20 @@ test "opt infer" do
     assert inspect([0], charlists: :infer) == "[0]"
   end
 
-  test "opt as strings" do
+  test "opt as charlists" do
     assert inspect(~c"john" ++ [0] ++ ~c"doe", charlists: :as_charlists) == ~s(~c"john\\0doe")
     assert inspect(~c"john", charlists: :as_charlists) == ~s(~c"john")
     assert inspect([0], charlists: :as_charlists) == ~s(~c"\\0")
+    assert inspect([nil], charlists: :as_charlists) == "[nil]"
+    assert inspect([0xD800], charlists: :as_charlists) == "[55296]"
+    assert inspect([1_114_112], charlists: :as_charlists) == "[1114112]"
+    assert inspect([[?a]], charlists: :as_charlists) == ~s([~c"a"])
+
+    assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 4) ==
+             "[97, 98, 99 | nil]"
+
+    assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 3) ==
+             ~s(~c"abc" ++ ...)
   end
 
   test "opt as lists" do
diff --git a/lib/elixir/test/elixir/list_test.exs b/lib/elixir/test/elixir/list_test.exs
index ee02a072a..ec4725877 100644
--- a/lib/elixir/test/elixir/list_test.exs
+++ b/lib/elixir/test/elixir/list_test.exs
@@ -309,6 +309,12 @@ test "to_string/1" do
     assert_raise ArgumentError, ~r"cannot convert the given list to a string", fn ->
       List.to_string([:a, :b])
     end
+
+    invalid = List.duplicate(?a, 4096) ++ [nil]
+
+    assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
+      List.to_string(invalid)
+    end
   end
 
   test "to_charlist/1" do
@@ -327,6 +333,12 @@ test "to_charlist/1" do
     assert_raise ArgumentError, ~r"cannot convert the given list to a charlist", fn ->
       List.to_charlist([:a, :b])
     end
+
+    invalid = List.duplicate(?a, 4096) ++ [nil]
+
+    assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
+      List.to_charlist(invalid)
+    end
   end
 
   describe "myers_difference/2" do
-- 
2.52.0

