Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
759443e724 | ||
|
|
0bba588757 | ||
|
|
04c31907c5 | ||
|
|
153489f9a6 | ||
|
|
3ecb083d5d | ||
|
|
d2689c839e | ||
|
|
309623adf1 | ||
|
|
ae432598e4 | ||
|
|
31288d2267 |
+17
-1
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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()}]},
|
||||
|
||||
|
||||
@@ -888,6 +888,9 @@ defmodule Module.Types.Expr do
|
||||
|
||||
{true, false} ->
|
||||
{type, :bitstring, context}
|
||||
|
||||
{false, false} ->
|
||||
{type, :none, context}
|
||||
end
|
||||
else
|
||||
{_type, context} =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>>) ==
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user