1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
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
|