Compare commits

...
9 Commits
19 changed files with 317 additions and 86 deletions
+17 -1
View File
@@ -192,6 +192,22 @@ It also introduces a new compiler option called `:module_definition`, which if t
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
## v1.20.4 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
### 2. Bug fixes
#### Elixir
* [Base] Fix false positives in Base64 validation
* [Kernel] Fix inferred type of `:erlang.split_binary/2` when given a bitstring
* [Kernel] Fix type checker crash on comprehensions with a non-returning `:into` expression
* [String] Fix final sigma handling in Greek casing
* [Task] Fix `Task.yield_many/2` waiting indefinitely when `:limit` exceeds the task count
## v1.20.3 (2026-08-04)
### 1. Enhancements
@@ -206,7 +222,7 @@ You can enable it by setting `elixirc_options: [module_definition: :interpreted]
* [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] Improve type refinement for `and/2` and `or/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
+1 -1
View File
@@ -1 +1 @@
1.20.3
1.20.4
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.3
ELIXIR_VERSION=1.20.4
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.20.3
set ELIXIR_VERSION=1.20.4
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+17 -17
View File
@@ -187,21 +187,21 @@ defmodule Base do
# For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we
# extend the digit range to [0x2F, 0x39], which absorbs '/' into one range
# check — saves one Mycroft singleton. Trick lifted from
# check — saves one singleton term. Trick lifted from
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
@swar_ge_slash 0x51515151515151
# Mycroft zero-byte detection for base64 singletons (+, -, _).
# Per lane: high bit set iff `bxor(w, K*ones) - 0x01..01` has its high bit
# set, i.e. that byte's V value was 0 → original byte was K. Simplified
# (no `bnot V` term) — for ASCII-gated `w`, borrow-propagation false
# positives only occur for adjacent bytes that happen to equal `K xor 0x01`,
# which is outside the base64 alphabet, so it never matters here.
# Pattern follows https://github.com/elixir-lang/elixir/pull/15255.
@swar_mask01 0x01010101010101
@swar_plus_x7 0x2B2B2B2B2B2B2B
@swar_dash_x7 0x2D2D2D2D2D2D2D
@swar_under_x7 0x5F5F5F5F5F5F5F
# base64's '+', '-' and '_' border no other range, so each is checked as the
# degenerate range [c, c]. Mycroft zero-byte detection is one operation
# cheaper per singleton but not exact per lane: subtracting 0x01..01 makes a
# zero lane borrow from the lane above it, and a lane holding 1 turns that
# borrow into 0xFF, so bytes equal to `c bxor 0x01` read as matches too.
@swar_ge_plus 0x55555555555555
@swar_gt_plus 0x54545454545454
@swar_ge_dash 0x53535353535353
@swar_gt_dash 0x52525252525252
@swar_ge_underscore 0x21212121212121
@swar_gt_underscore 0x20202020202020
# Per-byte validity checks (used in both the SWAR clauses for the 8th byte
# of each stride and in the body of the sub-8-byte tail clauses).
@@ -365,8 +365,8 @@ defmodule Base do
# base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons.
# For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as
# part of one range (Lemire merge), leaving only '+' as a Mycroft singleton.
# For url, the singletons '-' and '_' are detected via two Mycroft terms.
# part of one range (Lemire merge), leaving only '+' as a singleton. For url,
# '-' and '_' are two more singletons.
defp valid_word64base?(w) do
band(w, @swar_mask80) == 0 and
band(
@@ -378,7 +378,7 @@ defmodule Base do
),
bxor(w + @swar_ge_slash, w + @swar_gt_9)
),
bxor(w, @swar_plus_x7) - @swar_mask01
bxor(w + @swar_ge_plus, w + @swar_gt_plus)
),
@swar_mask80
) == @swar_mask80
@@ -396,8 +396,8 @@ defmodule Base do
bxor(w + @swar_ge_0, w + @swar_gt_9)
),
bor(
bxor(w, @swar_dash_x7) - @swar_mask01,
bxor(w, @swar_under_x7) - @swar_mask01
bxor(w + @swar_ge_dash, w + @swar_gt_dash),
bxor(w + @swar_ge_underscore, w + @swar_gt_underscore)
)
),
@swar_mask80
+25 -5
View File
@@ -387,13 +387,20 @@ defimpl Inspect, for: List do
close = color_doc("]", :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_doc(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
@@ -407,6 +414,19 @@ defimpl Inspect, for: List 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_doc(Macro.inspect_atom(:key, key), :atom, opts)
+2 -2
View File
@@ -21,8 +21,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.
+2 -2
View File
@@ -1193,7 +1193,7 @@ defmodule 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) ->
@@ -1245,7 +1245,7 @@ defmodule 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) ->
+5 -1
View File
@@ -213,7 +213,11 @@ defmodule Module.Types.Apply do
{:erlang, :spawn_link, [{mfargs, pid()}]},
{:erlang, :spawn_monitor, [{[fun(0)], tuple([pid(), reference()])}]},
{:erlang, :spawn_monitor, [{mfargs, tuple([pid(), reference()])}]},
{:erlang, :split_binary, [{[binary(), integer()], tuple([binary(), binary()])}]},
{:erlang, :split_binary,
[
{[binary(), integer()], tuple([binary(), binary()])},
{[bitstring_no_binary(), integer()], tuple([binary(), bitstring_no_binary()])}
]},
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
+3
View File
@@ -888,6 +888,9 @@ defmodule Module.Types.Expr do
{true, false} ->
{type, :bitstring, context}
{false, false} ->
{type, :none, context}
end
else
{_type, context} =
+30 -6
View File
@@ -1008,6 +1008,10 @@ defmodule String do
IO.iodata_to_binary(downcase_ascii(string))
end
def downcase(string, :greek) when is_binary(string) do
String.Unicode.downcase_greek(string, [])
end
def downcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
String.Unicode.downcase(string, [], mode)
end
@@ -1058,19 +1062,39 @@ defmodule String do
@letter_I_dot_above <<0x0130::utf8>>
def capitalize(<<@letter_i, right::binary>>, mode) do
if(mode == :turkic, do: @letter_I_dot_above, else: @letter_I) <> downcase(right, mode)
capitalized = if mode == :turkic, do: @letter_I_dot_above, else: @letter_I
capitalized <> downcase_after_capitalize(right, mode, ?i)
end
def capitalize(string, mode) when is_binary(string) do
case :unicode_util.gc(string) do
[gc] -> grapheme_to_binary(:string.titlecase([gc]))
[gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
[gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
[] -> ""
{:error, <<byte, rest::bits>>} -> <<byte>> <> downcase(rest, mode)
[gc] ->
grapheme_to_binary(:string.titlecase([gc]))
[gc, rest] ->
grapheme_to_binary(:string.titlecase([gc])) <>
downcase_after_capitalize(rest, mode, gc)
[gc | rest] ->
grapheme_to_binary(:string.titlecase([gc])) <>
downcase_after_capitalize(rest, mode, gc)
[] ->
""
{:error, <<byte, rest::bits>>} ->
<<byte>> <> downcase(rest, mode)
end
end
defp downcase_after_capitalize(rest, :greek, gc) do
String.Unicode.downcase_greek(rest, [], grapheme_to_binary(gc))
end
defp downcase_after_capitalize(rest, mode, _gc) do
downcase(rest, mode)
end
@doc false
@deprecated "Use String.trim_trailing/1 instead"
defdelegate rstrip(binary), to: String.Break, as: :trim_trailing
+2 -1
View File
@@ -1248,9 +1248,10 @@ defmodule Task do
{ref, nil}
end)
ref_count = map_size(refs)
on_timeout = Keyword.get(opts, :on_timeout, :nothing)
timeout = Keyword.get(opts, :timeout, 5_000)
limit = Keyword.get(opts, :limit, map_size(refs))
limit = min(Keyword.get(opts, :limit, ref_count), ref_count)
timeout_ref = make_ref()
timer_ref =
+40 -9
View File
@@ -263,6 +263,14 @@ defmodule BaseTest do
refute valid64?("Zm9)")
end
test "valid64?/1 returns false on a non-alphabet character before a ?+" do
# ?* is ?+ with its low bit flipped
for pos <- 0..14, char <- ~c"*),^ \t" do
string = String.duplicate("A", pos) <> <<char, ?+>> <> String.duplicate("A", 14 - pos)
refute valid64?(string), "expected #{inspect(string)} to be invalid"
end
end
test "valid64?/1 returns false on whitespace unless there's ignore: :whitespace" do
refute valid64?("\nQWxhZGRp bjpvcGVu\sIHNlc2Ft\t")
@@ -446,6 +454,14 @@ defmodule BaseTest do
refute url_valid64?("Zm9)")
end
test "url_valid64?/1 returns false on a non-alphabet character before a ?- or ?_" do
# ?, and ?^ are ?- and ?_ with their low bits flipped
for pos <- 0..14, char <- ~c",^*) \t", next <- ~c"-_" do
string = String.duplicate("A", pos) <> <<char, next>> <> String.duplicate("A", 14 - pos)
refute url_valid64?(string), "expected #{inspect(string)} to be invalid"
end
end
test "url_valid64?/1 returns false on whitespace unless there's ignore: :whitespace" do
refute url_valid64?("\nQWxhZGRp bjpvcGVu\sIHNlc2Ft\t")
@@ -1054,14 +1070,13 @@ defmodule BaseTest do
assert hex_valid32?("cPNg====", case: :mixed, padding: false)
end
# TODO: add valid? tests
test "encode then decode is identity" do
test "encode then decode is identity, and valid? agrees with decode" do
for {encode, decode, valid?} <- [
{&encode16/2, &decode16!/2, &valid16?/2},
{&encode32/2, &decode32!/2, &valid32?/2},
{&hex_encode32/2, &hex_decode32!/2, &hex_valid32?/2},
{&encode64/2, &decode64!/2, &valid64?/2},
{&url_encode64/2, &url_decode64!/2, &url_valid64?/2}
{&encode16/2, &decode16/2, &valid16?/2},
{&encode32/2, &decode32/2, &valid32?/2},
{&hex_encode32/2, &hex_decode32/2, &hex_valid32?/2},
{&encode64/2, &decode64/2, &valid64?/2},
{&url_encode64/2, &url_decode64/2, &url_valid64?/2}
],
encode_case <- [:upper, :lower],
decode_case <- [:upper, :lower, :mixed],
@@ -1089,10 +1104,26 @@ defmodule BaseTest do
decode_opts = Keyword.take([case: decode_case, padding: pad?], allowed_opts)
assert valid?.(encoded, decode_opts)
expected = decode.(encoded, decode_opts)
assert data == expected,
assert decode.(encoded, decode_opts) == {:ok, data},
"identity did not match for #{inspect(data)} when #{inspect(encode)} (#{encode_case})"
size = byte_size(encoded)
if size > 0 do
pos = :rand.uniform(size) - 1
taken = min(2, size - pos)
corruption = for _ <- 1..taken, into: <<>>, do: <<:rand.uniform(256) - 1>>
corrupted =
binary_part(encoded, 0, pos) <>
corruption <>
binary_part(encoded, pos + taken, size - pos - taken)
assert valid?.(corrupted, decode_opts) ==
match?({:ok, _}, decode.(corrupted, decode_opts)),
"valid? and decode disagree on #{inspect(corrupted)}"
end
end
end
end
+16 -1
View File
@@ -312,10 +312,15 @@ defmodule Inspect.ListTest 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
@@ -340,10 +345,20 @@ defmodule Inspect.ListTest 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
+12
View File
@@ -359,6 +359,12 @@ defmodule ListTest 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
@@ -377,6 +383,12 @@ defmodule ListTest 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
@@ -3223,6 +3223,10 @@ defmodule Module.Types.ExprTest do
"""
end
test ":into with non-returning collectable" do
assert typecheck!([list], for(x <- list, into: raise("oops"), do: x)) == none()
end
test ":reduce checks" do
assert typecheck!(
[list],
+9
View File
@@ -232,6 +232,11 @@ defmodule StringTest do
assert String.downcase("Σ ΣΑΣ Σ", :greek) == "σ σας σ"
assert String.downcase("ΜΕΣ'ΑΠΟ", :greek) == "μεσ'απο"
assert String.downcase("ΑΣ'ΤΟΥΣ", :greek) == "ασ'τους"
assert String.downcase("AΣ", :greek) == "aς"
assert String.downcase("AΣA", :greek) == "aσa"
assert String.downcase("A'Σ", :greek) == "a'ς"
assert String.downcase("İΣ", :greek) == "i̇ς"
assert String.downcase("ⅠΣ", :greek) == "ⅰς"
end
test "downcase/1 with ascii" do
@@ -274,6 +279,10 @@ defmodule StringTest do
assert String.capitalize("İii", :turkic) == "İii"
assert String.capitalize("Iıı", :turkic) == "Iıı"
assert String.capitalize("AΣ", :greek) == "Aς"
assert String.capitalize("iΣ", :greek) == "Iς"
assert String.capitalize("1Σ", :greek) == "1σ"
assert String.capitalize(<<138, ?B, ?C>>) == <<138, ?b, ?c>>
assert String.capitalize(<<225, 158, 128, 225, 158, 185, 225>>) ==
+15
View File
@@ -630,6 +630,21 @@ defmodule TaskTest do
[{task2, nil}, {task3, {:exit, :normal}}]
end
test "returns once all tasks have replied below limit" do
parent = self()
pid =
spawn(fn ->
task = Task.async(fn -> :done end)
result = Task.yield_many([task], limit: 2, timeout: :infinity)
send(parent, {self(), result})
end)
on_exit(fn -> Process.exit(pid, :kill) end)
assert_receive {^pid, [{%Task{}, {:ok, :done}}]}, 500
end
test "returns results from multiple tasks with limit and on timeout" do
Process.flag(:trap_exit, true)
task1 = Task.async(fn -> Process.sleep(:infinity) end)
+115 -38
View File
@@ -46,6 +46,35 @@ rangify = fn [head | tail] ->
[{first, last} | acc]
end
expand_range = fn range ->
[range | _] = :binary.split(range, " ")
case :binary.split(range, "..") do
[first] ->
[String.to_integer(first, 16)]
[first, last] ->
Enum.to_list(String.to_integer(first, 16)..String.to_integer(last, 16))
end
end
other_cased_letters =
Path.join(__DIR__, "PropList.txt")
|> File.read!()
|> String.split(["\r\n", "\n"])
|> Enum.reduce([], fn line, acc ->
case :binary.split(line, ";") do
[range, <<" Other_Lowercase", _::binary>>] ->
expand_range.(range) ++ acc
[range, <<" Other_Uppercase", _::binary>>] ->
expand_range.(range) ++ acc
_ ->
acc
end
end)
# A character is case ignorable if:
#
# Word_Break(C) = MidLetter or MidNumLet or Single_Quote, or
@@ -152,6 +181,17 @@ case_ignorable_categories = :binary.compile_pattern(["Mn", "Me", "Cf", "Lm", "Sk
{cacc, lacc, iacc, wacc, dacc, kacc}
end)
cased_letters = Enum.sort(Enum.uniq(cased_letters ++ other_cased_letters), :desc)
case_ignorable = Enum.sort(Enum.uniq(case_ignorable), :desc)
case_ignorable_map =
Enum.reduce(case_ignorable, %{}, fn codepoint, acc ->
:maps.put(codepoint, true, acc)
end)
cased_non_ignorable = Enum.reject(cased_letters, &:maps.is_key(&1, case_ignorable_map))
defmodule String.Unicode do
@moduledoc false
def version, do: {17, 0, 0}
@@ -236,6 +276,19 @@ defmodule String.Unicode do
# Downcase
def downcase_greek(string, acc) do
string
|> greek_final_sigma([], false)
|> downcase(acc, :default)
end
# Used by capitalize/2 to preserve the context supplied by the first grapheme.
def downcase_greek(string, acc, before) when is_binary(before) do
string
|> greek_final_sigma([], cased_letter_context(before, false))
|> downcase(acc, :default)
end
# Turkic İ -> i
def downcase(<<unquote(@letter_I_dot_above), rest::bits>>, acc, mode) do
char = if mode == :turkic, do: @letter_i, else: <<@letter_i, @combining_dot_above>>
@@ -255,14 +308,7 @@ defmodule String.Unicode do
# Greek sigma
def downcase(<<@letter_sigma, rest::bits>>, acc, mode) do
downcased =
if mode == :greek and cased_letter_list?(acc) and not cased_letter_binary?(rest) do
@letter_small_sigma_final
else
@letter_small_sigma
end
downcase(rest, [downcased | acc], mode)
downcase(rest, [@letter_small_sigma | acc], mode)
end
conditional_downcase = [@letter_I, @letter_I_dot_above, @letter_sigma]
@@ -300,49 +346,80 @@ defmodule String.Unicode do
# Sigma handling
defp greek_final_sigma(string, acc, cased_before) do
case :binary.match(string, @letter_sigma) do
{index, _length} ->
<<before::binary-size(^index), @letter_sigma, rest::bits>> = string
cased_before = cased_letter_context(before, cased_before)
downcased =
if cased_before and not cased_letter_binary?(rest) do
@letter_small_sigma_final
else
@letter_small_sigma
end
greek_final_sigma(rest, [downcased, before | acc], true)
:nomatch ->
if acc == [] do
string
else
IO.iodata_to_binary(:lists.reverse([string | acc]))
end
end
end
defp cased_letter_binary?(<<codepoint::utf8, rest::bits>>) do
if case_ignorable?(codepoint) do
cased_letter_binary?(rest)
else
cased_letter?(codepoint)
case case_context(codepoint) do
:ignorable -> cased_letter_binary?(rest)
:cased -> true
:other -> false
end
end
defp cased_letter_binary?(_), do: false
defp cased_letter_list?([<<codepoint::utf8>> | rest]) do
if case_ignorable?(codepoint) do
cased_letter_list?(rest)
else
cased_letter?(codepoint)
end
end
defp cased_letter_list?(_), do: false
for {first, last} <- rangify.(cased_letters) do
if first == last do
defp cased_letter?(unquote(first)), do: true
else
defp cased_letter?(codepoint)
when codepoint >= unquote(first) and codepoint <= unquote(last),
do: true
end
end
defp cased_letter?(_), do: false
for {first, last} <- rangify.(case_ignorable) do
if first == last do
defp case_ignorable?(unquote(first)), do: true
defp case_context(unquote(first)), do: :ignorable
else
defp case_ignorable?(codepoint)
defp case_context(codepoint)
when codepoint >= unquote(first) and codepoint <= unquote(last),
do: true
do: :ignorable
end
end
defp case_ignorable?(_), do: false
for {first, last} <- rangify.(cased_non_ignorable) do
if first == last do
defp case_context(unquote(first)), do: :cased
else
defp case_context(codepoint)
when codepoint >= unquote(first) and codepoint <= unquote(last),
do: :cased
end
end
defp case_context(_codepoint), do: :other
defp cased_letter_context(<<codepoint::utf8, rest::bits>>, cased_before) do
cased_before = update_cased_context(codepoint, cased_before)
cased_letter_context(rest, cased_before)
end
defp cased_letter_context(<<_byte, rest::bits>>, _cased_before) do
cased_letter_context(rest, false)
end
defp cased_letter_context("", cased_before), do: cased_before
defp update_cased_context(codepoint, cased_before) when is_integer(codepoint) do
case case_context(codepoint) do
:ignorable -> cased_before
:cased -> true
:other -> false
end
end
# Upcase