Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb95bd4b4e | ||
|
|
36351ae051 | ||
|
|
5034b7fa6b | ||
|
|
360ed75d17 | ||
|
|
7f3b294e19 | ||
|
|
7427c0cf5d | ||
|
|
0208ff88c3 | ||
|
|
a46b95b816 | ||
|
|
7223808989 | ||
|
|
24d5016632 | ||
|
|
1d0094e3f8 | ||
|
|
bb69e59b5c | ||
|
|
269b4a334b | ||
|
|
695401e58b | ||
|
|
2e9ce85e91 | ||
|
|
4a74923ebb | ||
|
|
48ff182db0 | ||
|
|
6d4d45a268 | ||
|
|
82b1360aa4 | ||
|
|
ccab980101 | ||
|
|
6f635d4f12 | ||
|
|
3b5bcaa6d8 | ||
|
|
b0d02d3965 | ||
|
|
96d47e12b2 | ||
|
|
394113faac | ||
|
|
bef6436cda | ||
|
|
fdebc8cba6 | ||
|
|
a5cfa254a3 | ||
|
|
aba75ca1b2 | ||
|
|
d4b2c58979 | ||
|
|
6fde3de13f | ||
|
|
936078255d | ||
|
|
4d242a8c3d | ||
|
|
a229c0e301 | ||
|
|
87af1e34ec | ||
|
|
3a21a6a50a | ||
|
|
1962a66575 | ||
|
|
6a44ab621a | ||
|
|
c6d5bbcd9d | ||
|
|
4fbdbc8acd | ||
|
|
05576dd133 | ||
|
|
9e2f0ff348 | ||
|
|
c86e8d2b54 | ||
|
|
7bd7665a9b | ||
|
|
0ae594d716 | ||
|
|
dbd2fadba1 | ||
|
|
a164205866 | ||
|
|
26f6dbdb2a | ||
|
|
0c8f1feb3a | ||
|
|
1eff1acffd | ||
|
|
dc939db4ad | ||
|
|
b945cee1a9 | ||
|
|
78f22cc8ef | ||
|
|
1d661fb91d | ||
|
|
4147e50ea0 | ||
|
|
5779f5be37 | ||
|
|
3389f84059 | ||
|
|
7d2a3d8a69 | ||
|
|
21a1e47740 | ||
|
|
4bc288d47d | ||
|
|
2b2a99d0cf | ||
|
|
e3d2cea83a | ||
|
|
093d4c8873 | ||
|
|
628afe5b74 | ||
|
|
3d8b254724 | ||
|
|
01ff4a5ec2 | ||
|
|
63a7a054ce | ||
|
|
583150d002 | ||
|
|
6e05add26b | ||
|
|
4f0c820624 | ||
|
|
11b7f08b0b | ||
|
|
30af9521cf | ||
|
|
5d605debe1 | ||
|
|
345507aca9 | ||
|
|
c4b42ddbbe | ||
|
|
15572f54c1 | ||
|
|
fee29461cb | ||
|
|
4616a06f73 | ||
|
|
6cde8680ab | ||
|
|
cb5fb8e2e0 | ||
|
|
11e920f9e4 | ||
|
|
b703302cd1 | ||
|
|
02124013de | ||
|
|
11d2adf0ce | ||
|
|
c62216d7cb | ||
|
|
9417808a56 | ||
|
|
2db772efa6 | ||
|
|
f6e177092e | ||
|
|
82196d2bd9 | ||
|
|
99b6970093 | ||
|
|
f3a86d828b | ||
|
|
5b682995bf | ||
|
|
a6e7663f8f | ||
|
|
cf7e87a637 | ||
|
|
e1738affb4 | ||
|
|
e3327051f9 | ||
|
|
f919b3e22b | ||
|
|
a704e91133 | ||
|
|
a357d19110 | ||
|
|
2096ac050d | ||
|
|
7c4552c156 | ||
|
|
bc7844c26c | ||
|
|
f67cbf14a6 | ||
|
|
a89dcdc865 | ||
|
|
c929e885bb | ||
|
|
9158e33883 | ||
|
|
7bf65189db | ||
|
|
f316902a80 | ||
|
|
b911bb0380 | ||
|
|
37c7da4fbf | ||
|
|
6ef6f78206 | ||
|
|
024beb1bfc | ||
|
|
44085d4347 | ||
|
|
0bd52d4a7e | ||
|
|
48c515f994 | ||
|
|
81fb10eec8 | ||
|
|
8b584147e8 | ||
|
|
5e7cb9a707 | ||
|
|
2971952edc | ||
|
|
309bb8a875 | ||
|
|
e697189e3b | ||
|
|
842faf0ec5 | ||
|
|
63e5a53925 | ||
|
|
79cf2ffa15 | ||
|
|
6c927126bb | ||
|
|
7df894e181 | ||
|
|
7ac3ec1a39 | ||
|
|
d5f4d491c9 | ||
|
|
44405c300b | ||
|
|
a9021b1c8d | ||
|
|
25bba6c57e | ||
|
|
8c49052a41 | ||
|
|
dc1ac5f461 | ||
|
|
03dda70bdd | ||
|
|
1a711f95d3 | ||
|
|
df537c4d44 | ||
|
|
a46b6e3ade | ||
|
|
ca29070564 | ||
|
|
a0b17a355a | ||
|
|
a5e437e2cd | ||
|
|
4434e1f5ea | ||
|
|
793583d057 | ||
|
|
2d60a959c0 | ||
|
|
2d0e5bab83 | ||
|
|
8425a039f2 | ||
|
|
138680489e | ||
|
|
550b01db0d | ||
|
|
e7a03d2feb | ||
|
|
8a1bbb59df | ||
|
|
745ee1d538 | ||
|
|
ce47442332 | ||
|
|
6c413a893b | ||
|
|
6ef392a98d | ||
|
|
2dcba2c0cb | ||
|
|
9f490a187f | ||
|
|
7c836e221a | ||
|
|
a598106b23 | ||
|
|
4b4ac32b73 | ||
|
|
9e78534880 | ||
|
|
b8c1eb7de2 | ||
|
|
4e616904c1 | ||
|
|
44ea7a621e | ||
|
|
a8d9921a89 | ||
|
|
82e17e8bc3 | ||
|
|
17f782e0eb | ||
|
|
6fda428a21 | ||
|
|
e303c78201 | ||
|
|
0f832059da | ||
|
|
3b96b74a51 | ||
|
|
70624eff47 | ||
|
|
ffcea82a1e | ||
|
|
a8f18d8105 | ||
|
|
aaaa921781 | ||
|
|
e20ca7e788 | ||
|
|
e308336c94 | ||
|
|
f08dff3536 | ||
|
|
8f63ee11b3 | ||
|
|
5d2ac0ca69 | ||
|
|
6e57f00e4b | ||
|
|
1570992d04 | ||
|
|
e713dc9c9b | ||
|
|
8eb51e6494 | ||
|
|
e641683a41 | ||
|
|
4270828c93 | ||
|
|
002e42d574 | ||
|
|
e94774926b | ||
|
|
7918013cfd | ||
|
|
fa7db38e6f | ||
|
|
1a63a83e07 | ||
|
|
cff8010e80 | ||
|
|
369f9b8844 | ||
|
|
dbf74a6098 | ||
|
|
450a24c840 | ||
|
|
ba04ada65d | ||
|
|
e25f52019a | ||
|
|
8ea2a5dc17 | ||
|
|
85b5838020 | ||
|
|
f16d8d290f | ||
|
|
6689fc1ff4 | ||
|
|
34abdd4361 | ||
|
|
9a6a7b38a8 | ||
|
|
d7c0671877 | ||
|
|
27438994fd | ||
|
|
92648c7eb2 | ||
|
|
dae2bcb4bd | ||
|
|
7bb87a7d0a | ||
|
|
62e5ebbda9 | ||
|
|
751a518d06 | ||
|
|
895b4064c9 | ||
|
|
0a21418534 | ||
|
|
ca719e08fb | ||
|
|
2e2fdf84c8 | ||
|
|
8564fd506a | ||
|
|
59e257d3ca | ||
|
|
51ac6a5c0f | ||
|
|
0821930417 | ||
|
|
f9a7552ab5 | ||
|
|
f2ad598ca3 | ||
|
|
1bfa86baf3 | ||
|
|
4720b39284 | ||
|
|
4fbe8ee9a7 | ||
|
|
d0b5519700 | ||
|
|
84f3ed66c9 | ||
|
|
7571ff2672 | ||
|
|
58b62fb594 | ||
|
|
0676818543 | ||
|
|
c423e83fb0 | ||
|
|
7e9c89780c | ||
|
|
3cf8682687 | ||
|
|
d1a33f8b63 | ||
|
|
2429df6a6a | ||
|
|
1db0a0c3fd | ||
|
|
92c3b9fddd | ||
|
|
9ed6724440 | ||
|
|
4c42f94aa2 | ||
|
|
025d8ef8ce | ||
|
|
e578a71b79 | ||
|
|
1a4a91a1f3 | ||
|
|
ac8c3dd3ab | ||
|
|
6adff3fcc2 | ||
|
|
259d52cba7 | ||
|
|
32597f08f7 | ||
|
|
a3522e4a25 | ||
|
|
8fcf677082 | ||
|
|
6b61234adb | ||
|
|
717952aacf | ||
|
|
5cad659e6d | ||
|
|
b3e4738a4c | ||
|
|
f54719040c | ||
|
|
f3aab1140b | ||
|
|
5b21c6a168 | ||
|
|
48c124abbe | ||
|
|
b7a82bfcea | ||
|
|
d0fb26a68a | ||
|
|
a324c883ce | ||
|
|
f401038c31 | ||
|
|
6d12dcecf7 | ||
|
|
e746c042bb | ||
|
|
4e840bc094 | ||
|
|
345deb35cc | ||
|
|
b7e22baa45 | ||
|
|
1d9d560fcd | ||
|
|
6957e15210 | ||
|
|
b82c44a109 | ||
|
|
1cb2089367 | ||
|
|
c7586755a6 | ||
|
|
77426cc770 | ||
|
|
ecf55172c7 | ||
|
|
251d3fe5b2 | ||
|
|
16bee46eef | ||
|
|
b639d87c9f | ||
|
|
b3797d0cf5 | ||
|
|
065b8532c0 | ||
|
|
0aa4acef63 | ||
|
|
233f824c16 | ||
|
|
a3bfe9d407 | ||
|
|
35cceec15b | ||
|
|
9c83ece467 | ||
|
|
9e687da4a9 | ||
|
|
681a9ee9cc | ||
|
|
2fbe44ef64 | ||
|
|
ad993f0d80 | ||
|
|
82fac2fd38 | ||
|
|
2bcaaf8a62 | ||
|
|
56e4e28d86 | ||
|
|
bec8d39796 | ||
|
|
6f9f2d1429 | ||
|
|
1c8b15897b | ||
|
|
e0d257251b | ||
|
|
b3c0f259ef | ||
|
|
b680bea383 | ||
|
|
9cb8bb63ce | ||
|
|
66ca149315 | ||
|
|
68b660d8e8 | ||
|
|
a897d59df7 | ||
|
|
bd36b69cec | ||
|
|
eecafef2c0 | ||
|
|
1ad6e735cb | ||
|
|
5fd13212d9 | ||
|
|
46c461e617 | ||
|
|
9a2029245d | ||
|
|
b4bd740eb7 | ||
|
|
940d01c1c4 | ||
|
|
f4bf3fd20e | ||
|
|
1d3a5b85f4 | ||
|
|
34c7f2106c | ||
|
|
42cf846de0 | ||
|
|
94dc133058 | ||
|
|
9347ccd564 | ||
|
|
4215bbe986 | ||
|
|
f70e504ebb | ||
|
|
73cf46d785 | ||
|
|
ec50a88ee4 | ||
|
|
311f6fb57d | ||
|
|
8e27ce49a3 | ||
|
|
afedada0d1 | ||
|
|
1e12c7d6be | ||
|
|
264200a4a6 | ||
|
|
39acd239af | ||
|
|
1ac9007bae | ||
|
|
72e5d84b4e | ||
|
|
0cdaea1a8c | ||
|
|
a9976cc5d9 | ||
|
|
fce9b1c0d4 | ||
|
|
83e9b66e65 | ||
|
|
8efdf4eb10 | ||
|
|
56bc730a2a | ||
|
|
fd1109e859 | ||
|
|
1d599978f7 | ||
|
|
b64b214412 | ||
|
|
c540d1fa94 | ||
|
|
75536d80f1 | ||
|
|
0395301060 | ||
|
|
59434b1590 | ||
|
|
97770a660a | ||
|
|
23d9bfd3e1 | ||
|
|
99cd654460 | ||
|
|
45ef8fd94a | ||
|
|
784db65375 | ||
|
|
2421be6732 | ||
|
|
d5e131623d | ||
|
|
9046a2fe72 | ||
|
|
1796733076 | ||
|
|
c958e540e9 | ||
|
|
36f272cdf0 | ||
|
|
efcdfb8eca | ||
|
|
df9d1d6e52 | ||
|
|
1094302c24 | ||
|
|
03c42d723c | ||
|
|
09e1448710 | ||
|
|
502421c94e | ||
|
|
47b762b4f1 | ||
|
|
b48221d221 | ||
|
|
6f18bf70c0 | ||
|
|
82fa62d0a5 | ||
|
|
83ac4fe29c | ||
|
|
6a14c55681 | ||
|
|
3ba669da3e | ||
|
|
7ad36f1bcc | ||
|
|
8032f1dd19 | ||
|
|
9f24a452f8 | ||
|
|
fcf47f34ab | ||
|
|
600b7c63d3 | ||
|
|
f8ffaf01b5 | ||
|
|
f594d98a15 | ||
|
|
bb8480fc3c | ||
|
|
c64417d72f | ||
|
|
eacf0a9baf | ||
|
|
0cdfc80737 | ||
|
|
b2aad3d9f8 | ||
|
|
eab32c2797 | ||
|
|
1cb62b731c | ||
|
|
d714fb203b | ||
|
|
65e6b08576 | ||
|
|
9ee4bcf564 | ||
|
|
8a8fbcf2b1 | ||
|
|
45fc548139 | ||
|
|
9b65902446 | ||
|
|
b897da7b4c | ||
|
|
d44ad8142a | ||
|
|
7f8a8c57a4 | ||
|
|
7ff272706a | ||
|
|
fa1ff52c39 | ||
|
|
55da97b9a3 | ||
|
|
b5f1864d4e | ||
|
|
8ac03ec3b7 | ||
|
|
8f43d2bd2b | ||
|
|
1f50ad0aa5 | ||
|
|
f781a02a24 | ||
|
|
91e622889d | ||
|
|
db6e80277d | ||
|
|
23a431223b | ||
|
|
85ff5da403 | ||
|
|
571c108503 | ||
|
|
92de556716 | ||
|
|
209b0b31ff | ||
|
|
274dbb6e10 | ||
|
|
e17cb856a0 | ||
|
|
32d5bf3f18 | ||
|
|
c494891344 | ||
|
|
5337ab8716 | ||
|
|
8336f33263 | ||
|
|
b08f6fa3e2 | ||
|
|
97abb54442 | ||
|
|
c4a63d3c11 | ||
|
|
07e34342da | ||
|
|
bb0362f3f6 |
@@ -0,0 +1,4 @@
|
||||
* Describe here the reasons behind the pull request.
|
||||
* Make sure you have read the CONTRIBUTING.md file.
|
||||
* Make sure any relevant documentation and tests have been added/updated.
|
||||
* Do not submit Draft pull requests unless previously asked/agreed.
|
||||
@@ -42,11 +42,11 @@ jobs:
|
||||
env:
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
@@ -123,11 +123,11 @@ jobs:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
|
||||
@@ -28,16 +28,16 @@ jobs:
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
|
||||
|
||||
@@ -74,6 +74,6 @@ unless System.get_env("DRYRUN") do
|
||||
"api-username" => "Elixir"
|
||||
}
|
||||
|
||||
resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
|
||||
end
|
||||
|
||||
@@ -89,9 +89,9 @@ runs:
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
|
||||
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0
|
||||
with:
|
||||
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
|
||||
image: ghcr.io/oss-review-toolkit/ort:92.2.0
|
||||
run: >-
|
||||
labels,
|
||||
cache-dependencies,
|
||||
@@ -107,4 +107,6 @@ runs:
|
||||
ort-cli-report-args: >-
|
||||
-O CycloneDX=output.file.formats=json,xml
|
||||
-O SpdxDocument=outputFileFormats=JSON,YAML
|
||||
ort-cli-scan-args: >-
|
||||
--scanners Provenant
|
||||
sw-version: "${{ inputs.version }}"
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
--draft \
|
||||
"$GITHUB_REF_NAME"
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# zizmor: ignore[artipacked]
|
||||
if: github.ref_type == 'branch'
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
otp_version: "29.0"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -145,14 +145,14 @@ jobs:
|
||||
|
||||
- name: Log in to Azure
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
|
||||
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: "Sign files with Trusted Signing"
|
||||
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
|
||||
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ jobs:
|
||||
name: Notify
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
with:
|
||||
otp-version: "27.3"
|
||||
elixir-version: "1.18.3"
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"**/*.md"
|
||||
],
|
||||
"ignores": [
|
||||
".git/**"
|
||||
".git/**",
|
||||
".github/**"
|
||||
],
|
||||
"gitignore": true,
|
||||
"config": {
|
||||
|
||||
@@ -37,6 +37,11 @@ curations:
|
||||
comment: "Apply Trademark Policy to VERSION file"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".github/pull_request_template.md"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to GitHub pull request template"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Wrongly Identified
|
||||
- path: ".gitignore"
|
||||
|
||||
+71
-375
@@ -4,412 +4,108 @@
|
||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
-->
|
||||
|
||||
# Changelog for Elixir v1.20
|
||||
# Changelog for Elixir v1.21
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
## Type system improvements
|
||||
|
||||
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
|
||||
|
||||
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
|
||||
|
||||
### Type inference of guards
|
||||
|
||||
This release also performs inference of guards! Let's see some examples:
|
||||
|
||||
```elixir
|
||||
def example(x, y) when is_list(x) and is_integer(y)
|
||||
```
|
||||
|
||||
The code above correctly infers `x` is a list and `y` is an integer.
|
||||
|
||||
```elixir
|
||||
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
|
||||
```
|
||||
|
||||
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
|
||||
|
||||
```elixir
|
||||
def example(x) when is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
|
||||
|
||||
```elixir
|
||||
def example(x) when not is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
|
||||
|
||||
You can also have expressions that assert on the size of data structures:
|
||||
|
||||
```elixir
|
||||
def example(x) when tuple_size(x) < 3
|
||||
```
|
||||
|
||||
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
|
||||
|
||||
### Whole-body type inference
|
||||
|
||||
Elixir also performs inference based on the function body itself. Take the following code:
|
||||
|
||||
```elixir
|
||||
def add_foo_and_bar(data) do
|
||||
data.foo + data.bar
|
||||
end
|
||||
```
|
||||
|
||||
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
|
||||
|
||||
Here is another example:
|
||||
|
||||
```elixir
|
||||
def sum_to_string(a, b) do
|
||||
Integer.to_string(a + b)
|
||||
end
|
||||
```
|
||||
|
||||
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
|
||||
|
||||
### Typing across clauses
|
||||
|
||||
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
|
||||
|
||||
```elixir
|
||||
case System.get_env("SOME_VAR") do
|
||||
nil -> :not_found
|
||||
value -> {:ok, String.upcase(value)}
|
||||
end
|
||||
```
|
||||
|
||||
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
|
||||
|
||||
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
|
||||
|
||||
### Typing of atom and domain keys in maps
|
||||
|
||||
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
|
||||
|
||||
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
|
||||
|
||||
```elixir
|
||||
%{123 => "hello", 456.0 => :ok}
|
||||
```
|
||||
|
||||
will have the type:
|
||||
|
||||
```elixir
|
||||
%{integer() => binary(), float() => :ok}
|
||||
```
|
||||
|
||||
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
|
||||
|
||||
```elixir
|
||||
%{integer() => integer(), root: integer()}
|
||||
```
|
||||
|
||||
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
|
||||
|
||||
### Typing of map operations
|
||||
|
||||
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
|
||||
|
||||
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
|
||||
|
||||
```elixir
|
||||
Map.put(map, :key, 123)
|
||||
#=> returns type %{..., key: integer()}
|
||||
|
||||
Map.delete(map, :key)
|
||||
#=> returns type %{..., key: not_set()}
|
||||
```
|
||||
|
||||
As you can see, we track when keys are set and also when they are removed.
|
||||
|
||||
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
|
||||
|
||||
```elixir
|
||||
Map.replace(map, :key, 123)
|
||||
#=> returns type %{..., key: if_set(integer())}
|
||||
```
|
||||
|
||||
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
|
||||
|
||||
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
def name(map), do: Map.fetch!(map, :name)
|
||||
end
|
||||
|
||||
defmodule CallsUser do
|
||||
def calls_name do
|
||||
User.name(%{})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The code above has a type violation, which is now caught by the type system:
|
||||
|
||||
```text
|
||||
warning: incompatible types given to User.name/1:
|
||||
|
||||
User.name(%{})
|
||||
|
||||
given types:
|
||||
|
||||
%{name: not_set()}
|
||||
|
||||
but expected one of:
|
||||
|
||||
dynamic(%{..., name: term()})
|
||||
|
||||
type warning found at:
|
||||
│
|
||||
16 │ User.name(%{})
|
||||
│ ~
|
||||
│
|
||||
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
|
||||
```
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
|
||||
|
||||
## Compile-time improvements
|
||||
|
||||
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
|
||||
|
||||
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
|
||||
|
||||
* Errors during compilation may have less precise stacktraces
|
||||
|
||||
* Anonymous functions within `defmodule` can have only up to 20 arguments.
|
||||
If this is an issue, you can use maps or tuples to group the data.
|
||||
Note the functions themselves inside `defmodule`, such as the ones defined
|
||||
inside `def` and friends, can still have up to 255 arguments
|
||||
|
||||
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
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix binary comprehensions with sizes when options such as `:uniq` or `:into` are used
|
||||
* [Kernel] Improve compiler error messages when `quote` with `unquote` is used inside a pattern or guard
|
||||
* [Kernel] Restore the compiler optimization of `Kernel.put_elem/3` to emit `:erlang.setelement/3`
|
||||
* [Module] Fix type checking when applying an empty function type
|
||||
* [Module] Fix type checking of bitstring patterns that reuse variables
|
||||
* [Module] Fix type information for `__info__(:struct)` to include the `:required` key
|
||||
* [Module] Fix type operations on map and optional keys during difference/intersection, including open keys and empty intersections
|
||||
* [Module] Fix typing of list types involving dynamic or empty lists
|
||||
* [Module] Include bitstrings as a possible domain key in the type system
|
||||
* [Module] Preserve file metadata from each clause in type system warnings
|
||||
* [Module] Fix type warnings for protocol implementations whose protocol module defines additional callbacks
|
||||
* [Module] Raise clearer type checking errors when an expected struct is removed or redefined during compilation
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Avoid unnecessary umbrella recompilation when a path dependency's manifest is newer but unchanged
|
||||
* [mix deps.compile] Recompile fetched dependencies when their compile-time environment changes
|
||||
|
||||
## v1.20.1 (2026-06-09)
|
||||
|
||||
### 1. Security
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Version] Limit integer components in Version to 14 decimal bytes, to avoid parsing too large integers from untrusted user input. We strongly advise developers parsing versions from user input to limit the data size given to the `Version` module (CVE-2026-49762, GHSA-w2h8-8x3g-278p)
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Cap width in `Calendar.strftime/2` to 1024 characters
|
||||
* [Code] Ensure `Code.require_file` releases the file if compilation fails
|
||||
* [Kernel] Fix documentation generation to use the correct version in search
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix archive.install] Validate paths and files when extracting archives
|
||||
* [mix format] Honor `--no-compile` option when loading plugins
|
||||
|
||||
## v1.20.0 (2026-06-03)
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
## v1.21.0-dev
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Optimize compiler by flattening expr list only once
|
||||
* [EEx] Support splitting middle expressions across EEx clauses
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Base] Optimize Base validation functions by using SWAR techniques
|
||||
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
|
||||
* [Code] Add `:dbg_callback` option to eval functions
|
||||
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
|
||||
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
|
||||
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
|
||||
* [Enum] Add `Enum.min_max` sorter
|
||||
* [File] Add support for `[:raw]` opts in `File.read/2`
|
||||
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
|
||||
* [Float] Optimize `Float.round/2` by avoiding big integers
|
||||
* [Inspect] Increase inspect limit to help print deeply nested data structures
|
||||
* [Inspect] Support printing Erlang records (using Erlang notation)
|
||||
* [Integer] Add `Integer.ceil_div/2`
|
||||
* [Integer] Add `Integer.popcount/1`
|
||||
* [IO] Add `IO.iodata_empty?/1`
|
||||
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
|
||||
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
|
||||
* [Kernel] Detect and warn on redundant clauses
|
||||
* [Kernel] Perform type inference across applications
|
||||
* [Kernel] Print intermediate results of `dbg` for pipes
|
||||
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
|
||||
* [Kernel] Warn on unused requires
|
||||
* [List] Add `List.first!/1` and `List.last!/1`
|
||||
* [Module] Purge and delete modules if `after_compile/2` callback fails
|
||||
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
|
||||
* [Process] Add `Process.get_label/1`
|
||||
* [Registry] Switch `keys: {:duplicate, :key}` to `ordered_set` with composite keys
|
||||
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
|
||||
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
|
||||
* Add Software Bill of Materials guide to the Documentation
|
||||
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1`
|
||||
* [Code] Add support for the `:erlc_options` compiler option
|
||||
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
|
||||
* [Kernel] Improve performance of type constructors and complex intersections
|
||||
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
|
||||
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation
|
||||
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
|
||||
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
|
||||
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
|
||||
* [Path] Add `Path.safe_join/2`
|
||||
* [Registry] Optimize exact key matching in lookups
|
||||
* [String] Optimize `String.bag_distance/2`
|
||||
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
|
||||
* [URI] Optimize percent-decoding and `URI.to_string/1`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
* [ExUnit.Assertions] Add `trace/3` helper
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Optimize autocompleting modules
|
||||
* [IEx.Helpers] Add `source/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix app.tree] Support `--output` option
|
||||
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
|
||||
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
|
||||
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
|
||||
* [mix deps] Support filtering `mix deps` output
|
||||
* [mix deps.tree] Support `--output` option
|
||||
* [mix format] Support `--no-compile` option
|
||||
* [mix help] Support printing docs for types and callbacks
|
||||
* [mix source] Add `mix source MODULE` to print or open a given module/function location
|
||||
* [mix test] Add `mix test --dry-run`
|
||||
|
||||
### 2. Potential breaking changes
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Disallow raw CR line ending in strings, comments, and after `?` for security reasons
|
||||
* [Kernel] `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
|
||||
|
||||
### 3. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
|
||||
* [File] Allowing preserving directory permissions in `File.cp_r/3`
|
||||
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
|
||||
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
|
||||
* [File] Fix `File.Stream`'s `Enumerable.count` for files without trailing newline
|
||||
* [File] Warn when defining `@type record()` for Erlang/OTP 29
|
||||
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
|
||||
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
|
||||
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
|
||||
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
|
||||
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
|
||||
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
|
||||
* [Kernel] Reject negative Duration in `to_timeout/1`
|
||||
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
|
||||
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
|
||||
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
|
||||
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
|
||||
* [String] Support empty pattern list in `String.count/2`
|
||||
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
|
||||
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y`
|
||||
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3`
|
||||
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision
|
||||
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats
|
||||
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments
|
||||
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator
|
||||
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars
|
||||
* [Date.Range] Fix slicing date ranges with stepped ranges
|
||||
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1`
|
||||
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges
|
||||
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown
|
||||
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
|
||||
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
|
||||
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
|
||||
* [Kernel] Fix inferred types for functions with non-returning clauses
|
||||
* [Kernel] Fix map field type inference in the presence of empty map types
|
||||
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
|
||||
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
|
||||
* [Kernel] Improve the error message for non-atom struct keys
|
||||
* [Kernel] Raise when `|` is used in guards
|
||||
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
|
||||
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
|
||||
* [Macro] Properly escape C1 control characters and Unicode noncharacters
|
||||
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
|
||||
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
|
||||
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
|
||||
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
|
||||
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
|
||||
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
|
||||
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas
|
||||
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Ensure pry works across remote nodes
|
||||
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Persist log level to app env in `Logger.configure/1`
|
||||
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys
|
||||
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
|
||||
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file
|
||||
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
|
||||
* [mix compile] Add a build lock around protocol consolidation in umbrellas
|
||||
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
|
||||
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
|
||||
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
|
||||
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
|
||||
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
|
||||
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
|
||||
* [mix test] Respect `--raise` when `mix test --warnings-as-errors` passes with warnings
|
||||
* [Mix] Prevent synchronization lock files from being overwritten with empty contents
|
||||
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4`
|
||||
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
|
||||
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
|
||||
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
|
||||
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
|
||||
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
|
||||
* [mix new] Avoid trailing whitespace in generated files
|
||||
|
||||
### 4. Hard deprecations
|
||||
### 3. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
|
||||
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
|
||||
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
|
||||
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead
|
||||
|
||||
#### Logger
|
||||
### 4. Soft deprecations
|
||||
|
||||
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
|
||||
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
|
||||
#### Elixir
|
||||
|
||||
#### Mix
|
||||
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1`
|
||||
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1`
|
||||
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1`
|
||||
|
||||
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
|
||||
## v1.20
|
||||
|
||||
## v1.19
|
||||
|
||||
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
|
||||
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md).
|
||||
|
||||
+25
-29
@@ -55,7 +55,7 @@ To recompile all (including Erlang modules):
|
||||
make compile
|
||||
```
|
||||
|
||||
After your changes are done, please remember to run `make format` to guarantee
|
||||
After your changes are done, run `make format` to guarantee
|
||||
all files are properly formatted, then run the full suite with
|
||||
`make test`.
|
||||
|
||||
@@ -71,44 +71,39 @@ updating an existing checkout, run `make clean compile`. You can check
|
||||
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
|
||||
More tasks can be found by reading the [Makefile](Makefile).
|
||||
|
||||
We encourage contributors to write tests that capture both existing and newly
|
||||
introduced behavior, especially for bug fixes and major changes:
|
||||
## Sending a pull request
|
||||
|
||||
* **Bug Fixes:** If you are fixing a bug, please try to include a test that
|
||||
*fails* before your change and *passes* afterward. This makes it easier to
|
||||
confirm that the fix addresses the underlying issue and helps prevent
|
||||
regressions in the future.
|
||||
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/)
|
||||
and must include tests and other relevant proof of work:
|
||||
|
||||
* **Bug Fixes:** If you are fixing a bug, include a test that *fails* before
|
||||
your change and *passes* afterward. This makes it easier to confirm that the
|
||||
fix addresses the underlying issue and helps prevent regressions in the future.
|
||||
|
||||
* **New Features or Major Changes:** If you are adding a new feature or making
|
||||
major changes to existing functionality, please add tests that cover the
|
||||
major parts of that functionality. Aim to have the best code coverage possible.
|
||||
major changes to existing functionality, please add assocaited tests. Aim to
|
||||
have the best code coverage possible.
|
||||
|
||||
With tests running and passing, you are ready to contribute to Elixir and
|
||||
[send a pull request](https://help.github.com/articles/using-pull-requests/).
|
||||
We have saved some excellent pull requests we have received in the past in
|
||||
case you are looking for some examples:
|
||||
* **Performance improvements:** For performance improvements, please include the
|
||||
benchmark script, with inputs and results, in the pull request description.
|
||||
We recommend using [benchee](https://github.com/bencheeorg/benchee).
|
||||
|
||||
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
|
||||
Here are some pull requests we have received in the past you can use as reference:
|
||||
|
||||
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
* [Implement Enum.member?](https://github.com/elixir-lang/elixir/pull/992)
|
||||
|
||||
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
|
||||
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
|
||||
## Reviewing changes
|
||||
|
||||
Once a pull request is sent, the Elixir team will review your changes.
|
||||
We outline our process below to clarify the roles of everyone involved.
|
||||
|
||||
All pull requests must be reviewed before being merged into the repository.
|
||||
If changes are necessary, the team will leave appropriate comments requesting
|
||||
changes to the code. Unfortunately, we cannot guarantee a pull request will
|
||||
be merged, even when modifications are requested, as the Elixir team will
|
||||
re-evaluate the contribution as it changes.
|
||||
|
||||
The Elixir team may optionally assign someone to review a pull request.
|
||||
If someone is assigned, they must explicitly approve the code before
|
||||
another team member can merge it.
|
||||
|
||||
When the review finishes, your pull request will be squashed and merged
|
||||
into the repository. If you have carefully organized your commits and
|
||||
believe they should be merged without squashing, please mention it in
|
||||
@@ -117,7 +112,7 @@ a comment.
|
||||
## Building documentation
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir.
|
||||
is cloned and compiled alongside Elixir.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
|
||||
@@ -135,7 +130,7 @@ make docs
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html).
|
||||
|
||||
## Licensing and Compliance Requirements
|
||||
|
||||
@@ -223,10 +218,11 @@ agents in Elixir spaces:
|
||||
use coding agents to implement it, as long as it follows the AI Contributions
|
||||
guidelines below.
|
||||
|
||||
* When automating AI usage on the Elixir codebase for performance improvements
|
||||
or security fixes, pair it with a separate set of agents whose job is to argue
|
||||
against and try to invalidate any proposed change. And treat their approval as
|
||||
advisory: a human must still validate it before opening issues or pull requests.
|
||||
* When automating AI usage on the Elixir codebase for performance improvements,
|
||||
security fixes, or correctness changes to the compiler or type system, pair it
|
||||
with a separate set of agents whose job is to argue against and try to invalidate
|
||||
any proposed change. And treat their approval as advisory: a human must still
|
||||
validate it before opening issues or pull requests.
|
||||
|
||||
If any code is written by AI, then you must follow the guidelines below.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
# CANONICAL := main/
|
||||
CANONICAL := main/
|
||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
@@ -189,7 +189,7 @@ clean_elixir:
|
||||
#==> Documentation tasks
|
||||
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<h1>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
|
||||
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
|
||||
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/public/images/logo/logo.png" width="200">
|
||||
</picture>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
|
||||
[](https://www.bestpractices.dev/projects/10187)
|
||||
|
||||
Elixir is a dynamic, functional language designed for building scalable
|
||||
Elixir is a programming language designed for building scalable
|
||||
and maintainable applications.
|
||||
|
||||
For more about Elixir, installation and documentation,
|
||||
|
||||
@@ -12,6 +12,7 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.21 | Development
|
||||
1.20 | Bug fixes and security patches
|
||||
1.19 | Security patches only
|
||||
1.18 | Security patches only
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.20.3
|
||||
ELIXIR_VERSION=1.21.0-dev
|
||||
|
||||
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.21.0-dev
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+3
-2
@@ -293,7 +293,7 @@ defmodule EEx do
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
|
||||
@spec eval_string(String.t(), Code.binding(), [compile_opt]) :: term()
|
||||
def eval_string(source, bindings \\ [], options \\ [])
|
||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||
compiled = compile_string(source, options)
|
||||
@@ -315,7 +315,7 @@ defmodule EEx do
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
|
||||
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t()
|
||||
def eval_file(filename, bindings \\ [], options \\ [])
|
||||
when is_list(bindings) and is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
@@ -344,6 +344,7 @@ defmodule EEx do
|
||||
|
||||
It returns `{:ok, [token]}` where a token is one of:
|
||||
|
||||
* `{:comment, content, %{column: column, line: line}}`
|
||||
* `{:text, content, %{column: column, line: line}}`
|
||||
* `{:expr, marker, content, %{column: column, line: line}}`
|
||||
* `{:start_expr, marker, content, %{column: column, line: line}}`
|
||||
|
||||
+47
-18
@@ -79,7 +79,7 @@ defmodule EEx.Compiler do
|
||||
{:error, message <> code_snippet(state.source, state.indentation, meta), meta}
|
||||
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
{key, expr} =
|
||||
{key, expr, extra_meta} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, _line, _column, _warnings, rev_tokens, []} ->
|
||||
# We ignore warnings because the code will be tokenized
|
||||
@@ -87,7 +87,7 @@ defmodule EEx.Compiler do
|
||||
token_key(rev_tokens, expr)
|
||||
|
||||
{:error, _, _, _, _} ->
|
||||
{:expr, expr}
|
||||
{:expr, expr, %{}}
|
||||
end
|
||||
|
||||
marker =
|
||||
@@ -102,8 +102,8 @@ defmodule EEx.Compiler do
|
||||
marker
|
||||
end
|
||||
|
||||
token = {key, marker, expr, %{line: line, column: column}}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
|
||||
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -127,6 +127,27 @@ defmodule EEx.Compiler do
|
||||
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
|
||||
end
|
||||
|
||||
# Merge middle expressions separated only by whitespace so the whitespace is
|
||||
# part of the Elixir expression, not a separate EEx body.
|
||||
defp merge_token(
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
|
||||
) do
|
||||
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
|
||||
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
|
||||
else
|
||||
[
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
{:text, text, text_meta},
|
||||
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_token(token, acc) do
|
||||
[token | acc]
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
|
||||
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
|
||||
@@ -177,35 +198,37 @@ defmodule EEx.Compiler do
|
||||
defp token_key(rev_tokens, expr) do
|
||||
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
|
||||
{_, [{:do, _} | _]} ->
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
{:start_expr, maybe_append_space(expr), %{}}
|
||||
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
{_, [{:block_identifier, _, identifier} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}}
|
||||
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
|
||||
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||
|
||||
if end_index > fn_index do
|
||||
{:start_expr, expr}
|
||||
{_, [{:stab_op, _, _} | rev_tokens]} ->
|
||||
if fn_before_end?(rev_tokens) do
|
||||
{:start_expr, expr, %{}}
|
||||
else
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
end
|
||||
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
[{:end, _} | _] -> {:end_expr, expr, %{}}
|
||||
_ -> {:expr, expr, %{}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp fn_before_end?([{:fn, _} | _]), do: true
|
||||
defp fn_before_end?([{:end, _} | _]), do: false
|
||||
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
|
||||
defp fn_before_end?([]), do: false
|
||||
|
||||
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
|
||||
defp drop_eol(rest), do: rest
|
||||
|
||||
@@ -476,6 +499,12 @@ defmodule EEx.Compiler do
|
||||
Enum.all?(chars, &(&1 in @all_spaces))
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(%{block_identifier: identifier}) do
|
||||
identifier in [:else, :rescue, :catch]
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(_meta), do: false
|
||||
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
|
||||
@@ -195,7 +195,7 @@ defmodule EEx.Engine do
|
||||
def handle_expr(state, "=", ast) do
|
||||
check_state!(state)
|
||||
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
|
||||
var = Macro.var(:"arg#{vars_count}", __MODULE__)
|
||||
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__)
|
||||
|
||||
ast =
|
||||
quote do
|
||||
|
||||
@@ -270,7 +270,7 @@ defmodule EEx.TokenizerTest do
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
|
||||
{:text, ~c"bar", %{column: 21, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}},
|
||||
{:text, ~c"baz", %{column: 34, line: 1}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
|
||||
{:eof, %{column: 46, line: 1}}
|
||||
@@ -286,7 +286,7 @@ defmodule EEx.TokenizerTest do
|
||||
exprs = [
|
||||
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
|
||||
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}},
|
||||
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
|
||||
{:eof, %{column: 3, line: 7}}
|
||||
|
||||
@@ -262,6 +262,37 @@ defmodule EExTest do
|
||||
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
||||
end
|
||||
|
||||
test "with expression with else clause split across tags" do
|
||||
template = """
|
||||
<%= with {:ok, x} <- @res do %>
|
||||
<p><%= x %></p>
|
||||
<% else %>
|
||||
<% _ -> %>
|
||||
<p>bad</p>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
|
||||
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
end
|
||||
|
||||
test "empty clauses separated by whitespace" do
|
||||
template = """
|
||||
<%= case x do %>
|
||||
<% :foo -> %>
|
||||
<% :bar -> %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n \n", template, x: :foo)
|
||||
assert_eval("\n\n", template, x: :bar)
|
||||
end
|
||||
|
||||
test "with end of token" do
|
||||
assert_eval("foo bar %>", "foo bar %>")
|
||||
end
|
||||
@@ -522,7 +553,7 @@ defmodule EExTest do
|
||||
|
||||
assert message |> Exception.message() |> strip_ansi() =~ """
|
||||
│
|
||||
514 │ true && @some[\s
|
||||
#{line + 2} │ true && @some[\s
|
||||
│ │ └ missing closing delimiter (expected "]")
|
||||
│ └ unclosed delimiter
|
||||
"""
|
||||
|
||||
+86
-30
@@ -10,8 +10,8 @@ defmodule Access do
|
||||
keys of any type in a data structure via the `data[key]` syntax.
|
||||
|
||||
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
||||
of the box. Keywords supports only atoms keys, keys for maps can
|
||||
be of any type. Both return `nil` if the key does not exist:
|
||||
of the box. Keyword lists support only atom keys, while keys for maps
|
||||
can be of any type. Both return `nil` if the key does not exist:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
@@ -33,7 +33,7 @@ defmodule Access do
|
||||
iex> keywords[:c][:unknown]
|
||||
nil
|
||||
|
||||
This works because accessing anything on a `nil` value, returns
|
||||
This works because accessing anything on a `nil` value returns
|
||||
`nil` itself:
|
||||
|
||||
iex> nil[:a]
|
||||
@@ -226,6 +226,8 @@ defmodule Access do
|
||||
end
|
||||
end
|
||||
|
||||
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
|
||||
|
||||
@doc """
|
||||
Fetches the value for the given key in a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
@@ -485,7 +487,7 @@ defmodule Access do
|
||||
## Accessors
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -514,30 +516,56 @@ defmodule Access do
|
||||
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
||||
{"john", %{user: %{}}}
|
||||
|
||||
An error is raised if the accessed structure is not a map or a struct:
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
|
||||
{"john", [user: []]}
|
||||
|
||||
iex> get_in([], [Access.key(:foo)])
|
||||
** (BadMapError) expected a map, got:
|
||||
...
|
||||
An error is raised if the accessed structure is not a map, struct, or keyword list:
|
||||
|
||||
iex> get_in(123, [Access.key(:foo)])
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
"""
|
||||
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
def key(key, default \\ nil) do
|
||||
fn
|
||||
:get, data, next ->
|
||||
:get, %{} = data, next ->
|
||||
next.(Map.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next ->
|
||||
:get_and_update, %{} = data, next ->
|
||||
value = Map.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Map.put(data, key, update)}
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -546,6 +574,19 @@ defmodule Access do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
{"john", [user: []]}
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
|
||||
** (KeyError) key :unknown not found in:
|
||||
...
|
||||
|
||||
iex> map = %{user: %{name: "john"}}
|
||||
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
@@ -574,13 +615,15 @@ defmodule Access do
|
||||
`Access.key!/1` is useful when the key is not known in advance
|
||||
and must be accessed dynamically.
|
||||
|
||||
An error is raised if the accessed structure is not a map/struct:
|
||||
An error is raised if the accessed structure is not a map/struct/keyword list:
|
||||
|
||||
iex> get_in([], [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
||||
iex> get_in(123, [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
|
||||
"""
|
||||
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
def key!(key) do
|
||||
fn
|
||||
:get, %{} = data, next ->
|
||||
@@ -594,8 +637,19 @@ defmodule Access do
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.fetch!(data, key))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.fetch!(data, key)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
|
||||
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -863,7 +917,7 @@ defmodule Access do
|
||||
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
|
||||
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
|
||||
|
||||
When no match is found, an empty list is returned and the update function is never called
|
||||
When no match is found, an empty list is returned and the update function is never called:
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
|
||||
@@ -880,7 +934,7 @@ defmodule Access do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
|
||||
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list)
|
||||
def filter(func) when is_function(func) do
|
||||
fn op, data, next -> filter(op, data, func, next) end
|
||||
end
|
||||
@@ -981,12 +1035,12 @@ defmodule Access do
|
||||
end
|
||||
|
||||
defp slice(:get_and_update, data, range, next) when is_list(data) do
|
||||
range = normalize_range(range, data)
|
||||
%Range{first: first, last: last, step: step} = normalize_range(range, data)
|
||||
|
||||
if range.first > range.last do
|
||||
if first > last do
|
||||
{[], data}
|
||||
else
|
||||
get_and_update_slice(data, range, next, [], [], 0)
|
||||
get_and_update_slice(data, first, last, step, next, [], [], 0)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1091,21 +1145,23 @@ defmodule Access do
|
||||
|
||||
defp normalize_range(range, _list), do: range
|
||||
|
||||
defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
|
||||
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index)
|
||||
when index > last do
|
||||
{:lists.reverse(gets), :lists.reverse(updates, rest)}
|
||||
end
|
||||
|
||||
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
||||
if index in range do
|
||||
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do
|
||||
if index >= first and rem(index - first, step) == 0 do
|
||||
case next.(head) do
|
||||
:pop ->
|
||||
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
|
||||
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1)
|
||||
|
||||
{get, update} ->
|
||||
get_and_update_slice(
|
||||
rest,
|
||||
range,
|
||||
first,
|
||||
last,
|
||||
step,
|
||||
next,
|
||||
[update | updates],
|
||||
[get | gets],
|
||||
@@ -1113,11 +1169,11 @@ defmodule Access do
|
||||
)
|
||||
end
|
||||
else
|
||||
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
|
||||
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
|
||||
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do
|
||||
{:lists.reverse(gets), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@@ -1144,7 +1200,7 @@ defmodule Access do
|
||||
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
|
||||
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
|
||||
|
||||
When no match is found, nil is returned and the update function is never called
|
||||
When no match is found, nil is returned and the update function is never called:
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
|
||||
|
||||
@@ -327,7 +327,7 @@ defmodule Agent do
|
||||
passing the agent state. The result of the function invocation is
|
||||
returned from this function.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
@@ -366,7 +366,7 @@ defmodule Agent do
|
||||
elements, the first being the value to return (that is, the "get" value)
|
||||
and the second one being the new state of the agent.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
@@ -407,7 +407,7 @@ defmodule Agent do
|
||||
|
||||
This function always returns `:ok`.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
|
||||
@@ -996,7 +996,7 @@ defmodule Application do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the directory for app.
|
||||
Gets the directory for `app`.
|
||||
|
||||
This information is returned based on the code path. Here is an
|
||||
example:
|
||||
@@ -1077,7 +1077,7 @@ defmodule Application do
|
||||
@doc """
|
||||
Formats the error reason returned by `start/2`,
|
||||
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
|
||||
returns a string.
|
||||
and returns a string.
|
||||
"""
|
||||
@spec format_error(any) :: String.t()
|
||||
def format_error(reason) do
|
||||
|
||||
+36
-36
@@ -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
|
||||
@@ -436,7 +436,7 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do
|
||||
name = :"encode16#{base}"
|
||||
name = String.to_unsafe_atom("encode16#{base}")
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -536,7 +536,7 @@ defmodule Base do
|
||||
* `:lower` - only allows lower case characters
|
||||
* `:mixed` - allows mixed case characters
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
An `ArgumentError` exception is raised if the string has an odd length or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
@@ -617,10 +617,10 @@ defmodule Base do
|
||||
upper = Enum.with_index(b16_alphabet)
|
||||
|
||||
for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do
|
||||
decode_name = :"decode16#{base}!"
|
||||
validate_name = :"validate16#{base}?"
|
||||
valid_char_name = :"valid_char16#{base}?"
|
||||
valid_word_name = :"valid_word16#{base}?"
|
||||
decode_name = String.to_unsafe_atom("decode16#{base}!")
|
||||
validate_name = String.to_unsafe_atom("validate16#{base}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char16#{base}?")
|
||||
valid_word_name = String.to_unsafe_atom("valid_word16#{base}?")
|
||||
|
||||
{min, decoded} = to_decode_list.(alphabet)
|
||||
|
||||
@@ -735,7 +735,7 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
|
||||
name = :"encode64#{base}"
|
||||
name = String.to_unsafe_atom("encode64#{base}")
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -1001,12 +1001,12 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
|
||||
decode_name = :"decode64#{base}!"
|
||||
decode_name = String.to_unsafe_atom("decode64#{base}!")
|
||||
|
||||
validate_name = :"validate64#{base}?"
|
||||
validate_main_name = :"validate_main64#{validate_name}?"
|
||||
valid_char_name = :"valid_char64#{base}?"
|
||||
valid_word_name = :"valid_word64#{base}?"
|
||||
validate_name = String.to_unsafe_atom("validate64#{base}?")
|
||||
validate_main_name = String.to_unsafe_atom("validate_main64#{validate_name}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char64#{base}?")
|
||||
valid_word_name = String.to_unsafe_atom("valid_word64#{base}?")
|
||||
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?`
|
||||
@@ -1314,7 +1314,7 @@ defmodule Base do
|
||||
hexupper: b32hex_alphabet,
|
||||
hexlower: to_lower_enc.(b32hex_alphabet)
|
||||
] do
|
||||
name = :"encode32#{base}"
|
||||
name = String.to_unsafe_atom("encode32#{base}")
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -1388,7 +1388,7 @@ defmodule Base do
|
||||
|
||||
The values for `:case` can be:
|
||||
|
||||
* `:upper` - only allows upper case characters (default)
|
||||
* `:upper` - only allows upper case characters (default)
|
||||
* `:lower` - only allows lower case characters
|
||||
* `:mixed` - allows mixed case characters
|
||||
|
||||
@@ -1653,16 +1653,16 @@ defmodule Base do
|
||||
hexlower: to_lower_dec.(hexupper),
|
||||
hexmixed: to_mixed_dec.(hexupper)
|
||||
] do
|
||||
decode_name = :"decode32#{base}!"
|
||||
validate_name = :"validate32#{base}?"
|
||||
validate_main_name = :"validate_main32#{validate_name}?"
|
||||
valid_char_name = :"valid_char32#{base}?"
|
||||
decode_name = String.to_unsafe_atom("decode32#{base}!")
|
||||
validate_name = String.to_unsafe_atom("validate32#{base}?")
|
||||
validate_main_name = String.to_unsafe_atom("validate_main32#{validate_name}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char32#{base}?")
|
||||
{min, decoded} = to_decode_list.(alphabet)
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?`
|
||||
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
|
||||
# 8-byte-multiple `main`) recurses through the single-byte clause.
|
||||
valid_word_name = :"valid_word32#{base}?"
|
||||
valid_word_name = String.to_unsafe_atom("valid_word32#{base}?")
|
||||
|
||||
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
|
||||
|
||||
+47
-27
@@ -106,6 +106,7 @@ defmodule Calendar do
|
||||
@typedoc "Any map or struct that contains the time fields."
|
||||
@type time :: %{
|
||||
optional(any) => any,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
@@ -295,7 +296,7 @@ defmodule Calendar do
|
||||
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Define the rollover moment for the calendar.
|
||||
Defines the rollover moment for the calendar.
|
||||
|
||||
This is the moment, in your calendar, when the current day ends
|
||||
and the next day starts.
|
||||
@@ -381,13 +382,13 @@ defmodule Calendar do
|
||||
@callback iso_days_to_end_of_day(iso_days) :: iso_days
|
||||
|
||||
@doc """
|
||||
Shifts date by given duration according to its calendar.
|
||||
Shifts date by the given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
|
||||
|
||||
@doc """
|
||||
Shifts naive datetime by given duration according to its calendar.
|
||||
Shifts naive datetime by the given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_naive_datetime(
|
||||
@@ -402,7 +403,7 @@ defmodule Calendar do
|
||||
) :: {year, month, day, hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Shifts time by given duration according to its calendar.
|
||||
Shifts time by the given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
|
||||
@@ -562,11 +563,11 @@ defmodule Calendar do
|
||||
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
||||
q | Quarter | 1, 2, 3, 4
|
||||
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
|
||||
S | Second | 00, 59, 60
|
||||
S | Second | 00, 59
|
||||
u | Day of the week | 1 (Monday), 7 (Sunday)
|
||||
x | Preferred date (without time) representation | 2018-10-17
|
||||
X | Preferred time (without date) representation | 12:34:56
|
||||
y | Year as 2-digits | 01, 01, 86, 18
|
||||
y | Year as 2-digits | -01, 01, 86, 18
|
||||
Y | Year | -0001, 0001, 1986
|
||||
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
||||
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
||||
@@ -623,7 +624,7 @@ defmodule Calendar do
|
||||
...>)
|
||||
"серпень"
|
||||
|
||||
Microsecond formatting:
|
||||
Microsecond formatting:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
|
||||
"19-08-26 13:52:06.0"
|
||||
@@ -711,7 +712,7 @@ defmodule Calendar do
|
||||
|
||||
# Literally just %
|
||||
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
|
||||
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc])
|
||||
end
|
||||
|
||||
# Abbreviated name of day
|
||||
@@ -780,7 +781,7 @@ defmodule Calendar do
|
||||
|
||||
# Day of the month
|
||||
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -799,31 +800,35 @@ defmodule Calendar do
|
||||
|
||||
# Hour using a 24-hour clock
|
||||
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 12-hour clock
|
||||
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the year
|
||||
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Month
|
||||
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Minute
|
||||
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -851,19 +856,23 @@ defmodule Calendar do
|
||||
|
||||
# Quarter
|
||||
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Second
|
||||
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the week
|
||||
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -913,20 +922,25 @@ defmodule Calendar do
|
||||
|
||||
# Year as 2-digits
|
||||
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
if datetime.year < 0 do
|
||||
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
||||
else
|
||||
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
end
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year
|
||||
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
{sign, year} =
|
||||
result =
|
||||
if datetime.year < 0 do
|
||||
{?-, -datetime.year}
|
||||
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
||||
else
|
||||
{[], datetime.year}
|
||||
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
end
|
||||
|
||||
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -973,7 +987,7 @@ defmodule Calendar do
|
||||
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
||||
|
||||
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
||||
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
|
||||
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}"
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -992,13 +1006,19 @@ defmodule Calendar do
|
||||
raise ArgumentError, "invalid strftime format: %#{next}"
|
||||
end
|
||||
|
||||
defp pad_preferred(result, width, pad) when length(result) < width do
|
||||
pad_preferred([pad | result], width, pad)
|
||||
defp pad_preferred(result, width, pad) do
|
||||
result
|
||||
|> IO.iodata_to_binary()
|
||||
|> pad_leading(width, pad)
|
||||
end
|
||||
|
||||
defp pad_preferred(result, _width, _pad), do: result
|
||||
|
||||
defp pad_leading(string, count, padding) do
|
||||
to_pad = count - String.length(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
|
||||
defp pad_leading_ascii(string, count, padding) do
|
||||
to_pad = count - byte_size(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
@@ -81,7 +81,7 @@ defmodule Date do
|
||||
|
||||
Ranges of dates can be increasing (`first <= last`) and are
|
||||
always inclusive. For a decreasing range, use `range/3` with
|
||||
a step of -1 as first argument.
|
||||
a step of -1 as third argument.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -160,7 +160,7 @@ defmodule Date do
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"both dates must have matching calendar and the step must be a " <>
|
||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
|
||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}"
|
||||
end
|
||||
|
||||
defp range(first, first_days, last, last_days, calendar, step) do
|
||||
@@ -422,7 +422,7 @@ defmodule Date do
|
||||
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
|
||||
date
|
||||
|> convert!(Calendar.ISO)
|
||||
|> to_iso8601()
|
||||
|> to_iso8601(format)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -556,14 +556,18 @@ defmodule Date do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
|
||||
def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
|
||||
%{year: year1, month: month1, day: day1} = date1
|
||||
%{year: year2, month: month2, day: day2} = date2
|
||||
|
||||
case {{year1, month1, day1}, {year2, month2, day2}} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
def compare(
|
||||
%{year: year1, month: month1, day: day1, calendar: calendar},
|
||||
%{year: year2, month: month2, day: day2, calendar: calendar}
|
||||
) do
|
||||
cond do
|
||||
year1 > year2 -> :gt
|
||||
year1 < year2 -> :lt
|
||||
month1 > month2 -> :gt
|
||||
month1 < month2 -> :lt
|
||||
day1 > day2 -> :gt
|
||||
day1 < day2 -> :lt
|
||||
true -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1047,7 +1051,7 @@ defmodule Date do
|
||||
@doc """
|
||||
Calculates the quarter of the year of a given `date`.
|
||||
|
||||
Returns the day of the year as an integer. For the ISO 8601
|
||||
Returns the quarter of the year as an integer. For the ISO 8601
|
||||
calendar (the default), it is an integer from 1 to 4.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -37,21 +37,19 @@ defmodule Date.Range do
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
} = range,
|
||||
},
|
||||
%Date{calendar: calendar} = date
|
||||
) do
|
||||
{days, _} = Date.to_iso_days(date)
|
||||
|
||||
cond do
|
||||
empty?(range) ->
|
||||
{:ok, false}
|
||||
in_range? =
|
||||
if step > 0 do
|
||||
first_days <= days and days <= last_days and rem(days - first_days, step) == 0
|
||||
else
|
||||
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
|
||||
end
|
||||
|
||||
first_days <= last_days ->
|
||||
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
|
||||
|
||||
true ->
|
||||
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
|
||||
end
|
||||
{:ok, in_range?}
|
||||
end
|
||||
|
||||
def member?(%Date.Range{step: _}, _) do
|
||||
@@ -88,7 +86,7 @@ defmodule Date.Range do
|
||||
step: step
|
||||
} = range
|
||||
) do
|
||||
{:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
|
||||
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
@@ -185,7 +183,7 @@ defmodule Date.Range do
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
}),
|
||||
do: abs(div(last_days - first_days, step)) + 1
|
||||
do: div(last_days - first_days, step) + 1
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp size(
|
||||
@@ -195,33 +193,6 @@ defmodule Date.Range do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
size(Map.put(date_range, :step, step))
|
||||
end
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step > 0 and first_days > last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step < 0 and first_days < last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{step: _}), do: false
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp empty?(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
empty?(Map.put(date_range, :step, step))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
|
||||
@@ -180,7 +180,7 @@ defmodule DateTime do
|
||||
since v1.15.0.
|
||||
|
||||
The default unit if none gets passed is `:native`,
|
||||
which results on a default resolution of microseconds.
|
||||
which results in a default resolution of microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -720,6 +720,9 @@ defmodule DateTime do
|
||||
Other time zone databases can be passed as argument or set globally.
|
||||
See the "Time zone database" section in the module docs.
|
||||
|
||||
Shifting to the `"Etc/UTC"` time zone always succeeds without
|
||||
consulting the `time_zone_database`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
|
||||
@@ -753,6 +756,28 @@ defmodule DateTime do
|
||||
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
|
||||
end
|
||||
|
||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
|
||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
||||
calendar.naive_datetime_from_iso_days(iso_days_utc)
|
||||
|
||||
datetime = %DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision},
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
{:ok, datetime}
|
||||
end
|
||||
|
||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
|
||||
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
|
||||
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
|
||||
@@ -1177,7 +1202,7 @@ defmodule DateTime do
|
||||
datetime
|
||||
|> to_iso_days()
|
||||
# Subtract total original offset in order to get UTC and add the new offset
|
||||
|> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|
||||
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second)
|
||||
|> calendar.naive_datetime_from_iso_days()
|
||||
end
|
||||
|
||||
@@ -1296,9 +1321,9 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian seconds to a `DateTime` struct.
|
||||
Converts a number of Gregorian seconds to a `DateTime` struct.
|
||||
|
||||
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
|
||||
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use
|
||||
`DateTime.shift_zone/3`.
|
||||
|
||||
## Examples
|
||||
@@ -1341,7 +1366,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
|
||||
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1576,15 +1601,15 @@ defmodule DateTime do
|
||||
def diff(datetime1, datetime2, unit \\ :second)
|
||||
|
||||
def diff(datetime1, datetime2, :day) do
|
||||
diff(datetime1, datetime2, :second) |> div(86400)
|
||||
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000)
|
||||
end
|
||||
|
||||
def diff(datetime1, datetime2, :hour) do
|
||||
diff(datetime1, datetime2, :second) |> div(3600)
|
||||
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000)
|
||||
end
|
||||
|
||||
def diff(datetime1, datetime2, :minute) do
|
||||
diff(datetime1, datetime2, :second) |> div(60)
|
||||
diff(datetime1, datetime2, :microsecond) |> div(60_000_000)
|
||||
end
|
||||
|
||||
def diff(
|
||||
@@ -1626,6 +1651,9 @@ defmodule DateTime do
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
||||
always succeeds without consulting the `time_zone_database`.
|
||||
|
||||
This function relies on a contiguous representation of time,
|
||||
ignoring timezone changes. For example, if you add one day when there
|
||||
are summer time/daylight saving time changes, it will also change the
|
||||
@@ -1744,6 +1772,9 @@ defmodule DateTime do
|
||||
|
||||
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
|
||||
|
||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
||||
always succeeds without consulting the `time_zone_database`.
|
||||
|
||||
This operation is equivalent to shifting the datetime wall clock
|
||||
(in other words, the value as someone in that timezone would see
|
||||
on their watch), then applying the time zone offset to convert it
|
||||
@@ -1812,44 +1843,6 @@ defmodule DateTime do
|
||||
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
|
||||
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
|
||||
|
||||
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
} = datetime
|
||||
|
||||
{year, month, day, hour, minute, second, microsecond} =
|
||||
calendar.shift_naive_datetime(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
__duration__!(duration)
|
||||
)
|
||||
|
||||
%DateTime{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
time_zone: "Etc/UTC",
|
||||
zone_abbr: "UTC",
|
||||
std_offset: 0,
|
||||
utc_offset: 0
|
||||
}
|
||||
end
|
||||
|
||||
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
|
||||
%{
|
||||
year: year,
|
||||
@@ -2060,11 +2053,12 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
defp apply_tz_offset(iso_days, offset) do
|
||||
Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
|
||||
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second)
|
||||
end
|
||||
|
||||
defp from_map(%{} = datetime_map) do
|
||||
%DateTime{
|
||||
calendar: datetime_map.calendar,
|
||||
year: datetime_map.year,
|
||||
month: datetime_map.month,
|
||||
day: datetime_map.day,
|
||||
|
||||
@@ -72,7 +72,7 @@ defmodule Duration do
|
||||
|
||||
However, once again, it is important to remember that shifting a duration is not
|
||||
arithmetic, so you may want to use the functions in this module depending on what
|
||||
you to achieve. Compare the results of both examples below:
|
||||
you want to achieve. Compare the results of both examples below:
|
||||
|
||||
# Adding one month after the other
|
||||
iex> date = ~D[2016-01-31]
|
||||
@@ -129,6 +129,16 @@ defmodule Duration do
|
||||
second: 0,
|
||||
microsecond: {0, 0}
|
||||
|
||||
@typedoc """
|
||||
The microsecond component of a duration.
|
||||
|
||||
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
|
||||
durations may represent negative amounts of time. The precision is
|
||||
an integer from 0 to 6 holding the number of significant digits,
|
||||
as in the calendar types.
|
||||
"""
|
||||
@type microsecond :: {value :: integer, precision :: 0..6}
|
||||
|
||||
@typedoc """
|
||||
The duration struct type.
|
||||
"""
|
||||
@@ -140,7 +150,7 @@ defmodule Duration do
|
||||
hour: integer,
|
||||
minute: integer,
|
||||
second: integer,
|
||||
microsecond: Calendar.microsecond()
|
||||
microsecond: microsecond()
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
@@ -154,7 +164,7 @@ defmodule Duration do
|
||||
| {:hour, integer}
|
||||
| {:minute, integer}
|
||||
| {:second, integer}
|
||||
| {:microsecond, Calendar.microsecond()}
|
||||
| {:microsecond, microsecond()}
|
||||
|
||||
@typedoc """
|
||||
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
|
||||
@@ -243,19 +253,19 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec add(t, t) :: t
|
||||
def add(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
|
||||
%Duration{
|
||||
year: d1.year + d2.year,
|
||||
month: d1.month + d2.month,
|
||||
week: d1.week + d2.week,
|
||||
day: d1.day + d2.day,
|
||||
hour: d1.hour + d2.hour,
|
||||
minute: d1.minute + d2.minute,
|
||||
second: d1.second + d2.second,
|
||||
microsecond: {m1 + m2, max(p1, p2)}
|
||||
year: y1 + y2,
|
||||
month: mo1 + mo2,
|
||||
week: w1 + w2,
|
||||
day: day1 + day2,
|
||||
hour: h1 + h2,
|
||||
minute: mi1 + mi2,
|
||||
second: s1 + s2,
|
||||
microsecond: {ms1 + ms2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -273,19 +283,19 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec subtract(t, t) :: t
|
||||
def subtract(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
|
||||
%Duration{
|
||||
year: d1.year - d2.year,
|
||||
month: d1.month - d2.month,
|
||||
week: d1.week - d2.week,
|
||||
day: d1.day - d2.day,
|
||||
hour: d1.hour - d2.hour,
|
||||
minute: d1.minute - d2.minute,
|
||||
second: d1.second - d2.second,
|
||||
microsecond: {m1 - m2, max(p1, p2)}
|
||||
year: y1 - y2,
|
||||
month: mo1 - mo2,
|
||||
week: w1 - w2,
|
||||
day: day1 - day2,
|
||||
hour: h1 - h2,
|
||||
minute: mi1 - mi2,
|
||||
second: s1 - s2,
|
||||
microsecond: {ms1 - ms2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -302,14 +312,16 @@ defmodule Duration do
|
||||
"""
|
||||
@spec multiply(t, integer) :: t
|
||||
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: duration.year * integer,
|
||||
month: duration.month * integer,
|
||||
week: duration.week * integer,
|
||||
day: duration.day * integer,
|
||||
hour: duration.hour * integer,
|
||||
minute: duration.minute * integer,
|
||||
second: duration.second * integer,
|
||||
year: y * integer,
|
||||
month: mo * integer,
|
||||
week: w * integer,
|
||||
day: d * integer,
|
||||
hour: h * integer,
|
||||
minute: mi * integer,
|
||||
second: s * integer,
|
||||
microsecond: {ms * integer, p}
|
||||
}
|
||||
end
|
||||
@@ -327,14 +339,16 @@ defmodule Duration do
|
||||
"""
|
||||
@spec negate(t) :: t
|
||||
def negate(%Duration{microsecond: {ms, p}} = duration) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: -duration.year,
|
||||
month: -duration.month,
|
||||
week: -duration.week,
|
||||
day: -duration.day,
|
||||
hour: -duration.hour,
|
||||
minute: -duration.minute,
|
||||
second: -duration.second,
|
||||
year: -y,
|
||||
month: -mo,
|
||||
week: -w,
|
||||
day: -d,
|
||||
hour: -h,
|
||||
minute: -mi,
|
||||
second: -s,
|
||||
microsecond: {-ms, p}
|
||||
}
|
||||
end
|
||||
|
||||
+133
-97
@@ -33,8 +33,9 @@ defmodule Calendar.ISO do
|
||||
you to format datetimes however else you desire.
|
||||
|
||||
Elixir does not support reduced accuracy formats (for example, a date without
|
||||
the day component) nor decimal precisions in the lowest component (such as
|
||||
`10:01:25,5`).
|
||||
the day component) nor decimal precisions in components other than seconds
|
||||
(such as `10:01,5`). Fractional seconds are supported and truncated to
|
||||
microsecond precision.
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -81,6 +82,11 @@ defmodule Calendar.ISO do
|
||||
iex> Calendar.ISO.parse_time("23")
|
||||
{:error, :invalid_format}
|
||||
|
||||
A decimal fraction is accepted on seconds, but not on any other component:
|
||||
|
||||
iex> Calendar.ISO.parse_time("10:01:25,5")
|
||||
{:ok, {10, 1, 25, {500000, 1}}}
|
||||
|
||||
### Extensions
|
||||
|
||||
The parser and formatter adopt one ISO 8601 extension: extended year notation.
|
||||
@@ -417,16 +423,13 @@ defmodule Calendar.ISO do
|
||||
@spec parse_date(String.t(), format) ::
|
||||
{:ok, {year, month, day}}
|
||||
| {:error, atom}
|
||||
def parse_date(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_date_guarded(string, format)
|
||||
|
||||
defp parse_date_guarded("-" <> string, format),
|
||||
def parse_date("-" <> string, format) when is_format(format),
|
||||
do: do_parse_date(string, -1, format)
|
||||
|
||||
defp parse_date_guarded("+" <> string, format),
|
||||
def parse_date("+" <> string, format) when is_format(format),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
defp parse_date_guarded(string, format),
|
||||
def parse_date(string, format) when is_binary(string) and is_format(format),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do
|
||||
@@ -502,16 +505,13 @@ defmodule Calendar.ISO do
|
||||
@spec parse_naive_datetime(String.t(), format) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}}
|
||||
| {:error, atom}
|
||||
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_naive_datetime_guarded(string, format)
|
||||
|
||||
defp parse_naive_datetime_guarded("-" <> string, format),
|
||||
def parse_naive_datetime("-" <> string, format) when is_format(format),
|
||||
do: do_parse_naive_datetime(string, -1, format)
|
||||
|
||||
defp parse_naive_datetime_guarded("+" <> string, format),
|
||||
def parse_naive_datetime("+" <> string, format) when is_format(format),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
defp parse_naive_datetime_guarded(string, format),
|
||||
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_naive_datetime(
|
||||
@@ -606,16 +606,13 @@ defmodule Calendar.ISO do
|
||||
@spec parse_utc_datetime(String.t(), format) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
|
||||
| {:error, atom}
|
||||
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_utc_datetime_guarded(string, format)
|
||||
|
||||
defp parse_utc_datetime_guarded("-" <> string, format),
|
||||
def parse_utc_datetime("-" <> string, format) when is_format(format),
|
||||
do: do_parse_utc_datetime(string, -1, format)
|
||||
|
||||
defp parse_utc_datetime_guarded("+" <> string, format),
|
||||
def parse_utc_datetime("+" <> string, format) when is_format(format),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
defp parse_utc_datetime_guarded(string, format),
|
||||
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_utc_datetime(
|
||||
@@ -666,7 +663,7 @@ defmodule Calendar.ISO do
|
||||
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
|
||||
|
||||
{{year, month, day}, {hour, minute, second, _}} =
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
|
||||
case add_time_unit_to_iso_days({0, day_fraction}, -offset, :second) do
|
||||
{0, day_fraction} ->
|
||||
{{year, month, day}, time_from_day_fraction(day_fraction)}
|
||||
|
||||
@@ -683,75 +680,73 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
|
||||
Parses an ISO 8601 formatted duration string to a list of `Duration` compatible unit pairs.
|
||||
|
||||
See `Duration.from_iso8601/1`.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
|
||||
def parse_duration("P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
parse_duration_date(string, 1, [], 0)
|
||||
end
|
||||
|
||||
def parse_duration("+P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
parse_duration_date(string, 1, [], 0)
|
||||
end
|
||||
|
||||
def parse_duration("-P" <> string) when byte_size(string) > 0 do
|
||||
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
|
||||
{:ok,
|
||||
Enum.map(fields, fn
|
||||
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
|
||||
{unit, value} -> {unit, -value}
|
||||
end)}
|
||||
end
|
||||
parse_duration_date(string, -1, [], 0)
|
||||
end
|
||||
|
||||
def parse_duration(_) do
|
||||
{:error, :invalid_duration}
|
||||
end
|
||||
|
||||
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
|
||||
defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
|
||||
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
|
||||
defp parse_duration_date("T" <> string, sign, acc, _min_position)
|
||||
when byte_size(string) > 0 do
|
||||
parse_duration_time(string, sign, acc, 0)
|
||||
end
|
||||
|
||||
defp parse_duration_date(string, acc, allowed) do
|
||||
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
|
||||
{key, allowed} <- find_unit(allowed, next) do
|
||||
parse_duration_date(rest, [{key, integer} | acc], allowed)
|
||||
defp parse_duration_date(string, sign, acc, min_position) do
|
||||
with {integer, <<unit, rest::binary>>} <- Integer.parse(string),
|
||||
{key, next_min_position} <- find_date_unit(min_position, unit) do
|
||||
parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position)
|
||||
else
|
||||
_ -> {:error, :invalid_date_component}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
|
||||
defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_time(string, acc, allowed) do
|
||||
defp parse_duration_time(string, sign, acc, min_position) do
|
||||
case Integer.parse(string) do
|
||||
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
|
||||
case parse_microsecond(rest) do
|
||||
{{ms, precision}, "S"} ->
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
with {:second, _next_min_position} <- find_time_unit(min_position, ?S),
|
||||
{{ms, precision}, "S"} <- parse_microsecond(rest) do
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
|
||||
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
|
||||
{:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc}
|
||||
else
|
||||
_ ->
|
||||
{:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
{integer, <<next, rest::binary>>} ->
|
||||
case find_unit(allowed, next) do
|
||||
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
|
||||
false -> {:error, :invalid_time_component}
|
||||
{integer, <<unit, rest::binary>>} ->
|
||||
case find_time_unit(min_position, unit) do
|
||||
{key, next_min_position} ->
|
||||
parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position)
|
||||
|
||||
false ->
|
||||
{:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -759,9 +754,17 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
|
||||
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
|
||||
defp find_unit([], _unit), do: false
|
||||
# The minimum position is the earliest unit still allowed, or one past the end.
|
||||
defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1}
|
||||
defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2}
|
||||
defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3}
|
||||
defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4}
|
||||
defp find_date_unit(_min_position, _unit), do: false
|
||||
|
||||
defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1}
|
||||
defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2}
|
||||
defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3}
|
||||
defp find_time_unit(_min_position, _unit), do: false
|
||||
|
||||
@doc """
|
||||
Returns the `t:Calendar.iso_days/0` format of the specified date.
|
||||
@@ -1134,6 +1137,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
It is an integer from 1 to 4.
|
||||
|
||||
In the ISO calendar, the quarter is determined solely by the month, so the `year`
|
||||
and `day` arguments are ignored. Combination of `year`, `month`, and `day` is not
|
||||
validated as a valid date, unlike in `day_of_year/3`. Use `valid_date?/3` when
|
||||
full date validation is required.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.quarter_of_year(2016, 1, 31)
|
||||
@@ -1176,7 +1184,7 @@ defmodule Calendar.ISO do
|
||||
@doc since: "1.8.0"
|
||||
@spec year_of_era(year) :: {1..10_000, era}
|
||||
def year_of_era(year) when is_year_CE(year), do: {year, 1}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {1 - year, 0}
|
||||
|
||||
@doc """
|
||||
Calendar callback to compute the year and era from the
|
||||
@@ -1229,7 +1237,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
def day_of_era(year, month, day) when is_year_BCE(year) do
|
||||
day = abs(date_to_iso_days(year, month, day) - @iso_epoch)
|
||||
day = @iso_epoch - date_to_iso_days(year, month, day)
|
||||
{day, 0}
|
||||
end
|
||||
|
||||
@@ -1456,7 +1464,7 @@ defmodule Calendar.ISO do
|
||||
@doc """
|
||||
Converts the given naive_datetime into a iodata.
|
||||
|
||||
See `naive_datetime_to_iodata/8` for more information.
|
||||
See `naive_datetime_to_string/8` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1581,7 +1589,7 @@ defmodule Calendar.ISO do
|
||||
@doc """
|
||||
Converts the given datetime into a iodata.
|
||||
|
||||
See `datetime_to_iodata/12` for more information.
|
||||
See `datetime_to_string/12` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1685,7 +1693,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines if the date given is valid according to the proleptic Gregorian calendar.
|
||||
Determines if the time given is valid.
|
||||
|
||||
Leap seconds are not supported by the built-in Calendar.ISO.
|
||||
|
||||
@@ -1705,7 +1713,7 @@ defmodule Calendar.ISO do
|
||||
boolean
|
||||
def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond)
|
||||
when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and
|
||||
is_integer(ms_value) do
|
||||
is_integer(ms_precision) do
|
||||
is_hour(hour) and is_minute(minute) and is_second(second) and
|
||||
is_microsecond(ms_value, ms_precision)
|
||||
end
|
||||
@@ -1838,6 +1846,9 @@ defmodule Calendar.ISO do
|
||||
shift_options = shift_datetime_options(duration)
|
||||
|
||||
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
|
||||
{:microsecond, {0, _}}, naive_datetime ->
|
||||
naive_datetime
|
||||
|
||||
{_, 0}, naive_datetime ->
|
||||
naive_datetime
|
||||
|
||||
@@ -1867,6 +1878,9 @@ defmodule Calendar.ISO do
|
||||
shift_options = shift_time_options(duration)
|
||||
|
||||
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
|
||||
{:microsecond, {0, _}}, time ->
|
||||
time
|
||||
|
||||
{_, 0}, time ->
|
||||
time
|
||||
|
||||
@@ -1929,8 +1943,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
|
||||
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
|
||||
ppd = System.convert_time_unit(86_400, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, value, ppd)
|
||||
add_time_unit_to_iso_days(iso_days, value, unit)
|
||||
end
|
||||
|
||||
defp shift_time_unit_values({0, _}, {_, original_precision}) do
|
||||
@@ -2011,8 +2024,8 @@ defmodule Calendar.ISO do
|
||||
total = System.convert_time_unit(integer, unit, :microsecond)
|
||||
|
||||
if total in @unix_range_microseconds do
|
||||
microseconds = Integer.mod(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
|
||||
{seconds, microseconds} = div_rem(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + seconds
|
||||
precision = precision_for_unit(unit)
|
||||
{date, time} = iso_seconds_to_datetime(seconds)
|
||||
{:ok, date, time, {microseconds, precision}}
|
||||
@@ -2033,38 +2046,27 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_microsecond("." <> rest) do
|
||||
case parse_microsecond(rest, 0, []) do
|
||||
{[], 0, _} ->
|
||||
:error
|
||||
defp parse_microsecond("." <> rest), do: parse_microsecond(rest, rest, 0)
|
||||
defp parse_microsecond("," <> rest), do: parse_microsecond(rest, rest, 0)
|
||||
defp parse_microsecond(rest), do: {{0, 0}, rest}
|
||||
|
||||
{microsecond, precision, rest} ->
|
||||
scale = scale_factor(precision)
|
||||
{{:erlang.list_to_integer(microsecond) * scale, precision}, rest}
|
||||
end
|
||||
end
|
||||
# Digits past the sixth are consumed but do not contribute to the value.
|
||||
defp parse_microsecond(<<head, tail::binary>>, digits, 6) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, digits, 6)
|
||||
|
||||
defp parse_microsecond("," <> rest) do
|
||||
parse_microsecond("." <> rest)
|
||||
end
|
||||
defp parse_microsecond(<<head, tail::binary>>, digits, precision) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, digits, precision + 1)
|
||||
|
||||
defp parse_microsecond(rest) do
|
||||
{{0, 0}, rest}
|
||||
end
|
||||
defp parse_microsecond(_rest, _digits, 0), do: :error
|
||||
|
||||
defp parse_microsecond(<<head, tail::binary>>, 6, acc) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, 6, acc)
|
||||
|
||||
defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, precision + 1, [head | acc])
|
||||
|
||||
defp parse_microsecond(rest, precision, acc) do
|
||||
{:lists.reverse(acc), precision, rest}
|
||||
defp parse_microsecond(rest, digits, precision) do
|
||||
scale = scale_factor(precision)
|
||||
microsecond = :erlang.binary_to_integer(:binary.part(digits, 0, precision)) * scale
|
||||
{{microsecond, precision}, rest}
|
||||
end
|
||||
|
||||
defp parse_offset(""), do: {nil, ""}
|
||||
defp parse_offset("Z"), do: {0, ""}
|
||||
defp parse_offset("-00:00"), do: :error
|
||||
|
||||
defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>),
|
||||
do: parse_offset(1, h1, h2, m1, m2, rest)
|
||||
@@ -2087,7 +2089,8 @@ defmodule Calendar.ISO do
|
||||
true <- m1 in ?0..?5 and m2 in ?0..?9,
|
||||
hour = (h1 - ?0) * 10 + h2 - ?0,
|
||||
min = (m1 - ?0) * 10 + m2 - ?0,
|
||||
true <- hour < 24 do
|
||||
true <- hour < 24,
|
||||
true <- sign == 1 or hour != 0 or min != 0 do
|
||||
{(hour * 60 + min) * 60 * sign, rest}
|
||||
else
|
||||
_ -> :error
|
||||
@@ -2103,10 +2106,43 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def iso_days_to_unit({days, {parts, ppd}}, unit) do
|
||||
day_microseconds = days * @parts_per_day
|
||||
microseconds = divide_by_parts_per_day(parts, ppd)
|
||||
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
|
||||
def iso_days_to_unit(iso_days, :second) do
|
||||
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), @microseconds_per_second)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, :millisecond) do
|
||||
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), 1_000)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, :microsecond) do
|
||||
iso_days_to_microseconds(iso_days)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, unit) do
|
||||
System.convert_time_unit(iso_days_to_microseconds(iso_days), :microsecond, unit)
|
||||
end
|
||||
|
||||
defp iso_days_to_microseconds({days, {parts, ppd}}) do
|
||||
days * @parts_per_day + divide_by_parts_per_day(parts, ppd)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_time_unit_to_iso_days(iso_days, add, :second) do
|
||||
add_day_fraction_to_iso_days(iso_days, add * @microseconds_per_second, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, :millisecond) do
|
||||
add_day_fraction_to_iso_days(iso_days, add * 1_000, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, :microsecond) do
|
||||
add_day_fraction_to_iso_days(iso_days, add, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, unit)
|
||||
when unit == :nanosecond or is_integer(unit) do
|
||||
ppd = System.convert_time_unit(@seconds_per_day, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, add, ppd)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -164,7 +164,7 @@ defmodule NaiveDateTime do
|
||||
Returns the "local time" for the machine the Elixir program is running on.
|
||||
|
||||
WARNING: This function can cause insidious bugs. It depends on the time zone
|
||||
configuration at run time. This can changed and be set to a time zone that has
|
||||
configuration at run time. This can change and be set to a time zone that has
|
||||
daylight saving jumps (spring forward or fall back).
|
||||
|
||||
This function can be used to display what the time is right now for the time
|
||||
@@ -541,15 +541,15 @@ defmodule NaiveDateTime do
|
||||
def diff(naive_datetime1, naive_datetime2, unit \\ :second)
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :day) do
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000)
|
||||
end
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :hour) do
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000)
|
||||
end
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :minute) do
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(60)
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000)
|
||||
end
|
||||
|
||||
def diff(
|
||||
@@ -570,9 +570,11 @@ defmodule NaiveDateTime do
|
||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
|
||||
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units1 - units2
|
||||
diff_microsecond =
|
||||
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) -
|
||||
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond))
|
||||
|
||||
System.convert_time_unit(diff_microsecond, :microsecond, unit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -672,7 +674,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec truncate(t(), :microsecond | :millisecond | :second) :: t()
|
||||
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t()
|
||||
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
|
||||
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
|
||||
end
|
||||
|
||||
@@ -62,7 +62,7 @@ defmodule Time do
|
||||
|
||||
You can pass a time unit to automatically truncate the resulting time.
|
||||
|
||||
The default unit if none gets passed is `:native` which results on a default resolution of microseconds.
|
||||
The default unit if none gets passed is `:native` which results in a default resolution of microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -146,8 +146,9 @@ defmodule Time do
|
||||
iex> Time.new(23, 59, 59, 1_000_000)
|
||||
{:error, :invalid_time}
|
||||
|
||||
# Invalid precision
|
||||
Time.new(23, 59, 59, {999_999, 10})
|
||||
Invalid precision:
|
||||
|
||||
iex> Time.new(23, 59, 59, {999_999, 10})
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@@ -466,8 +467,12 @@ defmodule Time do
|
||||
Calendar.microsecond(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
when is_integer(seconds) do
|
||||
def from_seconds_after_midnight(
|
||||
seconds,
|
||||
{microsecond, precision} \\ {0, 0},
|
||||
calendar \\ Calendar.ISO
|
||||
)
|
||||
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
|
||||
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
||||
|
||||
{hour, minute, second, {_, _}} =
|
||||
@@ -478,7 +483,7 @@ defmodule Time do
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
microsecond: {microsecond, precision}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -496,7 +501,7 @@ defmodule Time do
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()}
|
||||
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
||||
iso_days = {0, to_day_fraction(time)}
|
||||
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
||||
@@ -689,7 +694,7 @@ defmodule Time do
|
||||
@doc """
|
||||
Compares two time structs.
|
||||
|
||||
Returns `:gt` if first time is later than the second
|
||||
Returns `:gt` if the first time is later than the second
|
||||
and `:lt` for vice versa. If the two times are equal
|
||||
`:eq` is returned.
|
||||
|
||||
@@ -715,14 +720,32 @@ defmodule Time do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
|
||||
def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
|
||||
%{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
|
||||
%{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
|
||||
|
||||
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
def compare(
|
||||
%{
|
||||
hour: hour1,
|
||||
minute: minute1,
|
||||
second: second1,
|
||||
microsecond: {microsecond1, _},
|
||||
calendar: calendar
|
||||
},
|
||||
%{
|
||||
hour: hour2,
|
||||
minute: minute2,
|
||||
second: second2,
|
||||
microsecond: {microsecond2, _},
|
||||
calendar: calendar
|
||||
}
|
||||
) do
|
||||
cond do
|
||||
hour1 > hour2 -> :gt
|
||||
hour1 < hour2 -> :lt
|
||||
minute1 > minute2 -> :gt
|
||||
minute1 < minute2 -> :lt
|
||||
second1 > second2 -> :gt
|
||||
second1 < second2 -> :lt
|
||||
microsecond1 > microsecond2 -> :gt
|
||||
microsecond1 < microsecond2 -> :lt
|
||||
true -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
@@ -901,11 +924,11 @@ defmodule Time do
|
||||
def diff(time1, time2, unit \\ :second)
|
||||
|
||||
def diff(time1, time2, :hour) do
|
||||
diff(time1, time2, :second) |> div(3600)
|
||||
diff(time1, time2, :microsecond) |> div(3_600_000_000)
|
||||
end
|
||||
|
||||
def diff(time1, time2, :minute) do
|
||||
diff(time1, time2, :second) |> div(60)
|
||||
diff(time1, time2, :microsecond) |> div(60_000_000)
|
||||
end
|
||||
|
||||
def diff(
|
||||
|
||||
+40
-20
@@ -50,7 +50,7 @@ defmodule Code do
|
||||
|
||||
You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and
|
||||
`ensure_loaded!/1`) to check if a module is loaded before using it and
|
||||
act.
|
||||
act accordingly.
|
||||
|
||||
## `ensure_compiled/1` and `ensure_compiled!/1`
|
||||
|
||||
@@ -258,6 +258,7 @@ defmodule Code do
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:force_do_end_blocks, boolean()}
|
||||
| {:migrate, boolean()}
|
||||
| {:migrate_atom_interpolations, boolean()}
|
||||
| {:migrate_bitstring_modifiers, boolean()}
|
||||
| {:migrate_call_parens_on_pipe, boolean()}
|
||||
| {:migrate_charlists_as_sigils, boolean()}
|
||||
@@ -272,6 +273,7 @@ defmodule Code do
|
||||
| {:escape, boolean()}
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:comments, [term()]}
|
||||
| {:syntax_colors, [{Inspect.Opts.color_key(), IO.ANSI.ansidata()}]}
|
||||
|
||||
@typedoc """
|
||||
Options for parsing functions that convert strings to quoted expressions.
|
||||
@@ -285,7 +287,7 @@ defmodule Code do
|
||||
unescape: boolean(),
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
|
||||
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
|
||||
emit_warnings: boolean()
|
||||
]
|
||||
@@ -314,7 +316,7 @@ defmodule Code do
|
||||
:relative_paths
|
||||
]
|
||||
|
||||
@list_compiler_options [:tracers, :parser_options]
|
||||
@list_compiler_options [:tracers, :parser_options, :erlc_options]
|
||||
|
||||
@available_compiler_options @boolean_compiler_options ++
|
||||
@list_compiler_options ++
|
||||
@@ -413,7 +415,7 @@ defmodule Code do
|
||||
operations.
|
||||
|
||||
"""
|
||||
@spec append_path(Path.t(), cache: boolean()) :: true | false
|
||||
@spec append_path(Path.t(), cache: boolean()) :: boolean()
|
||||
def append_path(path, opts \\ []) do
|
||||
apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true
|
||||
end
|
||||
@@ -552,8 +554,7 @@ defmodule Code do
|
||||
This is the list of directories the Erlang VM uses for finding
|
||||
module code. The list of files is managed per Erlang VM node.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being deleted. If the
|
||||
path does not exist, this function returns `false`.
|
||||
All paths are expanded with `Path.expand/1` before being deleted.
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec delete_paths([Path.t()]) :: :ok
|
||||
@@ -767,6 +768,12 @@ defmodule Code do
|
||||
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
|
||||
to `true` by default. Defaults to `false`.
|
||||
|
||||
* `:migrate_atom_interpolations` (since v1.21.0) - when `true`, rewrites
|
||||
deprecated atom interpolations to explicit calls to `String.to_unsafe_atom/1`.
|
||||
For example, `:"foo_#{bar}"` becomes `String.to_unsafe_atom("foo_#{bar}")`.
|
||||
Interpolated keywords like `["foo_#{bar}": 1]` are **not** migrated.
|
||||
Defaults to the value of the `:migrate` option. This option changes the AST.
|
||||
|
||||
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
|
||||
removes unnecessary parentheses in known bitstring
|
||||
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
|
||||
@@ -1291,9 +1298,9 @@ defmodule Code do
|
||||
* `:literal_encoder` (since v1.10.0) - how to encode literals in the AST.
|
||||
It must be a function that receives two arguments, the literal and its
|
||||
metadata, and it must return `{:ok, ast :: Macro.t}` or
|
||||
`{:error, reason :: binary}`. If you return anything than the literal
|
||||
`{:error, reason :: binary}`. If you return anything other than the literal
|
||||
itself as the `term`, then the AST is no longer valid. This option
|
||||
may still useful for textual analysis of the source code.
|
||||
may still be useful for textual analysis of the source code.
|
||||
|
||||
* `:static_atoms_encoder` - the static atom encoder function, see
|
||||
"The `:static_atoms_encoder` function" section below. Note this
|
||||
@@ -1387,12 +1394,15 @@ defmodule Code do
|
||||
while preserving information like comments and literals position.
|
||||
|
||||
Returns `{:ok, quoted_form, comments}` if it succeeds,
|
||||
`{:error, {line, error, token}}` otherwise.
|
||||
`{:error, {location, error, token}}` otherwise, where `location`
|
||||
is keyword metadata containing the line and column of the error.
|
||||
|
||||
Comments are maps with the following fields:
|
||||
|
||||
* `:line` - The line number of the source code
|
||||
|
||||
* `:column` - The column number of the source code
|
||||
|
||||
* `:text` - The full text of the comment, including the leading `#`
|
||||
|
||||
* `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment
|
||||
@@ -1447,7 +1457,9 @@ defmodule Code do
|
||||
|
||||
Returns the AST and a list of comments if it succeeds, raises an exception
|
||||
otherwise. The exception is a `TokenMissingError` in case a token is missing
|
||||
(usually because the expression is incomplete), `SyntaxError` otherwise.
|
||||
(usually because the expression is incomplete), `MismatchedDelimiterError`
|
||||
(in case of mismatched opening and closing delimiters) and `SyntaxError`
|
||||
otherwise.
|
||||
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
@@ -1550,9 +1562,13 @@ defmodule Code do
|
||||
`string_to_quoted/2`, setting this option to `false` will prevent it from
|
||||
escaping the sequences twice. Defaults to `true`.
|
||||
|
||||
* `:syntax_colors` - a keyword list of colors the output is colorized.
|
||||
See `Inspect.Opts` for more information.
|
||||
|
||||
See `format_string!/2` for the full list of formatting options including
|
||||
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
|
||||
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
|
||||
`:file`, `:line`, `:locals_without_parens`, `:force_do_end_blocks`, and all
|
||||
migration options like `:migrate_charlists_as_sigils`. Note `:line_length`
|
||||
does not apply here.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
|
||||
@@ -1672,7 +1688,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores all given compilation options.
|
||||
|
||||
Changing the compilation options affect all processes
|
||||
Changing the compilation options affects all processes
|
||||
running in a given Erlang VM node. To store individual
|
||||
options and for a description of all options, see
|
||||
`put_compiler_option/2`.
|
||||
@@ -1736,7 +1752,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores a compilation option.
|
||||
|
||||
Changing the compilation options affect all processes running in a
|
||||
Changing the compilation options affects all processes running in a
|
||||
given Erlang VM node.
|
||||
|
||||
Available options are:
|
||||
@@ -1752,13 +1768,17 @@ defmodule Code do
|
||||
remove the `:debug_info` while deploying, tools like `mix release`
|
||||
already do such by default.
|
||||
|
||||
Other environments, such as `mix test`, automatically disables this
|
||||
Other environments, such as `mix test`, automatically disable this
|
||||
via the `:test_elixirc_options` project configuration, as there is
|
||||
typically no need to store debug chunks for test files.
|
||||
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
|
||||
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
|
||||
debug metadata for BEAM debuggers. Defaults to `[]`.
|
||||
|
||||
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
|
||||
when a protocol has already been consolidated and a new implementation is added.
|
||||
Defaults to `false`.
|
||||
@@ -1766,13 +1786,13 @@ defmodule Code do
|
||||
* `:ignore_module_conflict` - when `true`, does not warn when a module has
|
||||
already been defined. Defaults to `false`.
|
||||
|
||||
* `:infer_signatures` (since v1.18.0) - a list of applications of which modules
|
||||
should be using during type inference. When `false`, it disables module-local
|
||||
* `:infer_signatures` (since v1.18.0) - a list of applications whose modules
|
||||
should be used during type inference. When `false`, it disables module-local
|
||||
signature inference used when type checking remote calls to the compiled
|
||||
module. Type checking will be executed regardless of the value of this option.
|
||||
Mix projects will set this option to your dependencies list in dev/prod, and
|
||||
it will disable this option during test (as there is typically no need to infer
|
||||
signature for test files). Outside of Mix projects, it defaults to `[:elixir]`.
|
||||
signatures for test files). Outside of Mix projects, it defaults to `[:elixir]`.
|
||||
|
||||
* `:module_definition` (since v1.20.0) - stores if the module definition should
|
||||
be `:compiled` (the default) or `:interpreted`. Note this does not affect the
|
||||
@@ -1795,7 +1815,7 @@ defmodule Code do
|
||||
|
||||
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
|
||||
When `:raise` (the default), undefined variables will trigger a compilation
|
||||
error. You may be set it to `:warn` if you want undefined variables to
|
||||
error. You may set it to `:warn` if you want undefined variables to
|
||||
emit a warning and expand as to a local call to the zero-arity function
|
||||
of the same name (for example, `node` would be expanded as `node()`).
|
||||
This `:warn` behavior only exists for compatibility reasons when working
|
||||
@@ -2130,7 +2150,7 @@ defmodule Code do
|
||||
If the module being checked is currently in a compiler deadlock,
|
||||
this function returns `{:error, :unavailable}`. Unavailable doesn't
|
||||
necessarily mean the module doesn't exist, just that it is not currently
|
||||
available, but it (or may not) become available in the future.
|
||||
available, but it may (or may not) become available in the future.
|
||||
|
||||
Therefore, if you can only continue if the module is available, use
|
||||
`ensure_compiled!/1` instead. In particular, do not do this:
|
||||
|
||||
@@ -195,6 +195,7 @@ defmodule Code.Formatter do
|
||||
file = Keyword.get(opts, :file, nil)
|
||||
sigils = Keyword.get(opts, :sigils, [])
|
||||
migrate = Keyword.get(opts, :migrate, false)
|
||||
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate)
|
||||
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
|
||||
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
|
||||
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
|
||||
@@ -223,6 +224,7 @@ defmodule Code.Formatter do
|
||||
comments: comments,
|
||||
sigils: sigils,
|
||||
file: file,
|
||||
migrate_atom_interpolations: migrate_atom_interpolations,
|
||||
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
|
||||
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
|
||||
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
|
||||
@@ -334,14 +336,20 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra(
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} =
|
||||
quoted,
|
||||
context,
|
||||
state
|
||||
) do
|
||||
if interpolated?(entries) do
|
||||
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
||||
else
|
||||
remote_to_algebra(quoted, context, state)
|
||||
cond do
|
||||
not interpolated?(entries) ->
|
||||
remote_to_algebra(quoted, context, state)
|
||||
|
||||
state.migrate_atom_interpolations ->
|
||||
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
|
||||
|
||||
true ->
|
||||
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1457,7 +1465,7 @@ defmodule Code.Formatter do
|
||||
metadata = [
|
||||
file: state.file,
|
||||
line: meta[:line],
|
||||
sigil: String.to_atom(name),
|
||||
sigil: String.to_unsafe_atom(name),
|
||||
modifiers: modifiers,
|
||||
opening_delimiter: opening_delimiter
|
||||
]
|
||||
@@ -1696,7 +1704,7 @@ defmodule Code.Formatter do
|
||||
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
|
||||
end
|
||||
|
||||
defp integer_to_algebra(text, inspect_otps) do
|
||||
defp integer_to_algebra(text, inspect_opts) do
|
||||
case text do
|
||||
<<?0, ?x, rest::binary>> ->
|
||||
"0x" <> String.upcase(rest)
|
||||
@@ -1710,15 +1718,15 @@ defmodule Code.Formatter do
|
||||
decimal ->
|
||||
insert_underscores(decimal)
|
||||
end
|
||||
|> color_doc(:number, inspect_otps)
|
||||
|> color_doc(:number, inspect_opts)
|
||||
end
|
||||
|
||||
defp float_to_algebra(text, inspect_otps) do
|
||||
defp float_to_algebra(text, inspect_opts) do
|
||||
[int_part, decimal_part] = :binary.split(text, ".")
|
||||
decimal_part = String.downcase(decimal_part)
|
||||
|
||||
string = insert_underscores(int_part) <> "." <> decimal_part
|
||||
color_doc(string, :number, inspect_otps)
|
||||
color_doc(string, :number, inspect_opts)
|
||||
end
|
||||
|
||||
defp insert_underscores("-" <> digits) do
|
||||
|
||||
@@ -27,7 +27,8 @@ defmodule Code.Fragment do
|
||||
column: pos_integer(),
|
||||
columns: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
|
||||
preserve_sigils: boolean(),
|
||||
trailing_fragment: String.t()
|
||||
]
|
||||
|
||||
@@ -35,7 +36,7 @@ defmodule Code.Fragment do
|
||||
Returns the list of lines in the given string, preserving their line endings.
|
||||
|
||||
Only the line endings recognized by the Elixir compiler are
|
||||
considered, namely `\r\n` and `\n`. If you would like the retrieve
|
||||
considered, namely `\r\n` and `\n`. If you would like to retrieve
|
||||
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
|
||||
|
||||
## Examples
|
||||
@@ -51,6 +52,7 @@ defmodule Code.Fragment do
|
||||
|
||||
"""
|
||||
@doc since: "1.19.0"
|
||||
@spec lines(String.t()) :: [String.t()]
|
||||
def lines(string) do
|
||||
lines(string, <<>>)
|
||||
end
|
||||
@@ -140,6 +142,9 @@ defmodule Code.Fragment do
|
||||
* `{:anonymous_call, inside_caller}` - the context is an anonymous
|
||||
call, such as `fun.(` and `@fun.(`.
|
||||
|
||||
* `{:capture_arg, charlist}` - the context is a capture argument,
|
||||
such as `&1`
|
||||
|
||||
* `{:module_attribute, charlist}` - the context is a module attribute,
|
||||
such as `@hello_wor`
|
||||
|
||||
@@ -157,8 +162,8 @@ defmodule Code.Fragment do
|
||||
* `:none` - no context possible
|
||||
|
||||
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
|
||||
of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
|
||||
`~>` and `~>>`
|
||||
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as
|
||||
`~>` and `~>>`, are returned as :operator contexts
|
||||
|
||||
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
|
||||
`inside_struct` can either be a `charlist` in case of a static alias or an
|
||||
@@ -657,7 +662,7 @@ defmodule Code.Fragment do
|
||||
iex> Code.Fragment.surround_context("foo", {1, 1})
|
||||
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
|
||||
|
||||
## Differences to `cursor_context/2`
|
||||
## Differences from `cursor_context/2`
|
||||
|
||||
Because `surround_context/3` attempts to capture complex expressions,
|
||||
it has some differences to `cursor_context/2`:
|
||||
@@ -671,7 +676,7 @@ defmodule Code.Fragment do
|
||||
be a local or variable
|
||||
|
||||
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
|
||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
|
||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`)
|
||||
|
||||
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
|
||||
`{:struct, ~c""}` as context
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule Code.Identifier do
|
||||
with "-" <> rest <- Atom.to_string(atom),
|
||||
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
|
||||
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
@@ -150,20 +150,20 @@ defmodule Code.Identifier do
|
||||
<<acc::binary, char::utf8>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x100 do
|
||||
defp escape_char(char, acc) when char < 0x80 do
|
||||
<<a::4, b::4>> = <<char::8>>
|
||||
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x10000 do
|
||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
|
||||
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x1000000 do
|
||||
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
||||
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
||||
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
||||
?}>>
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
defmodule Code.Normalizer do
|
||||
@moduledoc false
|
||||
|
||||
@do_end_keywords [:rescue, :catch, :else, :after]
|
||||
|
||||
defguard is_literal(x)
|
||||
when is_integer(x) or
|
||||
is_float(x) or
|
||||
@@ -68,7 +70,7 @@ defmodule Code.Normalizer do
|
||||
|
||||
# Bit containers
|
||||
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
||||
normalize_bitstring(quoted, state, false)
|
||||
normalize_bitstring(quoted, state, state.escape)
|
||||
end
|
||||
|
||||
# Atoms with interpolations
|
||||
@@ -182,7 +184,8 @@ defmodule Code.Normalizer do
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
string = normalize_bitstring(string, %{state | parent_meta: meta}, false)
|
||||
{sigil, meta, [string, modifiers]}
|
||||
else
|
||||
_ ->
|
||||
normalize_call(quoted, state)
|
||||
@@ -264,7 +267,7 @@ defmodule Code.Normalizer do
|
||||
"Elixir." <> segments ->
|
||||
segments
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
|> Enum.map(&String.to_unsafe_atom/1)
|
||||
end
|
||||
|
||||
{:__aliases__, meta, segments}
|
||||
@@ -346,18 +349,20 @@ defmodule Code.Normalizer do
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
|
||||
Keyword.has_key?(meta, :do) ->
|
||||
Keyword.has_key?(meta, :do) and kw_blocks?(last) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
|
||||
match?([{:do, _} | _], last) and kw_blocks?(last) ->
|
||||
# Non normalized kw blocks
|
||||
line = state.parent_meta[:line] || meta[:line]
|
||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
true ->
|
||||
# The formatter renders do-end blocks from the meta alone
|
||||
meta = Keyword.drop(meta, [:do, :end])
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
||||
|
||||
@@ -396,12 +401,22 @@ defmodule Code.Normalizer do
|
||||
defp block_keyword?([]), do: true
|
||||
defp block_keyword?(_), do: false
|
||||
|
||||
# Anything after the do block that is not a block keyword makes it a keyword list
|
||||
defp kw_blocks?([{:do, _} | rest] = kw) do
|
||||
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
|
||||
end
|
||||
|
||||
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
|
||||
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
|
||||
end
|
||||
|
||||
defp kw_blocks?(_), do: false
|
||||
|
||||
defp allow_keyword?(:when, 2), do: true
|
||||
defp allow_keyword?(:{}, _), do: false
|
||||
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
|
||||
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
|
||||
parts = maybe_add_trailing_newline(meta, parts, state)
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
parts =
|
||||
@@ -420,6 +435,7 @@ defmodule Code.Normalizer do
|
||||
end)
|
||||
end
|
||||
|
||||
parts = maybe_add_trailing_newline(meta, parts, state)
|
||||
{:<<>>, meta, parts}
|
||||
end
|
||||
|
||||
@@ -559,7 +575,7 @@ defmodule Code.Normalizer do
|
||||
atom
|
||||
|> Atom.to_string()
|
||||
|> maybe_escape_literal(state)
|
||||
|> String.to_atom()
|
||||
|> String.to_unsafe_atom()
|
||||
end
|
||||
|
||||
defp maybe_escape_literal(term, _) do
|
||||
|
||||
@@ -80,7 +80,8 @@ defmodule Code.Typespec do
|
||||
Returns all types available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is the type (`:typep`, `:type` and `:opaque`).
|
||||
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+,
|
||||
`:nominal`).
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
@@ -95,9 +96,10 @@ defmodule Code.Typespec do
|
||||
|
||||
types =
|
||||
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
|
||||
kind in [:opaque, :type] do
|
||||
kind in [:opaque, :type, :nominal] do
|
||||
cond do
|
||||
kind == :opaque -> {:opaque, type}
|
||||
kind == :nominal -> {:nominal, type}
|
||||
{name, length(args)} in exported_types -> {:type, type}
|
||||
true -> {:typep, type}
|
||||
end
|
||||
@@ -117,7 +119,7 @@ defmodule Code.Typespec do
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
located by the runtime system. The specs will be in the Erlang
|
||||
Abstract Format.
|
||||
"""
|
||||
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
|
||||
@@ -135,10 +137,10 @@ defmodule Code.Typespec do
|
||||
Returns all callbacks available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is spec name and arity and the second is the spec.
|
||||
element is the callback name and arity and the second is the callback.
|
||||
|
||||
The module must have a corresponding BEAM file
|
||||
which can be located by the runtime system. The types will be
|
||||
which can be located by the runtime system. The callbacks will be
|
||||
in the Erlang Abstract Format.
|
||||
"""
|
||||
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
|
||||
@@ -191,8 +193,8 @@ defmodule Code.Typespec do
|
||||
|
||||
## To AST conversion
|
||||
|
||||
defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
|
||||
[]
|
||||
defp collect_vars({:ann_type, _anno, [_var, type]}) do
|
||||
collect_vars(type)
|
||||
end
|
||||
|
||||
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
|
||||
@@ -399,10 +401,10 @@ defmodule Code.Typespec do
|
||||
defp erl_to_ex_var(var) do
|
||||
case Atom.to_string(var) do
|
||||
<<"_", c::utf8, rest::binary>> ->
|
||||
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
|
||||
<<c::utf8, rest::binary>> ->
|
||||
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec config(atom(), keyword()) :: keyword()
|
||||
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
|
||||
if not Keyword.keyword?(opts) do
|
||||
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
|
||||
@@ -198,6 +199,7 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec config(atom(), atom(), term()) :: keyword()
|
||||
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
|
||||
get_config!()
|
||||
|> __merge__([{root_key, [{key, opts}]}])
|
||||
@@ -225,6 +227,7 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec read_config(atom()) :: keyword() | nil
|
||||
def read_config(root_key) when is_atom(root_key) do
|
||||
get_config!()[root_key]
|
||||
end
|
||||
@@ -233,7 +236,7 @@ defmodule Config do
|
||||
Returns the environment this configuration file is executed on.
|
||||
|
||||
In Mix projects this function returns the environment this configuration
|
||||
file is executed on.
|
||||
file is executed on.
|
||||
In releases, returns the `MIX_ENV` specified when running `mix release`.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
@@ -284,8 +287,8 @@ defmodule Config do
|
||||
|
||||
In case the file doesn't exist, an error is raised.
|
||||
|
||||
If file is a relative, it will be expanded relatively to the
|
||||
directory the current configuration file is in.
|
||||
If the file is relative, it will be expanded relative to the
|
||||
directory of the current configuration file.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule Config.Provider do
|
||||
For example, imagine you want to list some basic configuration
|
||||
on Mix's built-in `config/runtime.exs` file, but you also want
|
||||
to support additional configuration files. To do so, you can add
|
||||
this inside the `def project` portion of your `mix.exs`:
|
||||
this inside the `def project` portion of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule Config.Reader do
|
||||
|
||||
For example, if you expect the target system to have a config file
|
||||
in an absolute path, you can add this inside the `def project` portion
|
||||
of your `mix.exs`:
|
||||
of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
|
||||
+76
-34
@@ -618,7 +618,7 @@ defmodule Enum do
|
||||
acc,
|
||||
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
|
||||
(acc -> {:cont, chunk, acc} | {:cont, acc})
|
||||
) :: Enumerable.t()
|
||||
) :: [chunk]
|
||||
when chunk: any
|
||||
def chunk_while(enumerable, acc, chunk_fun, after_fun) do
|
||||
{_, {res, acc}} =
|
||||
@@ -666,7 +666,7 @@ defmodule Enum do
|
||||
[1, [2], 3, 4, 5, 6]
|
||||
|
||||
"""
|
||||
@spec concat(t) :: t
|
||||
@spec concat(Enumerable.t(Enumerable.t(elem))) :: [elem] when elem: term
|
||||
def concat(enumerables)
|
||||
|
||||
def concat(list) when is_list(list) do
|
||||
@@ -681,8 +681,8 @@ defmodule Enum do
|
||||
Concatenates the enumerable on the `right` with the enumerable on the
|
||||
`left`.
|
||||
|
||||
This function produces the same result as the `++/2` operator
|
||||
for lists.
|
||||
This function behaves similarly to the `++/2` operator with proper
|
||||
lists, but applied to enumerables.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -693,7 +693,7 @@ defmodule Enum do
|
||||
[1, 2, 3, 4, 5, 6]
|
||||
|
||||
"""
|
||||
@spec concat(t, t) :: t
|
||||
@spec concat(Enumerable.t(elem), Enumerable.t(elem)) :: [elem] when elem: term
|
||||
def concat(left, right) when is_list(left) and is_list(right) do
|
||||
left ++ right
|
||||
end
|
||||
@@ -859,6 +859,10 @@ defmodule Enum do
|
||||
|
||||
"""
|
||||
@spec dedup_by(t, (element -> term)) :: list
|
||||
def dedup_by([head | tail], fun) do
|
||||
dedup_by_list(tail, fun, fun.(head), [head])
|
||||
end
|
||||
|
||||
def dedup_by(enumerable, fun) do
|
||||
{list, _} = reduce(enumerable, {[], []}, R.dedup(fun))
|
||||
:lists.reverse(list)
|
||||
@@ -903,7 +907,7 @@ defmodule Enum do
|
||||
|
||||
def drop(enumerable, amount) when is_integer(amount) and amount < 0 do
|
||||
{count, fun} = slice_count_and_fun(enumerable, 1)
|
||||
amount = Kernel.min(amount + count, count)
|
||||
amount = amount + count
|
||||
|
||||
if amount > 0 do
|
||||
fun.(0, amount, 1)
|
||||
@@ -1442,7 +1446,7 @@ defmodule Enum do
|
||||
)
|
||||
|
||||
# Avoid warnings about Dict
|
||||
dict_module = String.to_atom("Dict")
|
||||
dict_module = String.to_unsafe_atom("Dict")
|
||||
|
||||
reduce(reverse(enumerable), dict, fn entry, categories ->
|
||||
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
|
||||
@@ -2229,14 +2233,19 @@ defmodule Enum do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module()) :: {min :: element, max :: element}
|
||||
@spec min_max(t, (-> empty_result)) :: {min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
|
||||
{element, element} | empty_result
|
||||
{min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
|
||||
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
|
||||
def min_max(list = [_ | _], empty_fallback) when is_function(empty_fallback, 0) do
|
||||
min_max_list(list)
|
||||
end
|
||||
|
||||
def min_max(first..last//step = range, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
case Range.size(range) do
|
||||
@@ -2343,14 +2352,14 @@ defmodule Enum do
|
||||
|
||||
"""
|
||||
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
|
||||
{element, element} | empty_result
|
||||
{min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
@spec min_max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module(),
|
||||
(-> empty_result)
|
||||
) :: {element, element} | empty_result
|
||||
) :: {min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max_by(
|
||||
enumerable,
|
||||
@@ -2394,6 +2403,18 @@ defmodule Enum do
|
||||
|
||||
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
|
||||
defp min_max_list([h | t]), do: min_max_list(t, h, h)
|
||||
|
||||
defp min_max_list([h | t], min, max) do
|
||||
cond do
|
||||
h < min -> min_max_list(t, h, max)
|
||||
max < h -> min_max_list(t, min, h)
|
||||
true -> min_max_list(t, min, max)
|
||||
end
|
||||
end
|
||||
|
||||
defp min_max_list([], min, max), do: {min, max}
|
||||
|
||||
@doc """
|
||||
Splits the `enumerable` in two lists according to the given function `fun`.
|
||||
|
||||
@@ -2671,7 +2692,7 @@ defmodule Enum do
|
||||
5050
|
||||
|
||||
"""
|
||||
@spec reduce_while(t, any, (element, any -> {:cont, any} | {:halt, any})) :: any
|
||||
@spec reduce_while(t, acc, (element, acc -> {:cont, acc} | {:halt, acc})) :: acc
|
||||
def reduce_while(enumerable, acc, fun) do
|
||||
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
|
||||
end
|
||||
@@ -2914,8 +2935,7 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
defp slide_list_middle(list, 0, last, start_to_middle) do
|
||||
{slid_range, tail} = slide_list_last(list, last + 1, [])
|
||||
slid_range ++ :lists.reverse(start_to_middle, tail)
|
||||
slide_list_last(list, last + 1, [], start_to_middle)
|
||||
end
|
||||
|
||||
# You asked for a middle index off the end of the list... you get what we've got
|
||||
@@ -2923,16 +2943,16 @@ defmodule Enum do
|
||||
:lists.reverse(acc)
|
||||
end
|
||||
|
||||
defp slide_list_last([h | t], last, acc) when last > 0 do
|
||||
slide_list_last(t, last - 1, [h | acc])
|
||||
defp slide_list_last([h | t], last, acc, start_to_middle) when last > 0 do
|
||||
slide_list_last(t, last - 1, [h | acc], start_to_middle)
|
||||
end
|
||||
|
||||
defp slide_list_last(rest, 0, acc) do
|
||||
{:lists.reverse(acc), rest}
|
||||
defp slide_list_last(rest, 0, acc, start_to_middle) do
|
||||
:lists.reverse(acc, :lists.reverse(start_to_middle, rest))
|
||||
end
|
||||
|
||||
defp slide_list_last([], _, acc) do
|
||||
{:lists.reverse(acc), []}
|
||||
defp slide_list_last([], _, acc, start_to_middle) do
|
||||
:lists.reverse(acc, :lists.reverse(start_to_middle))
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4198,7 +4218,7 @@ defmodule Enum do
|
||||
|
||||
`zip_with/2` can be used to transpose lists of lists:
|
||||
|
||||
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
|
||||
iex> Enum.zip_with([[1, 2], [3, 4]], & &1)
|
||||
[[1, 3], [2, 4]]
|
||||
|
||||
"""
|
||||
@@ -4309,11 +4329,24 @@ defmodule Enum do
|
||||
empty.()
|
||||
|
||||
_ ->
|
||||
last = last - rem(last - first, step)
|
||||
# The endpoint shortcut is only valid for sorters consistent with
|
||||
# the natural integer order of the range elements, which is known
|
||||
# to hold for the default sorters; any other sorter traverses the
|
||||
# elements, seeded with the first one since the range is not empty
|
||||
if fun == (&<=/2) or fun == (&>=/2) do
|
||||
last = last - rem(last - first, step)
|
||||
|
||||
case fun.(first, last) do
|
||||
true -> first
|
||||
false -> last
|
||||
case fun.(first, last) do
|
||||
true -> first
|
||||
false -> last
|
||||
end
|
||||
else
|
||||
reduce_range(first + step, last, step, first, fn element, acc ->
|
||||
case fun.(acc, element) do
|
||||
true -> acc
|
||||
false -> element
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4416,10 +4449,9 @@ defmodule Enum do
|
||||
## any?/2 all?/2
|
||||
|
||||
defp predicate_list([h | t], initial, fun) do
|
||||
if !!fun.(h) == initial do
|
||||
predicate_list(t, initial, fun)
|
||||
else
|
||||
not initial
|
||||
case !!fun.(h) do
|
||||
^initial -> predicate_list(t, initial, fun)
|
||||
_ -> not initial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4430,10 +4462,9 @@ defmodule Enum do
|
||||
defp predicate_range(first, last, step, initial, fun)
|
||||
when step > 0 and first <= last
|
||||
when step < 0 and first >= last do
|
||||
if !!fun.(first) == initial do
|
||||
predicate_range(first + step, last, step, initial, fun)
|
||||
else
|
||||
not initial
|
||||
case !!fun.(first) do
|
||||
^initial -> predicate_range(first + step, last, step, initial, fun)
|
||||
_ -> not initial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4516,6 +4547,17 @@ defmodule Enum do
|
||||
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
|
||||
defp dedup_list([]), do: []
|
||||
|
||||
## dedup_by
|
||||
|
||||
defp dedup_by_list([head | tail], fun, prev, acc) do
|
||||
case fun.(head) do
|
||||
^prev -> dedup_by_list(tail, fun, prev, acc)
|
||||
new_val -> dedup_by_list(tail, fun, new_val, [head | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp dedup_by_list([], _fun, _prev, acc), do: :lists.reverse(acc)
|
||||
|
||||
## drop
|
||||
|
||||
defp drop_list(list, 0), do: list
|
||||
|
||||
@@ -1046,7 +1046,7 @@ defmodule RuntimeError do
|
||||
iex> raise "oops!"
|
||||
** (RuntimeError) oops!
|
||||
|
||||
You should use this exceptions sparingly, since most of the time it might be
|
||||
You should use this exception sparingly, since most of the time it might be
|
||||
better to define your own exceptions specific to your application or library.
|
||||
Sometimes, however, there are situations in which you don't expect a condition to
|
||||
happen, but you want to give a meaningful error message if it does. In those cases,
|
||||
@@ -1137,7 +1137,7 @@ defmodule SystemLimitError do
|
||||
|
||||
For example, this can happen if you try to create an atom that is too large:
|
||||
|
||||
iex> String.to_atom(String.duplicate("a", 100_000))
|
||||
iex> String.to_unsafe_atom(String.duplicate("a", 100_000))
|
||||
** (SystemLimitError) a system limit has been reached
|
||||
"""
|
||||
|
||||
@@ -1807,7 +1807,7 @@ defmodule UndefinedFunctionError do
|
||||
|
||||
defp load_module({name, _path, _loaded?}) do
|
||||
name
|
||||
|> List.to_atom()
|
||||
|> List.to_unsafe_atom()
|
||||
|> Code.ensure_loaded()
|
||||
end
|
||||
|
||||
@@ -2376,7 +2376,7 @@ defmodule File.Error do
|
||||
@moduledoc """
|
||||
An exception that is raised when a file operation fails.
|
||||
|
||||
For example, this exception is raised, when trying to read a non existent file:
|
||||
For example, this exception is raised, when trying to read a nonexistent file:
|
||||
|
||||
iex> File.read!("nonexistent_file.txt")
|
||||
** (File.Error) could not read file "nonexistent_file.txt": no such file or directory
|
||||
@@ -2409,7 +2409,7 @@ defmodule File.CopyError do
|
||||
@moduledoc """
|
||||
An exception that is raised when copying a file fails.
|
||||
|
||||
For example, this exception is raised when trying to copy to file or directory that isn't present:
|
||||
For example, this exception is raised when trying to copy to a file or directory that isn't present:
|
||||
|
||||
iex> File.cp_r!("non_existent", "source_dir/subdir")
|
||||
** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory
|
||||
@@ -2477,7 +2477,7 @@ defmodule File.LinkError do
|
||||
@moduledoc """
|
||||
An exception that is raised when linking a file fails.
|
||||
|
||||
For example, this exception is raised when trying to link to file that isn't present:
|
||||
For example, this exception is raised when trying to link to a file that isn't present:
|
||||
|
||||
iex> File.ln!("existing.txt", "link.txt")
|
||||
** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory
|
||||
|
||||
@@ -897,7 +897,7 @@ defmodule File do
|
||||
File.copy("non_existing.txt", "copy.txt")
|
||||
#=> {:error, :enoent}
|
||||
"""
|
||||
@spec copy(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
|
||||
@spec copy(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
|
||||
{:ok, non_neg_integer} | {:error, posix | :badarg | :terminated}
|
||||
def copy(source, destination, bytes_count \\ :infinity) do
|
||||
source = normalize_path_or_io_device(source)
|
||||
@@ -918,7 +918,7 @@ defmodule File do
|
||||
File.copy!("non_existing.txt", "copy.txt")
|
||||
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
|
||||
"""
|
||||
@spec copy!(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
|
||||
@spec copy!(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
|
||||
non_neg_integer
|
||||
def copy!(source, destination, bytes_count \\ :infinity) do
|
||||
case copy(source, destination, bytes_count) do
|
||||
@@ -1362,9 +1362,8 @@ defmodule File do
|
||||
end
|
||||
|
||||
defp copy_file_mode(src, dest) do
|
||||
with {:ok, dest_fileinfo} <- stat(dest),
|
||||
{:ok, src_fileinfo} <- stat(src) do
|
||||
write_stat(dest, %{dest_fileinfo | mode: src_fileinfo.mode})
|
||||
with {:ok, src_fileinfo} <- stat(src) do
|
||||
chmod(dest, src_fileinfo.mode)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2221,7 +2220,7 @@ defmodule File do
|
||||
type. If you pass, for example, `[encoding: :utf8]` or
|
||||
`[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream
|
||||
will use `IO.write/2` and the `String.Chars` protocol to convert the data.
|
||||
See `IO.binwrite/2` and `IO.write/2` .
|
||||
See `IO.binwrite/2` and `IO.write/2`.
|
||||
|
||||
One may also consider passing the `:delayed_write` option if the stream
|
||||
is meant to be written to under a tight loop.
|
||||
|
||||
@@ -18,7 +18,13 @@ defmodule File.Stream do
|
||||
|
||||
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
@type t :: %__MODULE__{
|
||||
path: Path.t(),
|
||||
modes: [term()],
|
||||
line_or_bytes: :line | pos_integer(),
|
||||
raw: boolean(),
|
||||
node: node()
|
||||
}
|
||||
|
||||
@doc false
|
||||
def __build__(path, line_or_bytes, modes) do
|
||||
|
||||
+59
-26
@@ -25,7 +25,7 @@ defmodule Float do
|
||||
and arithmetic due to the fact most decimal fractions cannot be
|
||||
represented by a floating-point binary and most operations are not exact,
|
||||
but operate on approximations. Those issues are not specific
|
||||
to Elixir, they are a property of floating point representation itself.
|
||||
to Elixir, they are a property of floating-point representation itself.
|
||||
|
||||
For example, the numbers 0.1 and 0.01 are two of them, what means the result
|
||||
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
|
||||
@@ -167,40 +167,73 @@ defmodule Float do
|
||||
parse_unsigned(binary)
|
||||
end
|
||||
|
||||
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, false, false, [digit])
|
||||
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, false)
|
||||
|
||||
defp parse_unsigned(binary) when is_binary(binary), do: :error
|
||||
|
||||
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, dot?, e?, [digit | acc])
|
||||
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, dot?)
|
||||
|
||||
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, false, [digit, ?. | acc])
|
||||
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, true)
|
||||
|
||||
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
|
||||
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?)
|
||||
when exp_marker in ~c"eE" and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
|
||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
||||
|
||||
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
|
||||
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?)
|
||||
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
|
||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
||||
|
||||
# :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example,
|
||||
# "1.0e400". Because of this, we rescue the ArgumentError here and return an error.
|
||||
defp parse_unsigned(rest, dot?, _, acc) do
|
||||
acc
|
||||
|> add_dot(dot?)
|
||||
|> :lists.reverse()
|
||||
|> :erlang.list_to_float()
|
||||
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?)
|
||||
|
||||
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
||||
do: parse_exponent(binary, exp_pos, rest, dot?)
|
||||
|
||||
defp parse_exponent(binary, exp_pos, rest, dot?),
|
||||
do: finish_exponent(binary, exp_pos, rest, dot?)
|
||||
|
||||
defp finish_mantissa(binary, rest, _dot? = true) do
|
||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
else
|
||||
float -> {float, rest}
|
||||
end
|
||||
|
||||
defp add_dot(acc, true), do: acc
|
||||
defp add_dot(acc, false), do: [?0, ?. | acc]
|
||||
# Bare integer: * 1.0 casts to the nearest float without building a new binary,
|
||||
# and raises ArithmeticError on overflow (for example a 400-digit integer).
|
||||
defp finish_mantissa(binary, rest, _dot? = false) do
|
||||
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
|
||||
rescue
|
||||
ArithmeticError -> :error
|
||||
end
|
||||
|
||||
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400".
|
||||
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do
|
||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
|
||||
# form a valid float literal.
|
||||
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
|
||||
len = byte_size(binary) - byte_size(rest)
|
||||
|
||||
literal =
|
||||
IO.iodata_to_binary([
|
||||
:binary.part(binary, 0, exp_pos),
|
||||
".0",
|
||||
:binary.part(binary, exp_pos, len - exp_pos)
|
||||
])
|
||||
|
||||
{:erlang.binary_to_float(literal), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
defp consumed(binary, ""), do: binary
|
||||
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest float less than or equal to `number`.
|
||||
@@ -253,7 +286,7 @@ defmodule Float do
|
||||
@doc """
|
||||
Rounds a float to the smallest float greater than or equal to `number`.
|
||||
|
||||
`ceil/2` also accepts a precision to round a floating-point value down
|
||||
`ceil/2` also accepts a precision to round a floating-point value up
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
|
||||
The operation is performed on the binary floating point, without a
|
||||
@@ -322,7 +355,7 @@ defmodule Float do
|
||||
and therefore the number above is internally represented as 5.567499999,
|
||||
which explains the behavior above. If you want exact rounding for decimals,
|
||||
you must use a decimal library. The behavior above is also in accordance
|
||||
to reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||
with reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||
Decimal-Binary Conversions" by David M. Gay.
|
||||
|
||||
## Examples
|
||||
@@ -346,7 +379,7 @@ defmodule Float do
|
||||
@spec round(float, precision_range) :: float
|
||||
def round(float, precision \\ 0)
|
||||
|
||||
def round(float, 0) when float == 0.0, do: float
|
||||
def round(float, 0) when float === 0.0 or float === -0.0, do: float
|
||||
|
||||
def round(float, 0) when is_float(float) do
|
||||
case :erlang.round(float) * 1.0 do
|
||||
@@ -672,7 +705,7 @@ defmodule Float do
|
||||
end
|
||||
|
||||
defp invalid_precision_message(precision) do
|
||||
"precision #{precision} is out of valid range of #{inspect(@precision_range)}"
|
||||
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}"
|
||||
end
|
||||
|
||||
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
|
||||
|
||||
@@ -69,7 +69,6 @@ defmodule Function do
|
||||
| :name
|
||||
| :new_index
|
||||
| :new_uniq
|
||||
| :pid
|
||||
| :type
|
||||
| :uniq
|
||||
|
||||
@@ -112,8 +111,6 @@ defmodule Function do
|
||||
When `fun` is an anonymous function (that is, the type is `:local`), the following
|
||||
additional keys are returned:
|
||||
|
||||
* `:pid` - PID of the process that originally created the function.
|
||||
|
||||
* `:index` - (integer) an index into the module function table.
|
||||
|
||||
* `:new_index` - (integer) an index into the module function table.
|
||||
@@ -159,7 +156,7 @@ defmodule Function do
|
||||
`:module`, `:name`, `:arity`, `:env`, or `:type`.
|
||||
|
||||
For anonymous functions, there is also information about any of the
|
||||
atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
|
||||
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`.
|
||||
For a named function, the value of any of these items is always the
|
||||
atom `:undefined`.
|
||||
|
||||
@@ -179,8 +176,6 @@ defmodule Function do
|
||||
iex> fun = &String.length/1
|
||||
iex> Function.info(fun, :name)
|
||||
{:name, :length}
|
||||
iex> Function.info(fun, :pid)
|
||||
{:pid, :undefined}
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule GenEvent do
|
||||
alternative. GenStage is an external Elixir library maintained by the Elixir
|
||||
team; it provides a tool to implement systems that exchange events in a
|
||||
demand-driven way with built-in support for back-pressure. See the [GenStage
|
||||
documentation](https://hexdocs.pm/gen_stage) for more information.
|
||||
documentation](https://gen-stage.hexdocs.pm) for more information.
|
||||
|
||||
### `:gen_event`
|
||||
|
||||
|
||||
@@ -1158,7 +1158,7 @@ defmodule GenServer do
|
||||
|
||||
## Timeouts
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds to wait for a reply, or the atom `:infinity` to wait
|
||||
indefinitely. The default value is `5000`. If no reply is received within
|
||||
the specified time, the function call fails and the caller exits. If the
|
||||
|
||||
@@ -265,13 +265,13 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def reduce(dict, acc, fun) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module.reduce(dict, acc, fun)
|
||||
end
|
||||
|
||||
def member?(dict, {key, value}) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
|
||||
end
|
||||
|
||||
@@ -281,7 +281,7 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def count(dict) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
{:ok, module.size(dict)}
|
||||
end
|
||||
|
||||
@@ -296,7 +296,7 @@ defimpl Collectable, for: HashDict do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
|
||||
collector_fun = fn
|
||||
dict, {:cont, {key, value}} -> module.put(dict, key, value)
|
||||
@@ -315,7 +315,7 @@ defimpl Inspect, for: HashDict do
|
||||
|
||||
def inspect(dict, opts) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -279,19 +279,19 @@ defimpl Enumerable, for: HashSet do
|
||||
|
||||
def reduce(set, acc, fun) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module.reduce(set, acc, fun)
|
||||
end
|
||||
|
||||
def member?(set, term) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
{:ok, module.member?(set, term)}
|
||||
end
|
||||
|
||||
def count(set) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
{:ok, module.size(set)}
|
||||
end
|
||||
|
||||
@@ -306,7 +306,7 @@ defimpl Collectable, for: HashSet do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
|
||||
collector_fun = fn
|
||||
set, {:cont, term} -> module.put(set, term)
|
||||
@@ -325,7 +325,7 @@ defimpl Inspect, for: HashSet do
|
||||
|
||||
def inspect(set, opts) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -92,7 +92,7 @@ defprotocol Inspect do
|
||||
end
|
||||
|
||||
inspect(%Point{x: 1})
|
||||
%Point{x: 1, y: 0}
|
||||
#=> %Point{x: 1, y: 0}
|
||||
|
||||
## Custom implementation
|
||||
|
||||
@@ -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)
|
||||
@@ -416,8 +436,8 @@ defimpl Inspect, for: List do
|
||||
|
||||
@doc false
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
case Atom.to_charlist(key) do
|
||||
[?E, ?l, ?i, ?x, ?i, ?r, ?.] ++ _ -> false
|
||||
case Atom.to_string(key) do
|
||||
"Elixir." <> _ -> false
|
||||
_ -> keyword?(rest)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ defmodule Integer do
|
||||
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
|
||||
|
||||
@doc """
|
||||
Computes `base` raised to power of `exponent`.
|
||||
Computes `base` raised to the power of `exponent`.
|
||||
|
||||
Both `base` and `exponent` must be integers.
|
||||
The exponent must be zero or positive.
|
||||
@@ -545,10 +545,10 @@ defmodule Integer do
|
||||
@doc since: "1.12.0"
|
||||
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
|
||||
def extended_gcd(0, 0), do: {0, 0, 0}
|
||||
def extended_gcd(0, b) when b > 0, do: {b, 0, 1}
|
||||
def extended_gcd(0, b) when b < 0, do: {-b, 0, -1}
|
||||
def extended_gcd(a, 0) when a > 0, do: {a, 1, 0}
|
||||
def extended_gcd(a, 0) when a < 0, do: {-a, -1, 0}
|
||||
def extended_gcd(0, b) when is_integer(b) and b > 0, do: {b, 0, 1}
|
||||
def extended_gcd(0, b) when is_integer(b) and b < 0, do: {-b, 0, -1}
|
||||
def extended_gcd(a, 0) when is_integer(a) and a > 0, do: {a, 1, 0}
|
||||
def extended_gcd(a, 0) when is_integer(a) and a < 0, do: {-a, -1, 0}
|
||||
|
||||
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
|
||||
extended_gcd(integer2, integer1, 0, 1, 1, 0)
|
||||
|
||||
@@ -128,7 +128,7 @@ defmodule IO do
|
||||
@type nodata :: {:error, term} | :eof
|
||||
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
|
||||
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, String.Chars.t()}]
|
||||
|
||||
@typedoc """
|
||||
Stacktrace information as keyword options for `warn/2`.
|
||||
@@ -152,9 +152,9 @@ defmodule IO do
|
||||
|
||||
The `device` is iterated as specified by the `line_or_chars` argument:
|
||||
|
||||
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
|
||||
iterated by that number of bytes. This should be the preferred mode for reading
|
||||
non-textual inputs.
|
||||
* if `line_or_chars` is an integer, it is the number of Unicode
|
||||
code points to be retrieved for devices open in Unicode/utf8 mode.
|
||||
Otherwise, it is the number of raw bytes to be retrieved.
|
||||
|
||||
* if `line_or_chars` is `:line`, the device is iterated line by line.
|
||||
CRLF newlines ("\r\n") are automatically normalized to "\n".
|
||||
@@ -521,7 +521,7 @@ defmodule IO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from IO device `:stdio`.
|
||||
Gets a number of characters from IO device `:stdio`.
|
||||
|
||||
If `:stdio` is a Unicode device, `count` implies
|
||||
the number of Unicode code points to be retrieved.
|
||||
@@ -549,7 +549,7 @@ defmodule IO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from the IO `device`.
|
||||
Gets a number of characters from the IO `device`.
|
||||
|
||||
If the IO `device` is a Unicode device, `count` implies
|
||||
the number of Unicode code points to be retrieved.
|
||||
|
||||
@@ -111,13 +111,15 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defsequence = fn name, code, terminator ->
|
||||
sequence = "\e[#{code}#{terminator}"
|
||||
|
||||
@spec unquote(name)() :: String.t()
|
||||
def unquote(name)() do
|
||||
"\e[#{unquote(code)}#{unquote(terminator)}"
|
||||
unquote(sequence)
|
||||
end
|
||||
|
||||
defp format_sequence(unquote(name)) do
|
||||
unquote(name)()
|
||||
unquote(sequence)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -159,7 +161,7 @@ defmodule IO.ANSI do
|
||||
|
||||
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
|
||||
@doc "Sets alternative font #{font_n}."
|
||||
defsequence.(:"font_#{font_n}", font_n + 10, "m")
|
||||
defsequence.(String.to_unsafe_atom("font_#{font_n}"), font_n + 10, "m")
|
||||
end
|
||||
|
||||
@doc "Normal color or intensity."
|
||||
@@ -193,13 +195,13 @@ defmodule IO.ANSI do
|
||||
defsequence.(color, code + 30, "m")
|
||||
|
||||
@doc "Sets foreground color to light #{color}."
|
||||
defsequence.(:"light_#{color}", code + 90, "m")
|
||||
defsequence.(String.to_unsafe_atom("light_#{color}"), code + 90, "m")
|
||||
|
||||
@doc "Sets background color to #{color}."
|
||||
defsequence.(:"#{color}_background", code + 40, "m")
|
||||
defsequence.(String.to_unsafe_atom("#{color}_background"), code + 40, "m")
|
||||
|
||||
@doc "Sets background color to light #{color}."
|
||||
defsequence.(:"light_#{color}_background", code + 100, "m")
|
||||
defsequence.(String.to_unsafe_atom("light_#{color}_background"), code + 100, "m")
|
||||
end
|
||||
|
||||
@doc "Default text color."
|
||||
@@ -332,7 +334,7 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, true, true) do
|
||||
[acc | IO.ANSI.reset()]
|
||||
[acc | reset()]
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, _emit?, _append_reset) do
|
||||
|
||||
@@ -673,7 +673,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
# Characters that can mark the beginning or the end of a word.
|
||||
# Only support the most common ones at this moment.
|
||||
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
@delimiters [?\s, ?', ?", ?!, ??, ?,, ?:, ?;, ?/, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
|
||||
|
||||
### Inline start
|
||||
|
||||
@@ -31,7 +31,7 @@ defmodule IO.Stream do
|
||||
@type t :: %__MODULE__{
|
||||
device: IO.device(),
|
||||
raw: boolean(),
|
||||
line_or_bytes: :line | non_neg_integer()
|
||||
line_or_bytes: :line | pos_integer()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -24,7 +24,7 @@ defprotocol JSON.Encoder do
|
||||
> #### Leaking Private Information {: .error}
|
||||
>
|
||||
> Prefer using `:only` to avoid accidentally leaking private information when
|
||||
> new fields are added. Other approaches should be used with auction.
|
||||
> new fields are added. Other approaches should be used with caution.
|
||||
|
||||
You can also use `Protocol.derive/3` if you don't own the struct that you want
|
||||
to encode to JSON:
|
||||
@@ -175,6 +175,7 @@ defimpl JSON.Encoder, for: Map do
|
||||
|
||||
# Erlang supports only numbers, binaries, and atoms as keys,
|
||||
# we support anything that implements the String.Chars protocol.
|
||||
@compile inline: [key: 2]
|
||||
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
|
||||
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
|
||||
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
|
||||
|
||||
+17
-12
@@ -142,7 +142,7 @@ defmodule Kernel do
|
||||
* [Patterns and guards](patterns-and-guards.md) - an introduction to patterns,
|
||||
guards, and extensions
|
||||
* [Syntax reference](syntax-reference.md) - the language syntax reference
|
||||
* [Typespecs reference](typespecs.md)- types and function specifications, including list of types
|
||||
* [Typespecs reference](typespecs.md) - types and function specifications, including list of types
|
||||
* [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode
|
||||
|
||||
## Guards
|
||||
@@ -3088,7 +3088,7 @@ defmodule Kernel do
|
||||
@doc """
|
||||
Pops a key from the given nested structure.
|
||||
|
||||
Uses the `Access` protocol to traverse the structures
|
||||
Uses the `Access` behaviour to traverse the structures
|
||||
according to the given `keys`, unless the `key` is a
|
||||
function. If the key is a function, it will be invoked
|
||||
as specified in `get_and_update_in/3`.
|
||||
@@ -4960,6 +4960,7 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec binary_slice(binary, integer, non_neg_integer) :: binary
|
||||
def binary_slice(binary, start, size)
|
||||
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
|
||||
total = byte_size(binary)
|
||||
@@ -5033,6 +5034,7 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec binary_slice(binary, Range.t()) :: binary
|
||||
def binary_slice(binary, first..last//step)
|
||||
when is_binary(binary) and step > 0 do
|
||||
total = byte_size(binary)
|
||||
@@ -5147,7 +5149,7 @@ defmodule Kernel do
|
||||
warning saying that a module has been redefined.
|
||||
|
||||
There are some modules that Elixir does not currently implement but it
|
||||
may be implement in the future. Those modules are reserved and defining
|
||||
may implement in the future. Those modules are reserved and defining
|
||||
them will result in a compilation error:
|
||||
|
||||
defmodule Any do
|
||||
@@ -5248,12 +5250,12 @@ defmodule Kernel do
|
||||
# defmodule Alias nested
|
||||
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
|
||||
module = :elixir_aliases.concat([env.module, h])
|
||||
alias = String.to_atom("Elixir." <> Atom.to_string(h))
|
||||
alias = String.to_unsafe_atom("Elixir." <> Atom.to_string(h))
|
||||
opts = [as: alias, warn: false]
|
||||
|
||||
case t do
|
||||
[] -> {module, module, opts}
|
||||
_ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
|
||||
_ -> {String.to_unsafe_atom(Enum.join([module | t], ".")), module, opts}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5560,7 +5562,7 @@ defmodule Kernel do
|
||||
when the struct is printed:
|
||||
|
||||
defmodule User do
|
||||
@derive {Inspect, only: :name}
|
||||
@derive {Inspect, only: [:name]}
|
||||
defstruct name: nil, age: nil
|
||||
end
|
||||
|
||||
@@ -5587,6 +5589,9 @@ defmodule Kernel do
|
||||
defstruct name: nil, age: 10 + 11
|
||||
end
|
||||
|
||||
`@enforce_keys` must be set to an atom or a list of unique atoms,
|
||||
all of which must name fields defined by `defstruct/1`
|
||||
|
||||
Now trying to build a struct without the name key will fail:
|
||||
|
||||
%User{age: 21}
|
||||
@@ -6278,7 +6283,7 @@ defmodule Kernel do
|
||||
step through the code it sees). For general stepping, you can set breakpoints
|
||||
using `IEx.break!/4`.
|
||||
|
||||
For more information, [see IEx documentation](https://hexdocs.pm/iex/IEx.html#module-dbg-and-breakpoints).
|
||||
For more information, [see IEx documentation](https://iex.hexdocs.pm/IEx.html#module-dbg-and-breakpoints).
|
||||
|
||||
## Configuring the debug function
|
||||
|
||||
@@ -6377,7 +6382,7 @@ defmodule Kernel do
|
||||
|
||||
### Passing timeouts
|
||||
|
||||
You can also pass timeouts directly to this functions, that is, milliseconds or
|
||||
You can also pass timeouts directly to this function, that is, milliseconds or
|
||||
the atom `:infinity`. In this case, this function just returns the given argument.
|
||||
|
||||
## Examples
|
||||
@@ -6897,7 +6902,7 @@ defmodule Kernel do
|
||||
defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string)
|
||||
when alias >= ?A and alias <= ?Z do
|
||||
string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1)
|
||||
{String.to_atom("Elixir." <> last_part), string}
|
||||
{String.to_unsafe_atom("Elixir." <> last_part), string}
|
||||
end
|
||||
|
||||
defp maybe_atomize_calendar(_last_part, string) do
|
||||
@@ -7008,7 +7013,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> :lists.map(&String.to_atom/1, parts)
|
||||
?a -> :lists.map(&String.to_unsafe_atom/1, parts)
|
||||
?c -> :lists.map(&String.to_charlist/1, parts)
|
||||
end
|
||||
|
||||
@@ -7017,7 +7022,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> quote(do: :lists.map(&String.to_atom/1, unquote(parts)))
|
||||
?a -> quote(do: :lists.map(&String.to_unsafe_atom/1, unquote(parts)))
|
||||
?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts)))
|
||||
end
|
||||
end
|
||||
@@ -7051,7 +7056,7 @@ defmodule Kernel do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, #{exp} is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
||||
@@ -49,7 +49,7 @@ defmodule Kernel.CLI do
|
||||
|
||||
@doc """
|
||||
Runs the given function by catching any failure
|
||||
and printing them to stdout. `at_exit` hooks are
|
||||
and printing them to stderr. `at_exit` hooks are
|
||||
also invoked before exiting.
|
||||
|
||||
This function is used by Elixir's CLI and also
|
||||
@@ -99,9 +99,14 @@ defmodule Kernel.CLI do
|
||||
Shared helper for error formatting on CLI tools.
|
||||
"""
|
||||
def format_error(kind, reason, stacktrace) do
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
[banner, rest]
|
||||
end
|
||||
|
||||
defp format_error_parts(kind, reason, stacktrace) do
|
||||
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
|
||||
|
||||
iodata =
|
||||
banner =
|
||||
case blamed do
|
||||
%FunctionClauseError{} ->
|
||||
formatted = Exception.format_banner(kind, reason, stacktrace)
|
||||
@@ -112,7 +117,7 @@ defmodule Kernel.CLI do
|
||||
Exception.format_banner(kind, blamed, stacktrace)
|
||||
end
|
||||
|
||||
[iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
|
||||
{banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -179,7 +184,8 @@ defmodule Kernel.CLI do
|
||||
## Error handling
|
||||
|
||||
defp print_error(kind, reason, stacktrace) do
|
||||
IO.write(:stderr, format_error(kind, reason, stacktrace))
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest])
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
|
||||
@@ -200,7 +206,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++
|
||||
[:elixir_def, :elixir_dispatch, :elixir_expand, :elixir_lexical] ++
|
||||
[:elixir_dispatch, :elixir_expand, :elixir_lexical] ++
|
||||
[:elixir_map, :elixir_module] ++
|
||||
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
|
||||
[Kernel.ErrorHandler, Module.ParallelChecker]
|
||||
@@ -427,7 +433,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do
|
||||
node = List.to_atom(node)
|
||||
node = List.to_unsafe_atom(node)
|
||||
|
||||
# Explicitly connect the node in case the rpc node was started with --sname/--name undefined.
|
||||
_ = :net_kernel.connect_node(node)
|
||||
|
||||
@@ -23,7 +23,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate a require
|
||||
must be warned if unused.
|
||||
that must be warned if unused.
|
||||
"""
|
||||
def warn_require(pid, meta, module, alias) do
|
||||
:gen_server.cast(pid, {:warn_require, module, meta, alias})
|
||||
@@ -32,7 +32,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an alias
|
||||
must be warned if unused.
|
||||
that must be warned if unused.
|
||||
"""
|
||||
def warn_alias(pid, meta, alias, module) do
|
||||
:gen_server.cast(pid, {:warn_alias, alias, meta})
|
||||
@@ -41,7 +41,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an import
|
||||
must be warned if unused.
|
||||
that must be warned if unused.
|
||||
"""
|
||||
def warn_import(pid, module) do
|
||||
:gen_server.cast(pid, {:warn_import, module})
|
||||
|
||||
@@ -25,8 +25,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
|
||||
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
|
||||
each_module: (Path.t(), module(), binary() -> term()),
|
||||
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
each_cycle: (-> {:compile, [Path.t()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [{module(), Path.t()}], [Code.diagnostic(:warning)]}),
|
||||
long_compilation_threshold: pos_integer(),
|
||||
long_verification_threshold: pos_integer(),
|
||||
verification: boolean(),
|
||||
@@ -100,6 +100,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
This function allows a developer to perform such tasks.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@spec pmap(Enumerable.t(input), (input -> output)) :: [output]
|
||||
when input: term, output: term
|
||||
def pmap(collection, fun) when is_function(fun, 1) do
|
||||
ref = make_ref()
|
||||
|
||||
@@ -171,25 +173,25 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:each_long_verification` (since v1.19.0) - for each file that takes more
|
||||
than a given timeout (see the `:long_verification_threshold` option) to
|
||||
compile, invoke this callback passing the module as its argument (and
|
||||
verify, invoke this callback passing the module as its argument (and
|
||||
optionally the PID of the process verifying the module)
|
||||
|
||||
* `:each_module` - for each module compiled, invokes the callback passing
|
||||
the file, module and the module bytecode
|
||||
|
||||
* `:each_cycle` - after the given files are compiled, invokes this function
|
||||
that should return the following values:
|
||||
* `:each_cycle` - invoked after each compilation cycle and should return one
|
||||
of the following values:
|
||||
* `{:compile, modules, warnings}` - to continue compilation with a list of
|
||||
further modules to compile
|
||||
further module files to compile
|
||||
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
|
||||
of modules because dependent modules have changed
|
||||
of `{module, path}` pairs because dependent modules have changed
|
||||
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) to check for files
|
||||
taking too long to compile. For each file that exceeds the threshold, the
|
||||
`:each_long_compilation` callback is invoked. Defaults to `10` seconds.
|
||||
|
||||
* `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to
|
||||
check for modules taking too long to compile. For each module that exceeds the
|
||||
check for modules taking too long to verify. For each module that exceeds the
|
||||
threshold, the `:each_long_verification` callback is invoked. Defaults to
|
||||
`10` seconds.
|
||||
|
||||
@@ -212,7 +214,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
This option must be set to true, except for backward compatibility reasons.
|
||||
|
||||
* `:max_concurrency` - the maximum number of files to compile in parallel.
|
||||
Setting this option to 1 will compile files sequentially.
|
||||
@@ -269,7 +271,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
This option must be set to true, except for backward compatibility reasons.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
|
||||
@@ -768,8 +768,8 @@ defmodule Kernel.SpecialForms do
|
||||
It is used in typespecs to specify the type of a variable,
|
||||
function or of a type itself:
|
||||
|
||||
@type number :: integer | float
|
||||
@spec add(number, number) :: number
|
||||
@type num :: integer | float
|
||||
@spec add(num, num) :: num
|
||||
|
||||
It may also be used in bit strings to specify the type
|
||||
of a given bit segment:
|
||||
@@ -1333,9 +1333,9 @@ defmodule Kernel.SpecialForms do
|
||||
sum(1, value, 3)
|
||||
end
|
||||
|
||||
|
||||
Which the argument for the `:sum` function call is not the
|
||||
expected result:
|
||||
However, the code above does not work as expected, because this
|
||||
injects the representation of the `value` variable,
|
||||
not its contents:
|
||||
|
||||
{:sum, [], [1, {:value, [], Elixir}, 3]}
|
||||
|
||||
@@ -2077,9 +2077,9 @@ defmodule Kernel.SpecialForms do
|
||||
iex> try do
|
||||
...> 1 / 0
|
||||
...> rescue
|
||||
...> x in [ArithmeticError] -> [:rescued, is_exception(x)]
|
||||
...> x in [ArithmeticError] -> {:rescued, Exception.message(x)}
|
||||
...> end
|
||||
[:rescued, true]
|
||||
{:rescued, "bad argument in arithmetic expression"}
|
||||
|
||||
Rescue different errors with separate clauses:
|
||||
|
||||
|
||||
@@ -686,6 +686,13 @@ defmodule Kernel.Typespec do
|
||||
{{:type, location(meta), :range, [left, right]}, state}
|
||||
end
|
||||
|
||||
defp typespec({:..//, _meta, [_first, _last, _step]} = range, _vars, caller, _state) do
|
||||
compile_error(
|
||||
caller,
|
||||
"ranges with steps are not supported in typespecs, got: #{Macro.to_string(range)}"
|
||||
)
|
||||
end
|
||||
|
||||
# Handle special forms
|
||||
defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do
|
||||
typespec(caller.module, vars, caller, state)
|
||||
@@ -860,29 +867,29 @@ defmodule Kernel.Typespec do
|
||||
{{:type, location(meta), :nonempty_string, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
defp typespec({type, meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
if type == :char_list do
|
||||
warning = "the char_list() type is deprecated, use charlist()"
|
||||
IO.warn(warning, caller)
|
||||
end
|
||||
|
||||
typespec(quote(do: :elixir.charlist()), vars, caller, state)
|
||||
remote_typespec(:charlist, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state)
|
||||
defp typespec({:nonempty_charlist, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:nonempty_charlist, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:struct, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.struct()), vars, caller, state)
|
||||
defp typespec({:struct, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:struct, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state)
|
||||
defp typespec({:as_boolean, meta, [arg]}, vars, caller, state) do
|
||||
remote_typespec(:as_boolean, meta, [arg], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state)
|
||||
defp typespec({:keyword, meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
remote_typespec(:keyword, meta, args, vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:fun, meta, args}, vars, caller, state) do
|
||||
@@ -1007,6 +1014,10 @@ defmodule Kernel.Typespec do
|
||||
{{:remote_type, location(meta), [remote, name, args]}, state}
|
||||
end
|
||||
|
||||
defp remote_typespec(name, meta, args, vars, caller, state) do
|
||||
typespec({{:., meta, [:elixir, name]}, meta, args}, vars, caller, state)
|
||||
end
|
||||
|
||||
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
|
||||
defp collect_union(v), do: [v]
|
||||
|
||||
|
||||
@@ -162,16 +162,8 @@ defmodule Kernel.Utils do
|
||||
# TODO: Make it raise on v2.0
|
||||
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
|
||||
|
||||
foreach = fn
|
||||
key when is_atom(key) ->
|
||||
:ok
|
||||
|
||||
key ->
|
||||
raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}"
|
||||
end
|
||||
|
||||
:lists.foreach(foreach, enforce_keys)
|
||||
struct = :maps.from_list([__struct__: module] ++ fields)
|
||||
field_map = :maps.from_list(fields)
|
||||
struct = :maps.put(:__struct__, module, field_map)
|
||||
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
|
||||
|
||||
body =
|
||||
@@ -215,7 +207,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
end
|
||||
|
||||
case enforce_keys -- :maps.keys(struct) do
|
||||
case enforce_keys -- :maps.keys(field_map) do
|
||||
[] ->
|
||||
mapper = fn {key, val} ->
|
||||
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
|
||||
@@ -225,10 +217,9 @@ defmodule Kernel.Utils do
|
||||
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
|
||||
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
|
||||
|
||||
error_keys ->
|
||||
invalid_keys ->
|
||||
raise ArgumentError,
|
||||
"@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||||
"#{inspect(fields)}"
|
||||
"unknown or duplicate keys given to @enforce_keys, got: #{inspect(invalid_keys)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -329,21 +320,27 @@ defmodule Kernel.Utils do
|
||||
|
||||
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
|
||||
def defguard(args, expr, env) do
|
||||
{^args, vars} = extract_refs_from_args(args)
|
||||
env = :elixir_env.with_vars(%{env | context: :guard}, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
{_, vars} = extract_refs_from_args(args)
|
||||
guard_expr = expand_defguard(expr, %{env | context: :guard}, vars)
|
||||
body_expr = expand_defguard(expr, %{env | context: nil}, vars)
|
||||
|
||||
quote do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
unquote(literal_quote(unquote_every_ref(expr, vars), []))
|
||||
unquote(literal_quote(unquote_every_ref(guard_expr, vars), []))
|
||||
|
||||
false ->
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
|
||||
unquote(literal_quote(unquote_refs_once(body_expr, vars, env), generated: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_defguard(expr, env, vars) do
|
||||
env = :elixir_env.with_vars(env, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
expr
|
||||
end
|
||||
|
||||
defp extract_refs_from_args(args) do
|
||||
Macro.postwalk(args, [], fn
|
||||
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
|
||||
@@ -382,7 +379,7 @@ defmodule Kernel.Utils do
|
||||
{new_var, acc}
|
||||
|
||||
%{} ->
|
||||
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
generated = String.to_unsafe_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
new_var = Macro.unique_var(generated, module)
|
||||
{new_var, Map.put(acc, pair, {new_var, var})}
|
||||
end
|
||||
|
||||
+58
-35
@@ -38,7 +38,7 @@ defmodule Keyword do
|
||||
|
||||
## Duplicate keys and ordering
|
||||
|
||||
A keyword may have duplicate keys so it is not strictly a key-value
|
||||
A keyword list may have duplicate keys so it is not strictly a key-value
|
||||
data type. However, most of the functions in this module work on a
|
||||
key-value structure and behave similar to the functions you would
|
||||
find in the `Map` module. For example, `Keyword.get/3` will get the first
|
||||
@@ -269,38 +269,60 @@ defmodule Keyword do
|
||||
@doc since: "1.13.0"
|
||||
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
|
||||
{:ok, keyword()} | {:error, [atom]}
|
||||
def validate([], values) when is_list(values), do: {:ok, move_pairs!(values, [])}
|
||||
|
||||
def validate(keyword, values) when is_list(keyword) and is_list(values) do
|
||||
validate(keyword, values, [], keyword, [])
|
||||
validate_merge(keyword, values, [], keyword)
|
||||
end
|
||||
|
||||
defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
|
||||
case find_key!(key, values1, values2) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
defp validate_merge([], values, values_pre, original),
|
||||
do: {:ok, move_pairs!(values, move_pairs!(values_pre, original))}
|
||||
|
||||
:error ->
|
||||
case find_key!(key, values2, values1) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate(keyword, values1, values2, original, [key | bad_keys])
|
||||
end
|
||||
defp validate_merge([{key, _} = pair | keyword], [head | tail], values_pre, original)
|
||||
when is_atom(key) do
|
||||
case head do
|
||||
^key -> validate_merge(keyword, tail, values_pre, original)
|
||||
{^key, _} -> validate_merge(keyword, tail, values_pre, original)
|
||||
_ -> validate_merge([pair | keyword], tail, [head | values_pre], original)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], values1, values2, original, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
|
||||
defp validate_merge([{key, _} | keyword], [], values_pre, original) when is_atom(key) do
|
||||
case find_key!(key, values_pre, []) do
|
||||
{new_values, new_values_pre} ->
|
||||
validate_merge(keyword, new_values, new_values_pre, original)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values_pre, [key])
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], _values1, _values2, _original, bad_keys) do
|
||||
{:error, bad_keys}
|
||||
defp validate_merge([pair | _], _, _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
)
|
||||
|
||||
defp validate_fallback([{key, _} | keyword], values, bad_keys)
|
||||
when is_atom(key) do
|
||||
case find_key!(key, values, []) do
|
||||
{rest, acc} ->
|
||||
validate_fallback(keyword, rest ++ acc, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values, [key | bad_keys])
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([pair | _], _values1, _values2, _original, []) do
|
||||
raise ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
end
|
||||
defp validate_fallback([], _, bad), do: {:error, bad}
|
||||
|
||||
defp validate_fallback([p | _], _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(p)}"
|
||||
)
|
||||
|
||||
defp find_key!(key, [key | rest], acc), do: {rest, acc}
|
||||
defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc}
|
||||
@@ -495,7 +517,7 @@ defmodule Keyword do
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{current, :lists.reverse(acc, t)}
|
||||
{current, :lists.reverse(acc, delete(t, key))}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
@@ -561,7 +583,7 @@ defmodule Keyword do
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{value, :lists.reverse(acc, t)}
|
||||
{value, :lists.reverse(acc, delete(t, key))}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
@@ -1301,7 +1323,8 @@ defmodule Keyword do
|
||||
@doc """
|
||||
Drops the given `keys` from the keyword list.
|
||||
|
||||
Removes duplicate keys from the new keyword list.
|
||||
If a key occurs multiple times, all of its entries are removed when that key
|
||||
is included in `keys`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1342,9 +1365,9 @@ defmodule Keyword do
|
||||
"""
|
||||
@spec pop(t, key, default) :: {value | default, t}
|
||||
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {default, keywords}
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {default, keywords}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1369,9 +1392,9 @@ defmodule Keyword do
|
||||
@doc since: "1.10.0"
|
||||
@spec pop!(t, key) :: {value, t}
|
||||
def pop!(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> raise KeyError, key: key, term: keywords
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> raise KeyError, key: key, term: keywords
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1411,7 +1434,7 @@ defmodule Keyword do
|
||||
do: {values, acc}
|
||||
|
||||
@doc """
|
||||
Lazily returns and removes all values associated with `key` in the keyword list.
|
||||
Lazily returns the first value for `key` and removes all associated entries in the keyword list.
|
||||
|
||||
This is useful if the default value is very expensive to calculate or
|
||||
generally difficult to set up and tear down again.
|
||||
@@ -1434,9 +1457,9 @@ defmodule Keyword do
|
||||
@spec pop_lazy(t, key, (-> value)) :: {value, t}
|
||||
def pop_lazy(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {fun.(), keywords}
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {fun.(), keywords}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+103
-44
@@ -102,7 +102,7 @@ defmodule List do
|
||||
Even though the representation changed, the raw data does remain a list of
|
||||
integers, which can be handled as such:
|
||||
|
||||
iex> inspect(~c"abc", charlists: :as_list)
|
||||
iex> inspect(~c"abc", charlists: :as_lists)
|
||||
"[97, 98, 99]"
|
||||
iex> Enum.map(~c"abc", fn num -> 1000 + num end)
|
||||
[1097, 1098, 1099]
|
||||
@@ -311,9 +311,6 @@ defmodule List do
|
||||
iex> List.first!([1, 2, 3])
|
||||
1
|
||||
|
||||
iex> List.first!([])
|
||||
** (ArgumentError) attempted to get the first element of an empty list
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec first!([elem, ...]) :: elem when elem: var
|
||||
@@ -366,9 +363,6 @@ defmodule List do
|
||||
iex> List.last!([1, 2, 3])
|
||||
3
|
||||
|
||||
iex> List.last!([])
|
||||
** (ArgumentError) attempted to get the last element of an empty list
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec last!([elem, ...]) :: elem when elem: var
|
||||
@@ -435,7 +429,7 @@ defmodule List do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec keyfind!([tuple], any, non_neg_integer) :: any
|
||||
@spec keyfind!([tuple], any, non_neg_integer) :: tuple
|
||||
def keyfind!(list, key, position) when is_integer(position) do
|
||||
:lists.keyfind(key, position + 1, list) ||
|
||||
raise KeyError,
|
||||
@@ -520,8 +514,8 @@ defmodule List do
|
||||
|
||||
As in `Enum.sort/2`, avoid using the default sorting function to sort
|
||||
structs, as by default it performs structural comparison instead of a
|
||||
semantic one. In such cases, you shall pass a sorting function as third
|
||||
element or any module that implements a `compare/2` function. For example,
|
||||
semantic one. In such cases, you shall pass a sorting function as the third
|
||||
argument or any module that implements a `compare/2` function. For example,
|
||||
if you have tuples with user names and their birthday, and you want to
|
||||
sort on their birthday, in both ascending and descending order, you should
|
||||
do:
|
||||
@@ -661,7 +655,7 @@ defmodule List do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps `term` in a list if this is not list.
|
||||
Wraps `term` in a list if it is not a list.
|
||||
|
||||
If `term` is already a list, it returns the list.
|
||||
If `term` is `nil`, it returns an empty list.
|
||||
@@ -960,9 +954,12 @@ defmodule List do
|
||||
@spec pop_at(list, integer, any) :: {any, list}
|
||||
def pop_at(list, index, default \\ nil) when is_integer(index) do
|
||||
if index < 0 do
|
||||
do_pop_at(list, length(list) + index, default, [])
|
||||
case length(list) + index do
|
||||
index when index < 0 -> {default, list}
|
||||
index -> do_pop_at(list, index, default, [], list)
|
||||
end
|
||||
else
|
||||
do_pop_at(list, index, default, [])
|
||||
do_pop_at(list, index, default, [], list)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1024,31 +1021,51 @@ defmodule List do
|
||||
:lists.suffix(suffix, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an atom.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_atom(~c"Elixir")
|
||||
:Elixir
|
||||
|
||||
iex> List.to_atom(~c"🌢 Elixir")
|
||||
:"🌢 Elixir"
|
||||
|
||||
"""
|
||||
@spec to_atom(charlist) :: atom
|
||||
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
|
||||
def to_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an existing atom or creates a new one.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point.
|
||||
|
||||
> #### Dynamic Atom Creation {: .warning}
|
||||
>
|
||||
> This function creates atoms dynamically and atoms are
|
||||
> not garbage-collected. Therefore, `charlist` should not be an
|
||||
> untrusted value, such as input received from a socket or during
|
||||
> a web request. Consider using `to_existing_atom/1` instead.
|
||||
|
||||
By default, the maximum number of atoms is `1_048_576`. This limit
|
||||
can be raised or lowered using the VM option `+t`.
|
||||
|
||||
The maximum atom size is 255 Unicode code points.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_unsafe_atom(~c"Elixir")
|
||||
:Elixir
|
||||
|
||||
iex> List.to_unsafe_atom(~c"🌢 Elixir")
|
||||
:"🌢 Elixir"
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_unsafe_atom(charlist) :: atom
|
||||
def to_unsafe_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an existing atom.
|
||||
|
||||
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point. Raises an `ArgumentError` if the atom does not exist.
|
||||
|
||||
@@ -1079,6 +1096,47 @@ defmodule List do
|
||||
:erlang.list_to_existing_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to one of the `allowed_atoms` or raises.
|
||||
|
||||
Raises an `ArgumentError` if the atom either does not exist or is not within
|
||||
the existing list.
|
||||
|
||||
This should be preferred to `to_existing_atom/1` if the list is known upfront,
|
||||
since there is no risk that the atom has not been loaded.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_existing_atom(~c"foo", [:foo, :bar])
|
||||
:foo
|
||||
|
||||
iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
|
||||
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
|
||||
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
|
||||
atom = :erlang.list_to_existing_atom(charlist)
|
||||
|
||||
if atom not in allowed_atoms do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
atom
|
||||
end
|
||||
|
||||
# used just to have a less cryptic stacktrace and consistent error
|
||||
@doc false
|
||||
def __to_existing_atom__(charlist, allowed_atoms) do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
defp to_existing_atom_unexpected(charlist, allowed_atoms) do
|
||||
raise ArgumentError,
|
||||
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the float whose text representation is `charlist`.
|
||||
|
||||
@@ -1090,7 +1148,7 @@ defmodule List do
|
||||
2.2017764
|
||||
|
||||
"""
|
||||
@spec to_float(charlist) :: float
|
||||
@spec to_float(nonempty_charlist) :: float
|
||||
def to_float(charlist) do
|
||||
:erlang.list_to_float(charlist)
|
||||
end
|
||||
@@ -1106,7 +1164,7 @@ defmodule List do
|
||||
123
|
||||
|
||||
"""
|
||||
@spec to_integer(charlist) :: integer
|
||||
@spec to_integer(nonempty_charlist) :: integer
|
||||
def to_integer(charlist) do
|
||||
:erlang.list_to_integer(charlist)
|
||||
end
|
||||
@@ -1124,7 +1182,7 @@ defmodule List do
|
||||
1023
|
||||
|
||||
"""
|
||||
@spec to_integer(charlist, 2..36) :: integer
|
||||
@spec to_integer(nonempty_charlist, 2..36) :: integer
|
||||
def to_integer(charlist, base) do
|
||||
:erlang.list_to_integer(charlist, base)
|
||||
end
|
||||
@@ -1193,7 +1251,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 +1303,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) ->
|
||||
@@ -1310,13 +1368,13 @@ defmodule List do
|
||||
|
||||
defp myers_difference_with_diff_script(list1, list2, diff_script) do
|
||||
path = {0, list1, list2, []}
|
||||
find_script(0, length(list1) + length(list2), [path], diff_script)
|
||||
find_script(0, [path], diff_script)
|
||||
end
|
||||
|
||||
defp find_script(envelope, max, paths, diff_script) do
|
||||
defp find_script(envelope, paths, diff_script) do
|
||||
case each_diagonal(-envelope, envelope, paths, [], diff_script) do
|
||||
{:done, edits} -> compact_reverse(edits, [])
|
||||
{:next, paths} -> find_script(envelope + 1, max, paths, diff_script)
|
||||
{:next, paths} -> find_script(envelope + 1, paths, diff_script)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1461,15 +1519,16 @@ defmodule List do
|
||||
|
||||
# pop_at
|
||||
|
||||
defp do_pop_at([], _index, default, acc) do
|
||||
{default, :lists.reverse(acc)}
|
||||
# The original list is returned when the index is out of bounds
|
||||
defp do_pop_at([], _index, default, _acc, original) do
|
||||
{default, original}
|
||||
end
|
||||
|
||||
defp do_pop_at([head | tail], 0, _default, acc) do
|
||||
defp do_pop_at([head | tail], 0, _default, acc, _original) do
|
||||
{head, :lists.reverse(acc, tail)}
|
||||
end
|
||||
|
||||
defp do_pop_at([head | tail], index, default, acc) do
|
||||
do_pop_at(tail, index - 1, default, [head | acc])
|
||||
defp do_pop_at([head | tail], index, default, acc, original) do
|
||||
do_pop_at(tail, index - 1, default, [head | acc], original)
|
||||
end
|
||||
end
|
||||
|
||||
+152
-60
@@ -166,7 +166,7 @@ defmodule Macro do
|
||||
of a `__block__` or the right side of `->`. The last expression of the
|
||||
block does not have metadata if it is not followed by an end of line
|
||||
character (either a newline or `;`). This entry may appear multiple times
|
||||
in the same metadata if the expression is surround by parens
|
||||
in the same metadata if the expression is surrounded by parens
|
||||
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword.
|
||||
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
|
||||
@@ -235,7 +235,7 @@ defmodule Macro do
|
||||
`div/2` function, so that the AST for that function will become `{:div, [],
|
||||
[100, 5]}` (`div(100, 5)`).
|
||||
"""
|
||||
@spec unpipe(t()) :: [t()]
|
||||
@spec unpipe(t()) :: [{t(), non_neg_integer}]
|
||||
def unpipe(expr) do
|
||||
:lists.reverse(unpipe(expr, []))
|
||||
end
|
||||
@@ -518,7 +518,7 @@ defmodule Macro do
|
||||
@doc since: "1.11.3"
|
||||
@spec generate_unique_arguments(0, context :: atom) :: []
|
||||
@spec generate_unique_arguments(pos_integer, context) ::
|
||||
[{atom, [counter: integer], context}, ...]
|
||||
[{atom, metadata(), context}, ...]
|
||||
when context: atom
|
||||
def generate_unique_arguments(amount, context),
|
||||
do: generate_arguments(amount, context, &unique_var/2)
|
||||
@@ -527,7 +527,7 @@ defmodule Macro do
|
||||
|
||||
defp generate_arguments(amount, context, fun)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
for id <- 1..amount, do: fun.(String.to_unsafe_atom("arg" <> Integer.to_string(id)), context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -576,7 +576,7 @@ defmodule Macro do
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec unique_var(var, context) :: {var, [counter: integer], context}
|
||||
@spec unique_var(var, context) :: {var, metadata(), context}
|
||||
when var: atom, context: atom
|
||||
def unique_var(var, context) when is_atom(var) and is_atom(context) do
|
||||
{var, [counter: :elixir_module.next_counter(context)], context}
|
||||
@@ -690,7 +690,39 @@ defmodule Macro do
|
||||
"""
|
||||
@spec prewalk(t, (t -> t)) :: t
|
||||
def prewalk(ast, fun) when is_function(fun, 1) do
|
||||
elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
do_prewalk(fun.(ast), fun)
|
||||
end
|
||||
|
||||
# Mirrors do_traverse/4 with an always-pre fun and no accumulator,
|
||||
# avoiding the wrapper closures and tuple threading of traverse/4.
|
||||
# Each clause dispatches on the already-transformed node, so fun's
|
||||
# rewrites are descended into, exactly as in traverse/4.
|
||||
defp do_prewalk({form, meta, args}, fun) when is_atom(form) do
|
||||
{form, meta, do_prewalk_args(args, fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk({form, meta, args}, fun) do
|
||||
form = do_prewalk(fun.(form), fun)
|
||||
{form, meta, do_prewalk_args(args, fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk({left, right}, fun) do
|
||||
left = do_prewalk(fun.(left), fun)
|
||||
{left, do_prewalk(fun.(right), fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk(list, fun) when is_list(list) do
|
||||
do_prewalk_args(list, fun)
|
||||
end
|
||||
|
||||
defp do_prewalk(x, _fun) do
|
||||
x
|
||||
end
|
||||
|
||||
defp do_prewalk_args(args, _fun) when is_atom(args), do: args
|
||||
|
||||
defp do_prewalk_args(args, fun) when is_list(args) do
|
||||
:lists.map(fn x -> do_prewalk(fun.(x), fun) end, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -728,11 +760,41 @@ defmodule Macro do
|
||||
"""
|
||||
@spec postwalk(t, (t -> t)) :: t
|
||||
def postwalk(ast, fun) when is_function(fun, 1) do
|
||||
elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
do_postwalk(ast, fun)
|
||||
end
|
||||
|
||||
# Mirrors do_traverse/4 with an always-post fun and no accumulator,
|
||||
# avoiding the wrapper closures and tuple threading of traverse/4
|
||||
defp do_postwalk({form, meta, args}, fun) when is_atom(form) do
|
||||
fun.({form, meta, do_postwalk_args(args, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk({form, meta, args}, fun) do
|
||||
form = do_postwalk(form, fun)
|
||||
fun.({form, meta, do_postwalk_args(args, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk({left, right}, fun) do
|
||||
left = do_postwalk(left, fun)
|
||||
fun.({left, do_postwalk(right, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk(list, fun) when is_list(list) do
|
||||
fun.(do_postwalk_args(list, fun))
|
||||
end
|
||||
|
||||
defp do_postwalk(x, fun) do
|
||||
fun.(x)
|
||||
end
|
||||
|
||||
defp do_postwalk_args(args, _fun) when is_atom(args), do: args
|
||||
|
||||
defp do_postwalk_args(args, fun) when is_list(args) do
|
||||
:lists.map(fn x -> do_postwalk(x, fun) end, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This functions behaves like `prewalk/3`, but performs a depth-first,
|
||||
This function behaves like `prewalk/3`, but performs a depth-first,
|
||||
post-order traversal of quoted expressions using an accumulator.
|
||||
"""
|
||||
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
|
||||
@@ -940,7 +1002,7 @@ defmodule Macro do
|
||||
|
||||
This is useful when a struct needs to be expanded at
|
||||
compilation time and the struct being expanded may or may
|
||||
not have been compiled (including structs in the defined
|
||||
not have been compiled (including structs defined
|
||||
under the module being compiled). For compiled modules,
|
||||
it will invoke `module.__info__(:struct)`.
|
||||
|
||||
@@ -1034,7 +1096,7 @@ defmodule Macro do
|
||||
defp find_invalid(other), do: {:error, other}
|
||||
|
||||
@doc """
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
pre-order traversal.
|
||||
|
||||
## Examples
|
||||
@@ -1092,7 +1154,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
post-order traversal.
|
||||
|
||||
## Examples
|
||||
@@ -1195,14 +1257,15 @@ defmodule Macro do
|
||||
The mapping function receives an integer representing the code point
|
||||
of the character it wants to unescape. There are also the special atoms
|
||||
`:newline`, `:unicode`, and `:hex`, which control newline, unicode,
|
||||
and escaping respectively.
|
||||
and escaping respectively, and for which the mapping function must return
|
||||
a boolean.
|
||||
|
||||
Here is the default mapping function implemented by Elixir:
|
||||
|
||||
def unescape_map(:newline), do: true
|
||||
def unescape_map(:unicode), do: true
|
||||
def unescape_map(:hex), do: true
|
||||
def unescape_map(?0), do: ?0
|
||||
def unescape_map(?0), do: 0
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(?b), do: ?\b
|
||||
def unescape_map(?d), do: ?\d
|
||||
@@ -1225,7 +1288,11 @@ defmodule Macro do
|
||||
Macro.unescape_string("example\\n", &unescape_map(&1))
|
||||
|
||||
"""
|
||||
@spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
|
||||
@spec unescape_string(
|
||||
String.t(),
|
||||
(non_neg_integer | :newline | :unicode | :hex ->
|
||||
non_neg_integer | boolean)
|
||||
) :: String.t()
|
||||
def unescape_string(string, map) do
|
||||
:elixir_interpolation.unescape_string(string, map)
|
||||
end
|
||||
@@ -1740,9 +1807,9 @@ defmodule Macro do
|
||||
|
||||
defp kw_blocks_to_string(kw, fun) do
|
||||
Enum.reduce(unquote(kw_keywords), " ", fn x, acc ->
|
||||
case Keyword.has_key?(kw, x) do
|
||||
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
|
||||
false -> acc
|
||||
case Keyword.fetch(kw, x) do
|
||||
{:ok, value} -> acc <> kw_block_to_string(x, value, fun)
|
||||
:error -> acc
|
||||
end
|
||||
end) <> "end"
|
||||
end
|
||||
@@ -1856,6 +1923,7 @@ defmodule Macro do
|
||||
definition compile-time, and you can use `Macro.expand/2`.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@spec compile_apply(module(), atom(), [term()], Macro.Env.t()) :: term()
|
||||
def compile_apply(mod, fun, args, caller) do
|
||||
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
|
||||
Kernel.apply(mod, fun, args)
|
||||
@@ -1980,10 +2048,9 @@ defmodule Macro do
|
||||
|
||||
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
|
||||
when is_atom(atom) and is_atom(field) and env.context != :match do
|
||||
if Map.has_key?(env, field) do
|
||||
{maybe_escape_map(Map.get(env, field)), true}
|
||||
else
|
||||
{original, false}
|
||||
case Map.fetch(env, field) do
|
||||
{:ok, value} -> {maybe_escape_map(value), true}
|
||||
:error -> {original, false}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2121,6 +2188,9 @@ defmodule Macro do
|
||||
def quoted_literal?({:__aliases__, _, args}),
|
||||
do: quoted_literal?(args)
|
||||
|
||||
def quoted_literal?({:__block__, _, [wrapped]}),
|
||||
do: quoted_literal?(wrapped)
|
||||
|
||||
def quoted_literal?({:%, _, [left, right]}),
|
||||
do: quoted_literal?(left) and quoted_literal?(right)
|
||||
|
||||
@@ -2500,7 +2570,7 @@ defmodule Macro do
|
||||
|
||||
### As a remote call
|
||||
|
||||
Inspect an atom the function name of a remote call.
|
||||
Inspect an atom as the function name of a remote call.
|
||||
|
||||
iex> Macro.inspect_atom(:remote_call, :foo)
|
||||
"foo"
|
||||
@@ -2621,55 +2691,77 @@ defmodule Macro do
|
||||
:unquoted_operator
|
||||
|
||||
true ->
|
||||
charlist = Atom.to_charlist(atom)
|
||||
|
||||
if valid_alias?(charlist) do
|
||||
:alias
|
||||
else
|
||||
case :elixir_config.identifier_tokenizer().tokenize(charlist) do
|
||||
{kind, _acc, [], _, _, special} ->
|
||||
cond do
|
||||
kind != :identifier or :lists.member(:at, special) ->
|
||||
:not_callable
|
||||
|
||||
# identifier_tokenizer used to return errors for non-nfc, but
|
||||
# now it nfc-normalizes everything. However, lack of nfc is
|
||||
# still a good reason to quote an atom when printing.
|
||||
:lists.member(:nfkc, special) ->
|
||||
:other
|
||||
|
||||
true ->
|
||||
:identifier
|
||||
end
|
||||
|
||||
_ ->
|
||||
:other
|
||||
end
|
||||
end
|
||||
classify_binary(Atom.to_string(atom), atom)
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
|
||||
defp valid_alias?(_other), do: false
|
||||
# ASCII identifiers and aliases are recognized on the binary to avoid building a
|
||||
# charlist and running the (unicode aware) tokenizer, which dominates the cost of
|
||||
# classifying keyword list, map and struct keys.
|
||||
defp classify_binary(<<char, rest::binary>>, atom)
|
||||
when char >= ?a and char <= ?z
|
||||
when char == ?_ do
|
||||
if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom)
|
||||
end
|
||||
|
||||
defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
|
||||
do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
|
||||
defp classify_binary("Elixir" <> rest, atom) do
|
||||
if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom)
|
||||
end
|
||||
|
||||
defp valid_alias_piece?([]), do: true
|
||||
defp valid_alias_piece?(_other), do: false
|
||||
defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom)
|
||||
|
||||
defp trim_leading_while_valid_identifier([char | rest])
|
||||
defp classify_with_tokenizer(atom) do
|
||||
case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do
|
||||
{kind, _acc, [], _, _, special} ->
|
||||
cond do
|
||||
kind != :identifier or :lists.member(:at, special) ->
|
||||
:not_callable
|
||||
|
||||
# identifier_tokenizer used to return errors for non-nfc, but
|
||||
# now it nfc-normalizes everything. However, lack of nfc is
|
||||
# still a good reason to quote an atom when printing.
|
||||
:lists.member(:nfkc, special) ->
|
||||
:other
|
||||
|
||||
true ->
|
||||
:identifier
|
||||
end
|
||||
|
||||
_ ->
|
||||
:other
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_identifier_rest?(<<char, rest::binary>>)
|
||||
when char >= ?a and char <= ?z
|
||||
when char >= ?A and char <= ?Z
|
||||
when char >= ?0 and char <= ?9
|
||||
when char == ?_ do
|
||||
trim_leading_while_valid_identifier(rest)
|
||||
valid_identifier_rest?(rest)
|
||||
end
|
||||
|
||||
defp trim_leading_while_valid_identifier(other) do
|
||||
other
|
||||
defp valid_identifier_rest?(<<char>>) when char == ?? when char == ?!, do: true
|
||||
defp valid_identifier_rest?(<<>>), do: true
|
||||
defp valid_identifier_rest?(_other), do: false
|
||||
|
||||
defp valid_alias_piece?(<<?., char, rest::binary>>) when char >= ?A and char <= ?Z,
|
||||
do: valid_alias_piece_rest?(rest)
|
||||
|
||||
defp valid_alias_piece?(<<>>), do: true
|
||||
defp valid_alias_piece?(_other), do: false
|
||||
|
||||
# A helper returning the rest of the binary would build a sub binary per piece,
|
||||
# so branch back into valid_alias_piece?/1 to keep the match context.
|
||||
defp valid_alias_piece_rest?(<<char, rest::binary>>)
|
||||
when char >= ?a and char <= ?z
|
||||
when char >= ?A and char <= ?Z
|
||||
when char >= ?0 and char <= ?9
|
||||
when char == ?_ do
|
||||
valid_alias_piece_rest?(rest)
|
||||
end
|
||||
|
||||
defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other)
|
||||
|
||||
@doc """
|
||||
Default backend for `Kernel.dbg/2`.
|
||||
|
||||
@@ -2699,7 +2791,7 @@ defmodule Macro do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, dbg is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
@@ -2954,6 +3046,8 @@ defmodule Macro do
|
||||
@doc false
|
||||
def __dbg__(to_debug, header, options) do
|
||||
{print_location?, options} = Keyword.pop(options, :print_location, true)
|
||||
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
|
||||
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
|
||||
ansi_enabled? = options[:syntax_colors] != []
|
||||
|
||||
if print_location? and is_binary(header) do
|
||||
@@ -2961,8 +3055,6 @@ defmodule Macro do
|
||||
:ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?))
|
||||
end
|
||||
|
||||
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
|
||||
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
|
||||
{formatted, result} = dbg_format_ast_to_debug(to_debug, options)
|
||||
:ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?))
|
||||
result
|
||||
|
||||
+12
-12
@@ -203,23 +203,23 @@ defmodule Macro.Env do
|
||||
Returns a keyword list containing the file and line
|
||||
information as keys.
|
||||
"""
|
||||
@spec location(t) :: keyword
|
||||
@spec location(t) :: [file: file, line: line]
|
||||
def location(env)
|
||||
|
||||
def location(%{__struct__: Macro.Env, file: file, line: line}) do
|
||||
[file: file, line: line]
|
||||
end
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
|
||||
when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
|
||||
|
||||
@doc """
|
||||
Returns the modules from which the given `{name, arity}` was
|
||||
@@ -234,7 +234,7 @@ defmodule Macro.Env do
|
||||
> This function does not emit compiler tracing events,
|
||||
> which may block the compiler from correctly tracking
|
||||
> dependencies. Use this function for reflection purposes
|
||||
> but to do not use it to expand imports into qualified
|
||||
> but do not use it to expand imports into qualified
|
||||
> calls. Instead, use `expand_import/5`.
|
||||
|
||||
## Examples
|
||||
@@ -345,7 +345,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.require/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForms.require/2` plus:
|
||||
|
||||
* #{trace_option}
|
||||
|
||||
@@ -386,7 +386,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.import/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForms.import/2` plus:
|
||||
|
||||
* `:emit_warnings` - emit warnings found when defining imports
|
||||
|
||||
@@ -394,7 +394,7 @@ defmodule Macro.Env do
|
||||
|
||||
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
|
||||
The function will be invoked with `:functions` or `:macros` argument.
|
||||
It has to return a list of `{function, arity}` key value pairs.
|
||||
It has to return a list of `{function, arity}` key-value pairs.
|
||||
If it fails, it defaults to using module metadata based on `module_info/1`.
|
||||
|
||||
## Examples
|
||||
@@ -406,7 +406,7 @@ defmodule Macro.Env do
|
||||
iex> Macro.Env.lookup_import(env, {:flatten, 1})
|
||||
[{:function, List}]
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.import/2`:
|
||||
It accepts the same options as `Kernel.SpecialForms.import/2`:
|
||||
|
||||
iex> env = __ENV__
|
||||
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
|
||||
@@ -448,7 +448,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForms.alias/2` plus:
|
||||
|
||||
* #{trace_option}
|
||||
|
||||
@@ -700,7 +700,7 @@ defmodule Macro.Env do
|
||||
@doc """
|
||||
Returns the environment stacktrace.
|
||||
"""
|
||||
@spec stacktrace(t) :: list
|
||||
@spec stacktrace(t) :: [{module, atom, arity, keyword}]
|
||||
def stacktrace(%{__struct__: Macro.Env} = env) do
|
||||
cond do
|
||||
is_nil(env.module) ->
|
||||
|
||||
@@ -201,7 +201,7 @@ defmodule Map do
|
||||
%{}
|
||||
|
||||
"""
|
||||
@spec new :: map
|
||||
@spec new :: %{}
|
||||
def new, do: %{}
|
||||
|
||||
@doc """
|
||||
|
||||
+20
-15
@@ -51,6 +51,15 @@ defmodule MapSet do
|
||||
that they share many properties, including logarithmic time complexity. Erlang
|
||||
`:sets` (version 2) are implemented on top of maps, so see the documentation
|
||||
for `Map` for more information on its execution time complexity.
|
||||
|
||||
> #### Dialyzer opaqueness warnings {: .warning}
|
||||
>
|
||||
> `MapSet` internally relies on the `:sets` module which uses
|
||||
> opaque types. This might cause Dialyzer to report opaqueness violations.
|
||||
> These can be silenced by setting the following module attribute:
|
||||
>
|
||||
> @dialyzer :no_opaque
|
||||
|
||||
"""
|
||||
|
||||
@type value :: term
|
||||
@@ -171,18 +180,14 @@ defmodule MapSet do
|
||||
@doc since: "1.14.0"
|
||||
@spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
|
||||
def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do
|
||||
{small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1}
|
||||
|
||||
disjointer_fun = fn elem, {small, acc} ->
|
||||
if :sets.is_element(elem, small) do
|
||||
{:sets.del_element(elem, small), acc}
|
||||
map =
|
||||
if :sets.is_disjoint(set1, set2) do
|
||||
:sets.union(set1, set2)
|
||||
else
|
||||
{small, [elem | acc]}
|
||||
:sets.union(:sets.subtract(set1, set2), :sets.subtract(set2, set1))
|
||||
end
|
||||
end
|
||||
|
||||
{new_small, list} = :sets.fold(disjointer_fun, {small, []}, large)
|
||||
%{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))}
|
||||
%{map_set1 | map: map}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -205,7 +210,7 @@ defmodule MapSet do
|
||||
Checks if two sets are equal.
|
||||
|
||||
The comparison between elements is done using `===/2`,
|
||||
which a set with `1` is not equivalent to a set with
|
||||
which means a set with `1` is not equivalent to a set with
|
||||
`1.0`.
|
||||
|
||||
## Examples
|
||||
@@ -344,7 +349,7 @@ defmodule MapSet do
|
||||
> If you find yourself doing multiple calls to `MapSet.filter/2`
|
||||
> and `MapSet.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
|
||||
> a map at the end using `MapSet.new/1`.
|
||||
> a set at the end using `MapSet.new/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -400,15 +405,15 @@ defmodule MapSet do
|
||||
iex> while_false
|
||||
MapSet.new([1, 3])
|
||||
|
||||
iex> {while_true, while_false} = MapSet.split_with(MapSet.new(), fn {_k, v} -> v > 50 end)
|
||||
iex> {while_true, while_false} = MapSet.split_with(MapSet.new([10, 20, 60, 70]), fn v -> v > 50 end)
|
||||
iex> while_true
|
||||
MapSet.new([])
|
||||
MapSet.new([60, 70])
|
||||
iex> while_false
|
||||
MapSet.new([])
|
||||
MapSet.new([10, 20])
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
|
||||
@spec split_with(t(a), (a -> as_boolean(term))) :: {t(a), t(a)} when a: value
|
||||
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
|
||||
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
|
||||
{%MapSet{map: while_true}, %MapSet{map: while_false}}
|
||||
|
||||
+12
-10
@@ -190,7 +190,7 @@ defmodule Module do
|
||||
|
||||
* a string (often a heredoc)
|
||||
* `false`, which will make the entity invisible to documentation-extraction
|
||||
tools like [`ExDoc`](https://hexdocs.pm/ex_doc/)
|
||||
tools like [`ExDoc`](https://ex-doc.hexdocs.pm/)
|
||||
* a keyword list, since Elixir 1.7.0
|
||||
|
||||
For example:
|
||||
@@ -216,7 +216,7 @@ defmodule Module do
|
||||
|
||||
As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc`
|
||||
also accept a keyword list that serves as a way to provide arbitrary metadata
|
||||
about the entity. Tools like [`ExDoc`](https://hexdocs.pm/ex_doc/) and
|
||||
about the entity. Tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) and
|
||||
`IEx` may use this information to display annotations. A common use
|
||||
case is the `:since` key, which may be used to annotate in which version the
|
||||
function was introduced.
|
||||
@@ -263,7 +263,7 @@ defmodule Module do
|
||||
|
||||
Tools may use this information to ensure the module is recompiled
|
||||
in case any of the external resources change, see for example:
|
||||
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
|
||||
[`mix compile.elixir`](https://mix.hexdocs.pm/Mix.Tasks.Compile.Elixir.html).
|
||||
|
||||
The specified file path provided is interpreted as relative to
|
||||
the folder containing the project's `mix.exs`, which is the
|
||||
@@ -321,7 +321,7 @@ defmodule Module do
|
||||
|
||||
Accepts a string (often a heredoc) or `false` where `@moduledoc false`
|
||||
will make the module invisible to documentation extraction tools like
|
||||
[`ExDoc`](https://hexdocs.pm/ex_doc/).
|
||||
[`ExDoc`](https://ex-doc.hexdocs.pm/).
|
||||
|
||||
Similarly to `@doc` also accepts a keyword list to provide metadata
|
||||
about the module. For more details, see the documentation of `@doc`
|
||||
@@ -366,7 +366,8 @@ defmodule Module do
|
||||
Unlike other hooks, `@on_definition` will only invoke functions and
|
||||
never macros. This is to avoid `@on_definition` callbacks from
|
||||
redefining functions that have just been defined in favor of more
|
||||
explicit approaches.
|
||||
explicit approaches. They are also invoked in the reverse order of
|
||||
registration.
|
||||
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__on_definition__/6`.
|
||||
@@ -753,7 +754,7 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec reserved_attributes() :: map
|
||||
@spec reserved_attributes() :: %{optional(atom()) => %{doc: binary()}}
|
||||
def reserved_attributes() do
|
||||
%{
|
||||
after_compile: %{
|
||||
@@ -1147,7 +1148,7 @@ defmodule Module do
|
||||
defp simplify_var(var, guess_priority) do
|
||||
case Atom.to_string(var) do
|
||||
"_" -> {:_, [], guess_priority}
|
||||
"_" <> rest -> {String.to_atom(rest), [], guess_priority}
|
||||
"_" <> rest -> {String.to_unsafe_atom(rest), [], guess_priority}
|
||||
_ -> {var, [], nil}
|
||||
end
|
||||
end
|
||||
@@ -1158,7 +1159,7 @@ defmodule Module do
|
||||
rescue
|
||||
ArgumentError -> module
|
||||
else
|
||||
module_name -> String.to_atom(Macro.underscore(List.last(module_name)))
|
||||
module_name -> String.to_unsafe_atom(Macro.underscore(List.last(module_name)))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1191,7 +1192,7 @@ defmodule Module do
|
||||
defp expand_key(key, counters) do
|
||||
case counters do
|
||||
%{^key => count} when is_integer(count) and count >= 1 ->
|
||||
{{:"#{key}#{count}", [], Elixir}, Map.put(counters, key, count - 1)}
|
||||
{{String.to_unsafe_atom("#{key}#{count}"), [], Elixir}, Map.put(counters, key, count - 1)}
|
||||
|
||||
_ ->
|
||||
{{key, [], Elixir}, counters}
|
||||
@@ -1224,7 +1225,8 @@ defmodule Module do
|
||||
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
|
||||
|
||||
# Otherwise, returns a generic guess
|
||||
defp merge_signature({_, meta, _}, _newer, i), do: {:"arg#{i}", meta, Elixir}
|
||||
defp merge_signature({_, meta, _}, _newer, i),
|
||||
do: {String.to_unsafe_atom("arg#{i}"), meta, Elixir}
|
||||
|
||||
@doc """
|
||||
Checks if the module defines the given function or macro.
|
||||
|
||||
@@ -228,11 +228,7 @@ defmodule Module.Behaviour do
|
||||
defp behaviour_defined?(callbacks, behaviour) do
|
||||
callbacks
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.any?(fn
|
||||
{_kind, ^behaviour, _optional?} -> true
|
||||
{_kind, _behaviour, _optional?} -> false
|
||||
end)
|
||||
|> Enum.any?(fn list -> :lists.keymember(behaviour, 2, list) end)
|
||||
end
|
||||
|
||||
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
|
||||
|
||||
@@ -232,7 +232,7 @@ defmodule Module.Types do
|
||||
|
||||
context =
|
||||
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
|
||||
# Optimized version of finder, since we already the definition
|
||||
# Optimized version of finder, since we already have the definition
|
||||
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
|
||||
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
context
|
||||
@@ -251,27 +251,23 @@ defmodule Module.Types do
|
||||
context ->
|
||||
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
|
||||
|
||||
if pending != [] do
|
||||
{used_indexes, unused_indexes} =
|
||||
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
|
||||
{used_indexes, unused_indexes} ->
|
||||
if type_index in pending and not skip_unused_clause?(info, type_index) do
|
||||
{used_indexes, [clause_index | unused_indexes]}
|
||||
else
|
||||
{[clause_index | used_indexes], unused_indexes}
|
||||
end
|
||||
end)
|
||||
|
||||
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
|
||||
|
||||
Enum.reduce(unused_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
{used_indexes, unused_indexes} =
|
||||
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
|
||||
{used_indexes, unused_indexes} ->
|
||||
if type_index in pending and not skip_unused_clause?(info, type_index) do
|
||||
{used_indexes, [clause_index | unused_indexes]}
|
||||
else
|
||||
{[clause_index | used_indexes], unused_indexes}
|
||||
end
|
||||
end)
|
||||
else
|
||||
context
|
||||
end
|
||||
|
||||
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
|
||||
|
||||
Enum.reduce(unused_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -387,7 +383,7 @@ defmodule Module.Types do
|
||||
_ ->
|
||||
inferred
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.opt_union/2) end)
|
||||
end
|
||||
|
||||
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
|
||||
@@ -398,16 +394,15 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do
|
||||
{_, _, _, domain, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
|
||||
{meta, args, guards, body},
|
||||
{index, total, previous, domain, mapping, inferred, acc_context} ->
|
||||
{_, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {Pattern.init_previous(), [], context}, fn
|
||||
{meta, args, guards, body}, {previous, inferred, acc_context} ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
fresh_context = fresh_context(acc_context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
try do
|
||||
{trees, _precise?, head_no_previous_args_types, previous, head_context} =
|
||||
{trees, precise?, head_no_previous_args_types, previous, head_context} =
|
||||
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
|
||||
|
||||
{return_type, context} =
|
||||
@@ -415,42 +410,88 @@ defmodule Module.Types do
|
||||
|
||||
args_types = Pattern.of_domain(trees, stack, context)
|
||||
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, args_types, return_type, total - 1, [])
|
||||
|
||||
domain =
|
||||
case domain do
|
||||
[] ->
|
||||
args_types
|
||||
|
||||
_ ->
|
||||
head_args_types = Pattern.of_domain(trees, stack, head_context)
|
||||
compute_domain(args_types, head_args_types, head_no_previous_args_types, domain)
|
||||
head_args_types =
|
||||
case inferred do
|
||||
[] -> nil
|
||||
_ -> Pattern.of_domain(trees, stack, head_context)
|
||||
end
|
||||
|
||||
if type_index == -1 do
|
||||
mapping = [{index, total} | mapping]
|
||||
{index + 1, total + 1, previous, domain, mapping, inferred, context}
|
||||
else
|
||||
mapping = [{index, type_index} | mapping]
|
||||
{index + 1, total, previous, domain, mapping, inferred, context}
|
||||
end
|
||||
args_triplet = {args_types, head_args_types, head_no_previous_args_types}
|
||||
inferred = [{args_triplet, return_type, precise?} | inferred]
|
||||
{previous, inferred, context}
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
|
||||
end
|
||||
end)
|
||||
|
||||
{clauses_types, mapping, domain} =
|
||||
clauses_types
|
||||
|> Enum.reverse()
|
||||
|> group_clauses()
|
||||
|
||||
domain =
|
||||
case clauses_types do
|
||||
[_] -> nil
|
||||
_ -> domain
|
||||
end
|
||||
|
||||
inferred = {:infer, domain, Enum.reverse(clauses_types)}
|
||||
inferred = {:infer, domain, clauses_types}
|
||||
{inferred, mapping, restore_context(clauses_context, context)}
|
||||
end
|
||||
|
||||
defp group_clauses(clauses) do
|
||||
{_, all_clauses, filtered_clauses, non_empty?} =
|
||||
Enum.reduce(clauses, {0, [], [], false}, fn
|
||||
{_args_triplet, return, precise?} = clause,
|
||||
{index, all_clauses, filtered_clauses, non_empty?} ->
|
||||
empty? = Descr.empty?(return)
|
||||
indexed_clause = {clause, index}
|
||||
|
||||
filtered_clauses =
|
||||
if precise? and empty? do
|
||||
filtered_clauses
|
||||
else
|
||||
[indexed_clause | filtered_clauses]
|
||||
end
|
||||
|
||||
{index + 1, [indexed_clause | all_clauses], filtered_clauses, non_empty? or not empty?}
|
||||
end)
|
||||
|
||||
clauses =
|
||||
if non_empty? do
|
||||
Enum.reverse(filtered_clauses)
|
||||
else
|
||||
Enum.reverse(all_clauses)
|
||||
end
|
||||
|
||||
[
|
||||
{{{args, _head_args, _head_no_previous_args}, _return, _precise?}, _index}
|
||||
| clauses_tail
|
||||
] = clauses
|
||||
|
||||
domain =
|
||||
Enum.reduce(clauses_tail, args, fn
|
||||
{{{args, head_args, head_no_previous_args}, _return, _precise?}, _index}, domain ->
|
||||
compute_domain(args, head_args, head_no_previous_args, domain)
|
||||
end)
|
||||
|
||||
{_, mapping, inferred} =
|
||||
Enum.reduce(clauses, {0, [], []}, fn
|
||||
{{{args, _head_args, _head_no_previous_args}, return, _precise?}, index},
|
||||
{total, mapping, inferred} ->
|
||||
{type_index, inferred} = add_inferred(inferred, args, return, total - 1, [])
|
||||
|
||||
if type_index == -1 do
|
||||
{total + 1, [{index, total} | mapping], inferred}
|
||||
else
|
||||
{total, [{index, type_index} | mapping], inferred}
|
||||
end
|
||||
end)
|
||||
|
||||
{Enum.reverse(inferred), mapping, domain}
|
||||
end
|
||||
|
||||
defp compute_domain(
|
||||
[arg | args_types],
|
||||
[head_arg | head_args_types],
|
||||
@@ -465,7 +506,7 @@ defmodule Module.Types do
|
||||
# the domain is computed by unioning their inferred types. However, their
|
||||
# inferred types often have the different of the previous clauses:
|
||||
#
|
||||
# union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
|
||||
# opt_union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
|
||||
#
|
||||
# Where `rN` represents the refinement in every function body.
|
||||
#
|
||||
@@ -496,9 +537,9 @@ defmodule Module.Types do
|
||||
# Furthermore, the signature used in type checking is not refined in any way,
|
||||
# so type checking is still sound.
|
||||
if arg == head_arg do
|
||||
Descr.union(Descr.upper_bound(no_prev_arg), d)
|
||||
Descr.opt_union(Descr.upper_bound(no_prev_arg), d)
|
||||
else
|
||||
Descr.union(arg, d)
|
||||
Descr.opt_union(arg, d)
|
||||
end
|
||||
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
|
||||
]
|
||||
@@ -509,7 +550,7 @@ defmodule Module.Types do
|
||||
# We check for term equality of types as an optimization
|
||||
# to reduce the amount of check we do at runtime.
|
||||
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])}
|
||||
|
||||
defp add_inferred([head | tail], args, return, index, acc),
|
||||
do: add_inferred(tail, args, return, index - 1, [head | acc])
|
||||
@@ -561,7 +602,7 @@ defmodule Module.Types do
|
||||
# Allow exactly one differing argument. That one position is widened
|
||||
# with union/2. A second difference means the clauses must stay separate.
|
||||
defp union_args([existing_arg | existing], [arg | args], acc, false) do
|
||||
union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true)
|
||||
union_args(existing, args, [Descr.opt_union(existing_arg, arg) | acc], true)
|
||||
end
|
||||
|
||||
defp union_args([_ | _], [_ | _], _acc, true), do: nil
|
||||
@@ -631,7 +672,7 @@ defmodule Module.Types do
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler,
|
||||
# Reverse arrow handling (nil | :cache | :use)
|
||||
# Reverse arrow handling (nil | :cache | :except_none | :include_none)
|
||||
reverse_arrow: nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -33,14 +33,20 @@ defmodule Module.Types.Apply do
|
||||
|> Enum.map(fn {key, type} when is_atom(key) ->
|
||||
tuple([atom([key]), type])
|
||||
end)
|
||||
|> Enum.reduce(&union/2)
|
||||
|> Enum.reduce(&opt_union/2)
|
||||
|> list()
|
||||
end
|
||||
|
||||
fas = list(tuple([atom(), integer()]))
|
||||
|
||||
struct_info =
|
||||
list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
|
||||
list(
|
||||
closed_map(
|
||||
default: {term(), true},
|
||||
field: {atom(), false},
|
||||
required: {boolean(), true}
|
||||
)
|
||||
)
|
||||
|
||||
shared_info = [
|
||||
attributes: list(tuple([atom(), list(term())])),
|
||||
@@ -58,7 +64,7 @@ defmodule Module.Types.Apply do
|
||||
exports_md5: binary(),
|
||||
functions: fas,
|
||||
macros: fas,
|
||||
struct: struct_info |> union(atom([nil]))
|
||||
struct: struct_info |> opt_union(atom([nil]))
|
||||
] ++ shared_info
|
||||
|
||||
infos =
|
||||
@@ -74,7 +80,7 @@ defmodule Module.Types.Apply do
|
||||
module: atom(),
|
||||
functions: fas,
|
||||
consolidated?: boolean(),
|
||||
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
|
||||
impls: opt_union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
|
||||
]
|
||||
|
||||
for {name, clauses} <- infos do
|
||||
@@ -98,10 +104,10 @@ defmodule Module.Types.Apply do
|
||||
|
||||
send_destination =
|
||||
pid()
|
||||
|> union(reference())
|
||||
|> union(port())
|
||||
|> union(atom())
|
||||
|> union(tuple([atom(), atom()]))
|
||||
|> opt_union(reference())
|
||||
|> opt_union(port())
|
||||
|> opt_union(atom())
|
||||
|> opt_union(tuple([atom(), atom()]))
|
||||
|
||||
basic_arith_2_args_clauses = [
|
||||
{[integer(), integer()], integer()},
|
||||
@@ -110,16 +116,24 @@ defmodule Module.Types.Apply do
|
||||
{[float(), float()], float()}
|
||||
]
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
args_or_none = union(list(term()), atom([:none]))
|
||||
extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
|
||||
args_or_arity = opt_union(list(term()), integer())
|
||||
args_or_none = opt_union(list(term()), atom([:none]))
|
||||
custom_info_key = opt_difference(atom(), atom([:file, :line, :error_info]))
|
||||
|
||||
extra_info =
|
||||
list(
|
||||
tuple([atom([:file]), opt_union(list(integer()), binary())])
|
||||
|> opt_union(tuple([atom([:line]), integer()]))
|
||||
|> opt_union(tuple([atom([:error_info]), open_map()]))
|
||||
|> opt_union(tuple([custom_info_key, term()]))
|
||||
)
|
||||
|
||||
raise_stacktrace =
|
||||
list(
|
||||
tuple([atom(), atom(), args_or_arity, extra_info])
|
||||
|> union(tuple([atom(), atom(), args_or_arity]))
|
||||
|> union(tuple([fun(), args_or_arity, extra_info]))
|
||||
|> union(tuple([fun(), args_or_arity]))
|
||||
|> opt_union(tuple([atom(), atom(), args_or_arity]))
|
||||
|> opt_union(tuple([fun(), args_or_arity, extra_info]))
|
||||
|> opt_union(tuple([fun(), args_or_arity]))
|
||||
)
|
||||
|
||||
not_signature =
|
||||
@@ -137,6 +151,8 @@ defmodule Module.Types.Apply do
|
||||
{[atom([left]), atom([right])], atom([left or right])}
|
||||
end
|
||||
|
||||
non_empty_tuple = opt_difference(open_tuple([]), tuple([]))
|
||||
|
||||
for {mod, fun, clauses} <- [
|
||||
# :binary
|
||||
{:binary, :copy, [{[binary(), integer()], binary()}]},
|
||||
@@ -147,7 +163,8 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :-, basic_arith_2_args_clauses},
|
||||
{:erlang, :*, basic_arith_2_args_clauses},
|
||||
{:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
|
||||
{:erlang, :/,
|
||||
[{[opt_union(integer(), float()), opt_union(integer(), float())], float()}]},
|
||||
{:erlang, :"/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :<, [{[term(), term()], boolean()}]},
|
||||
@@ -177,12 +194,12 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :bsr, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bxor, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :byte_size, [{[bitstring()], integer()}]},
|
||||
{:erlang, :ceil, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :ceil, [{[opt_union(integer(), float())], integer()}]},
|
||||
{:erlang, :div, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :error, [{[term()], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
|
||||
{:erlang, :floor, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :floor, [{[opt_union(integer(), float())], integer()}]},
|
||||
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer()], binary()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
|
||||
@@ -200,12 +217,12 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :make_tuple, [{[integer(), term()], tuple()}]},
|
||||
{:erlang, :map_size, [{[open_map()], integer()}]},
|
||||
{:erlang, :node, [{[], atom()}]},
|
||||
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
|
||||
{:erlang, :node, [{[pid() |> opt_union(reference()) |> opt_union(port())], atom()}]},
|
||||
{:erlang, :not, not_signature},
|
||||
{:erlang, :or, or_signature},
|
||||
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
|
||||
{:erlang, :rem, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :round, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :round, [{[opt_union(integer(), float())], integer()}]},
|
||||
{:erlang, :self, [{[], pid()}]},
|
||||
{:erlang, :spawn, [{[fun(0)], pid()}]},
|
||||
{:erlang, :spawn, [{mfargs, pid()}]},
|
||||
@@ -213,35 +230,38 @@ 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()}]},
|
||||
|
||||
# TODO: Replace term()/dynamic() by parametric types
|
||||
{:erlang, :trunc, [{[opt_union(integer(), float())], integer()}]},
|
||||
{:erlang, :++,
|
||||
[
|
||||
{[empty_list(), term()], dynamic(term())},
|
||||
{[non_empty_list(term()), term()], dynamic(non_empty_list(term(), term()))}
|
||||
]},
|
||||
{:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]},
|
||||
{:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :delete_element, [{[integer(), non_empty_tuple], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]},
|
||||
{:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]},
|
||||
{:erlang, :element, [{[integer(), non_empty_tuple], dynamic()}]},
|
||||
{:erlang, :insert_element,
|
||||
[{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
|
||||
[{[integer(), open_tuple([]), term()], dynamic(non_empty_tuple)}]},
|
||||
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :map_get, [{[term(), open_map()], term()}]},
|
||||
{:erlang, :max, [{[term(), term()], dynamic()}]},
|
||||
{:erlang, :min, [{[term(), term()], dynamic()}]},
|
||||
{:erlang, :send, [{[send_destination, term()], dynamic()}]},
|
||||
{:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
|
||||
{:erlang, :setelement,
|
||||
[{[integer(), non_empty_tuple, term()], dynamic(non_empty_tuple)}]},
|
||||
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
|
||||
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
|
||||
|
||||
## Kernel
|
||||
{Kernel, :elem, [{[open_tuple([]), integer()], dynamic()}]},
|
||||
{Kernel, :elem, [{[non_empty_tuple, integer()], dynamic()}]},
|
||||
{Kernel, :is_map_key, [{[open_map(), term()], boolean()}]},
|
||||
{Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
|
||||
{Kernel, :put_elem, [{[non_empty_tuple, integer(), term()], dynamic(non_empty_tuple)}]},
|
||||
|
||||
## Lists
|
||||
{:lists, :member,
|
||||
@@ -253,9 +273,10 @@ defmodule Module.Types.Apply do
|
||||
## Map
|
||||
{Map, :delete, [{[open_map(), term()], open_map()}]},
|
||||
{Map, :fetch,
|
||||
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
|
||||
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]},
|
||||
{Map, :fetch!, [{[open_map(), term()], term()}]},
|
||||
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
|
||||
{Map, :from_struct,
|
||||
[{[open_map(__struct__: {atom(), false})], open_map(__struct__: {none(), true})}]},
|
||||
{Map, :get, [{[open_map(), term()], term()}]},
|
||||
{Map, :get, [{[open_map(), term(), term()], term()}]},
|
||||
{Map, :get_lazy, [{[open_map(), term(), fun(0)], term()}]},
|
||||
@@ -272,22 +293,24 @@ defmodule Module.Types.Apply do
|
||||
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
|
||||
{Map, :update, [{[open_map(), term(), term(), fun(1)], open_map()}]},
|
||||
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
|
||||
{Tuple, :delete_at, [{[open_tuple([]), integer()], dynamic(open_tuple([]))}]},
|
||||
{Tuple, :delete_at, [{[non_empty_tuple, integer()], dynamic(open_tuple([]))}]},
|
||||
{Tuple, :duplicate, [{[term(), integer()], tuple()}]},
|
||||
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
|
||||
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(non_empty_tuple)}]},
|
||||
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
|
||||
{:maps, :find,
|
||||
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
|
||||
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]},
|
||||
{:maps, :get, [{[term(), open_map()], term()}]},
|
||||
{:maps, :is_key, [{[term(), open_map()], boolean()}]},
|
||||
{:maps, :keys, [{[open_map()], list(term())}]},
|
||||
{:maps, :put, [{[term(), term(), open_map()], open_map()}]},
|
||||
{:maps, :remove, [{[term(), open_map()], open_map()}]},
|
||||
{:maps, :take,
|
||||
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
|
||||
[{[term(), open_map()], tuple([term(), open_map()]) |> opt_union(atom([:error]))}]},
|
||||
{:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]},
|
||||
{:maps, :update, [{[term(), term(), open_map()], open_map()}]},
|
||||
{:maps, :values, [{[open_map()], list(term())}]}
|
||||
{:maps, :values, [{[open_map()], list(term())}]},
|
||||
{String, :to_existing_atom, [{[binary(), non_empty_list(atom())], atom()}]},
|
||||
{List, :to_existing_atom, [{[list(integer()), non_empty_list(atom())], atom()}]}
|
||||
] do
|
||||
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
|
||||
|
||||
@@ -300,7 +323,7 @@ defmodule Module.Types.Apply do
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &opt_union/2) end)
|
||||
|
||||
{:strong, domain, clauses}
|
||||
end
|
||||
@@ -317,9 +340,9 @@ defmodule Module.Types.Apply do
|
||||
is_float: float(),
|
||||
is_function: fun(),
|
||||
is_integer: integer(),
|
||||
is_list: union(empty_list(), non_empty_list(term(), term())),
|
||||
is_list: opt_union(empty_list(), non_empty_list(term(), term())),
|
||||
is_map: open_map(),
|
||||
is_number: union(float(), integer()),
|
||||
is_number: opt_union(float(), integer()),
|
||||
is_pid: pid(),
|
||||
is_port: port(),
|
||||
is_reference: reference(),
|
||||
@@ -331,7 +354,7 @@ defmodule Module.Types.Apply do
|
||||
{:strong, [term()],
|
||||
[
|
||||
{[type], atom([true])},
|
||||
{[negation(type)], atom([false])}
|
||||
{[Module.Types.Descr.opt_negation(type)], atom([false])}
|
||||
]}
|
||||
|
||||
def signature(:erlang, unquote(guard), 1),
|
||||
@@ -345,7 +368,7 @@ defmodule Module.Types.Apply do
|
||||
{:strong, [term(), integer(), integer()],
|
||||
[
|
||||
{[integer(), integer(), integer()], boolean()},
|
||||
{[negation(integer()), integer(), integer()], atom([false])}
|
||||
{[Module.Types.Descr.opt_negation(integer()), integer(), integer()], atom([false])}
|
||||
]}
|
||||
)
|
||||
)
|
||||
@@ -443,9 +466,14 @@ defmodule Module.Types.Apply do
|
||||
# a and not number(), b and not number() -> a and b
|
||||
#
|
||||
# However, during inference, we type it as `a, b -> a and b` only.
|
||||
{left_type, context} = of_fun.(left, expected, expr, stack, context)
|
||||
{right_type, context} = of_fun.(right, expected, expr, stack, context)
|
||||
result = union(left_type, right_type)
|
||||
# Either argument may be discarded based on term ordering, so the result's
|
||||
# expected type cannot be used to refine either argument. The result itself
|
||||
# flows directly to the consumer, so it can still be contextually refined.
|
||||
{left_type, context} = of_fun.(left, term(), expr, stack, context)
|
||||
{right_type, context} = of_fun.(right, term(), expr, stack, context)
|
||||
result = opt_union(left_type, right_type)
|
||||
refined_result = opt_intersection(result, expected)
|
||||
result = if empty?(refined_result), do: result, else: refined_result
|
||||
|
||||
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
|
||||
remote_error(error, :erlang, name, 2, expr, stack, context)
|
||||
@@ -670,10 +698,14 @@ defmodule Module.Types.Apply do
|
||||
{type, context} = of_fun.(literal, term(), expr, stack, context)
|
||||
|
||||
if singleton?(type) do
|
||||
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
|
||||
acc =
|
||||
if polarity,
|
||||
do: opt_union(acc, type),
|
||||
else: opt_intersection(acc, Module.Types.Descr.opt_negation(type))
|
||||
|
||||
{acc, all_singleton?, context}
|
||||
else
|
||||
acc = if polarity, do: union(acc, type), else: acc
|
||||
acc = if polarity, do: opt_union(acc, type), else: acc
|
||||
{acc, false, context}
|
||||
end
|
||||
end)
|
||||
@@ -711,9 +743,9 @@ defmodule Module.Types.Apply do
|
||||
|
||||
@empty_list empty_list()
|
||||
@non_empty_list non_empty_list(term())
|
||||
@list union(empty_list(), non_empty_list(term()))
|
||||
@list opt_union(empty_list(), non_empty_list(term()))
|
||||
@empty_map empty_map()
|
||||
@non_empty_map difference(open_map(), empty_map())
|
||||
@non_empty_map opt_difference(open_map(), empty_map())
|
||||
|
||||
# Limit the size of tuples to 16 entries
|
||||
# as otherwise we may create large nodes
|
||||
@@ -773,7 +805,7 @@ defmodule Module.Types.Apply do
|
||||
{tuple(List.duplicate(term(), literal)), true}
|
||||
|
||||
:tuple_size ->
|
||||
{difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true}
|
||||
{opt_difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true}
|
||||
end
|
||||
|
||||
{actual, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
@@ -804,7 +836,7 @@ defmodule Module.Types.Apply do
|
||||
# This logic mirrors the code in `Pattern.of_pattern_tree`
|
||||
# If it is a singleton, we can always be precise
|
||||
if singleton?(type) do
|
||||
expected = if polarity, do: type, else: negation(type)
|
||||
expected = if polarity, do: type, else: Module.Types.Descr.opt_negation(type)
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
|
||||
|
||||
@@ -912,13 +944,13 @@ defmodule Module.Types.Apply do
|
||||
defp expected_order(_, :<, 0), do: :none
|
||||
|
||||
defp expected_order(:tuple_size, :<, size),
|
||||
do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true}
|
||||
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true}
|
||||
|
||||
defp expected_order(:tuple_size, :"=<", 0),
|
||||
do: {tuple([]), true}
|
||||
|
||||
defp expected_order(:tuple_size, :"=<", size),
|
||||
do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true}
|
||||
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true}
|
||||
|
||||
defp expected_order(:tuple_size, :>, size),
|
||||
do: {open_tuple(List.duplicate(term(), size + 1)), true}
|
||||
@@ -974,7 +1006,7 @@ defmodule Module.Types.Apply do
|
||||
{:strong, [term(), integer()],
|
||||
[
|
||||
{[type, integer()], atom([true])},
|
||||
{[negation(type), integer()], atom([false])}
|
||||
{[Module.Types.Descr.opt_negation(type), integer()], atom([false])}
|
||||
]}
|
||||
|
||||
{info, filter_domain(info, expected, 2), context}
|
||||
@@ -985,61 +1017,71 @@ defmodule Module.Types.Apply do
|
||||
info =
|
||||
{:strong, [term(), open_map()],
|
||||
[
|
||||
{[term(), open_map([{key, term()}])], atom([true])},
|
||||
{[term(), open_map([{key, not_set()}])], atom([false])}
|
||||
{[term(), open_map([{key, {term(), false}}])], atom([true])},
|
||||
{[term(), open_map([{key, {none(), true}}])], atom([false])}
|
||||
]}
|
||||
|
||||
{info, filter_domain(info, expected, 2), context}
|
||||
end
|
||||
|
||||
def remote_domain(:maps, :is_key, [key, map], expected, meta, stack, context)
|
||||
when is_atom(key) do
|
||||
remote_domain(:erlang, :is_map_key, [key, map], expected, meta, stack, context)
|
||||
end
|
||||
|
||||
def remote_domain(Kernel, :is_map_key, [_map, key], expected, _meta, _stack, context)
|
||||
when is_atom(key) do
|
||||
info =
|
||||
{:strong, [open_map(), term()],
|
||||
[
|
||||
{[open_map([{key, term()}]), term()], atom([true])},
|
||||
{[open_map([{key, not_set()}]), term()], atom([false])}
|
||||
{[open_map([{key, {term(), false}}]), term()], atom([true])},
|
||||
{[open_map([{key, {none(), true}}]), term()], atom([false])}
|
||||
]}
|
||||
|
||||
{info, filter_domain(info, expected, 2), context}
|
||||
end
|
||||
|
||||
def remote_domain(Map, :has_key?, [map, key], expected, meta, stack, context)
|
||||
when is_atom(key) do
|
||||
remote_domain(Kernel, :is_map_key, [map, key], expected, meta, stack, context)
|
||||
end
|
||||
|
||||
def remote_domain(:erlang, :map_get, [key, _], expected, _meta, _stack, context)
|
||||
when is_atom(key) do
|
||||
domain = [term(), open_map([{key, expected}])]
|
||||
domain = [term(), open_map([{key, {expected, false}}])]
|
||||
{{:strong, nil, [{domain, term()}]}, domain, context}
|
||||
end
|
||||
|
||||
def remote_domain(Map, :fetch!, [_, key], expected, _meta, _stack, context) when is_atom(key) do
|
||||
domain = [open_map([{key, expected}]), term()]
|
||||
domain = [open_map([{key, {expected, false}}]), term()]
|
||||
{{:strong, nil, [{domain, term()}]}, domain, context}
|
||||
end
|
||||
|
||||
def remote_domain(:maps, :get, [key, _], expected, _meta, _stack, context) when is_atom(key) do
|
||||
domain = [term(), open_map([{key, expected}])]
|
||||
domain = [term(), open_map([{key, {expected, false}}])]
|
||||
{{:strong, nil, [{domain, term()}]}, domain, context}
|
||||
end
|
||||
|
||||
def remote_domain(Map, :replace!, [_, key, _], _expected, _meta, _stack, context)
|
||||
when is_atom(key) do
|
||||
domain = [open_map([{key, term()}]), term(), term()]
|
||||
domain = [open_map([{key, {term(), false}}]), term(), term()]
|
||||
{{:strong, nil, [{domain, open_map()}]}, domain, context}
|
||||
end
|
||||
|
||||
def remote_domain(:maps, :update, [key, _, _], _expected, _meta, _stack, context)
|
||||
when is_atom(key) do
|
||||
domain = [term(), term(), open_map([{key, term()}])]
|
||||
domain = [term(), term(), open_map([{key, {term(), false}}])]
|
||||
{{:strong, nil, [{domain, open_map()}]}, domain, context}
|
||||
end
|
||||
|
||||
def remote_domain(Map, :pop!, [_, key], _expected, _meta, _stack, context) when is_atom(key) do
|
||||
domain = [open_map([{key, term()}]), term()]
|
||||
domain = [open_map([{key, {term(), false}}]), term()]
|
||||
{{:strong, nil, [{domain, tuple([term(), open_map()])}]}, domain, context}
|
||||
end
|
||||
|
||||
def remote_domain(Map, :update!, [_, key, _], _expected, _meta, _stack, context)
|
||||
when is_atom(key) do
|
||||
domain = [open_map([{key, term()}]), term(), fun(1)]
|
||||
domain = [open_map([{key, {term(), false}}]), term(), fun(1)]
|
||||
{{:strong, nil, [{domain, open_map()}]}, domain, context}
|
||||
end
|
||||
|
||||
@@ -1079,6 +1121,10 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(:erlang, :element, info, [_index, tuple] = args_types, stack) do
|
||||
remote_apply_tuple_element(info, args_types, tuple, stack)
|
||||
end
|
||||
|
||||
defp remote_apply(:erlang, :map_get, _info, [key, map] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(value, args_types, stack)}
|
||||
@@ -1121,26 +1167,38 @@ defmodule Module.Types.Apply do
|
||||
none()
|
||||
end
|
||||
|
||||
{:ok, return(union(left_result, right_result), [left, right], stack)}
|
||||
{:ok, return(opt_union(left_result, right_result), [left, right], stack)}
|
||||
|
||||
:badproperlist ->
|
||||
{:error, badremote(:erlang, :++, [left, right])}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(Kernel, :elem, info, [tuple, _index] = args_types, stack) do
|
||||
remote_apply_tuple_element(info, args_types, tuple, stack)
|
||||
end
|
||||
|
||||
@struct_key atom([:__struct__])
|
||||
@nil_atom atom([nil])
|
||||
|
||||
defp remote_apply(Map, :from_struct, _info, [map] = args_types, stack) do
|
||||
case map_update(map, @struct_key, not_set(), false, true) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :from_struct, args_types)}
|
||||
{:error, _errors} -> {:ok, map}
|
||||
defp remote_apply(Map, :from_struct, info, [map] = args_types, stack) do
|
||||
case remote_apply(info, args_types, stack) do
|
||||
{:ok, _type} ->
|
||||
case map_update(map, @struct_key, none(), true, false, true) do
|
||||
{_value, descr, _errors} ->
|
||||
{:ok, return(descr, args_types, stack)}
|
||||
|
||||
_ ->
|
||||
{:error, badremote(Map, :from_struct, args_types)}
|
||||
end
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :delete, _info, [map, key] = args_types, stack) do
|
||||
case map_update(map, key, not_set(), false, true) do
|
||||
case map_update(map, key, none(), true, false, true) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :delete, args_types)}
|
||||
{:error, _errors} -> {:ok, map}
|
||||
@@ -1150,7 +1208,7 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{_, value} ->
|
||||
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
|
||||
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error]))
|
||||
{:ok, return(result, args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1171,7 +1229,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)}
|
||||
{:ok, value} -> {:ok, return(opt_union(value, @nil_atom), args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :get, args_types)}
|
||||
:error -> {:error, {:badkeydomain, map, key, @nil_atom}}
|
||||
end
|
||||
@@ -1179,7 +1237,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
defp remote_apply(Map, :get, _info, [map, key, default] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
|
||||
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :get, args_types)}
|
||||
:error -> {:error, {:badkeydomain, map, key, default}}
|
||||
end
|
||||
@@ -1189,7 +1247,7 @@ defmodule Module.Types.Apply do
|
||||
case fun_apply(fun, []) do
|
||||
{:ok, default} ->
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
|
||||
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :get_lazy, args_types)}
|
||||
:error -> {:error, {:badkeydomain, map, key, default}}
|
||||
end
|
||||
@@ -1217,9 +1275,9 @@ defmodule Module.Types.Apply do
|
||||
_ -> args_types
|
||||
end
|
||||
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
case map_update(map, key, none(), true, true, false) do
|
||||
{value, descr, _errors} ->
|
||||
value = union(value, default)
|
||||
value = opt_union(value, default)
|
||||
{:ok, return(tuple([value, descr]), args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1233,9 +1291,9 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(Map, :pop_lazy, _info, [map, key, fun] = args_types, stack) do
|
||||
case fun_apply(fun, []) do
|
||||
{:ok, default} ->
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
case map_update(map, key, none(), true, true, false) do
|
||||
{value, descr, _errors} ->
|
||||
value = union(value, default)
|
||||
value = opt_union(value, default)
|
||||
{:ok, return(tuple([value, descr]), args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1251,7 +1309,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :put, _info, [map, key, value] = args_types, stack) do
|
||||
case map_update(map, key, value, false, true) do
|
||||
case map_update(map, key, value, false, false, true) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :put, args_types)}
|
||||
{:error, _errors} -> {:ok, map}
|
||||
@@ -1259,7 +1317,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :pop!, _info, [map, key] = args_types, stack) do
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
case map_update(map, key, none(), true, true, false) do
|
||||
{value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :pop!, args_types)}
|
||||
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
|
||||
@@ -1267,7 +1325,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :replace, _info, [map, key, value] = args_types, stack) do
|
||||
fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
|
||||
fun = fn _value, optional? -> {value, optional?} end
|
||||
|
||||
case map_update_fun(map, key, fun, false, false) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
@@ -1277,7 +1335,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :replace!, _info, [map, key, value] = args_types, stack) do
|
||||
fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
|
||||
fun = fn _value, optional? -> {value, optional?} end
|
||||
|
||||
case map_update_fun(map, key, fun, false, false) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
@@ -1304,12 +1362,12 @@ defmodule Module.Types.Apply do
|
||||
_ -> map
|
||||
end
|
||||
|
||||
fun_apply = fn optional?, arg_type ->
|
||||
fun_apply = fn arg_type, optional? ->
|
||||
if empty?(arg_type) do
|
||||
default
|
||||
{default, false}
|
||||
else
|
||||
case fun_apply(fun, [arg_type]) do
|
||||
{:ok, res} -> if optional?, do: union(res, default), else: res
|
||||
{:ok, res} -> {if(optional?, do: opt_union(res, default), else: res), false}
|
||||
reason -> throw({:badapply, reason, [arg_type]})
|
||||
end
|
||||
end
|
||||
@@ -1333,7 +1391,7 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(:maps, :find, _info, [key, map] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{_, value} ->
|
||||
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
|
||||
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error]))
|
||||
{:ok, return(result, args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1353,7 +1411,6 @@ defmodule Module.Types.Apply do
|
||||
if key_type == dynamic() or key_type == term() do
|
||||
{:ok, return(open_map(), args_types, stack)}
|
||||
else
|
||||
value_type = if_set(value_type)
|
||||
domain_keys = to_domain_keys(key_type)
|
||||
|
||||
keys =
|
||||
@@ -1362,12 +1419,18 @@ defmodule Module.Types.Apply do
|
||||
_ -> [domain_keys]
|
||||
end
|
||||
|
||||
map = closed_map(Enum.map(keys, &{&1, value_type}))
|
||||
map =
|
||||
closed_map(
|
||||
Enum.map(keys, fn
|
||||
key when is_atom(key) -> {key, {value_type, true}}
|
||||
key -> {key, value_type}
|
||||
end)
|
||||
)
|
||||
|
||||
map_and_maybe_empty_map =
|
||||
case empty_list? do
|
||||
true -> map
|
||||
false -> difference(map, empty_map())
|
||||
false -> opt_difference(map, empty_map())
|
||||
end
|
||||
|
||||
{:ok, return(map_and_maybe_empty_map, args_types, stack)}
|
||||
@@ -1394,7 +1457,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(:maps, :put, _info, [key, value, map] = args_types, stack) do
|
||||
case map_update(map, key, value, false, true) do
|
||||
case map_update(map, key, value, false, false, true) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(:maps, :put, args_types)}
|
||||
{:error, _errors} -> {:ok, map}
|
||||
@@ -1402,7 +1465,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do
|
||||
case map_update(map, key, not_set(), false, true) do
|
||||
case map_update(map, key, none(), true, false, true) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(:maps, :remove, args_types)}
|
||||
{:error, _errors} -> {:ok, map}
|
||||
@@ -1410,9 +1473,9 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
case map_update(map, key, none(), true, true, false) do
|
||||
{value, descr, _errors} ->
|
||||
result = union(tuple([value, descr]), atom([:error]))
|
||||
result = opt_union(tuple([value, descr]), atom([:error]))
|
||||
{:ok, return(result, args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1431,7 +1494,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp remote_apply(:maps, :update, _info, [key, value, map] = args_types, stack) do
|
||||
fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
|
||||
fun = fn _value, optional? -> {value, optional?} end
|
||||
|
||||
case map_update_fun(map, key, fun, false, false) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
@@ -1447,10 +1510,49 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(mod, :to_existing_atom, info, [_string, list] = args_types, stack)
|
||||
when mod in [String, List] do
|
||||
# TODO: remove once we add parametric types, this will just be:
|
||||
# binary(), non_empty_list(a) -> a when a: atom()
|
||||
|
||||
case remote_apply(info, args_types, stack) do
|
||||
{:ok, _} ->
|
||||
# If list is a dynamic type, remote_apply only guarantees compatibility,
|
||||
# and therefore we may still have improper lists or lists of dynamic().
|
||||
# Note we cannot have static empty lists, as those would fail on remote_apply,
|
||||
# but we can have dynamic empty lists (which we should ignore)
|
||||
case list_of(list) do
|
||||
{_empty_list, refined_atom} when is_descr(refined_atom) ->
|
||||
{:ok, return(opt_intersection(refined_atom, atom()), args_types, stack)}
|
||||
|
||||
:badproperlist ->
|
||||
{:error, badremote(mod, :to_existing_atom, args_types)}
|
||||
end
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(_mod, _fun, info, args_types, stack) do
|
||||
remote_apply(info, args_types, stack)
|
||||
end
|
||||
|
||||
defp remote_apply_tuple_element(info, args_types, tuple, stack) do
|
||||
# TODO: Convert this into a regular type signature {...a} and not {}, integer -> a
|
||||
with {:ok, fallback} <- remote_apply(info, args_types, stack) do
|
||||
case tuple_values(tuple) do
|
||||
:badtuple ->
|
||||
# This is not virtually possible, since remote apply validates the arguments.
|
||||
# So returning the fallback is acceptable.
|
||||
{:ok, fallback}
|
||||
|
||||
values ->
|
||||
{:ok, return(values, args_types, stack)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(:none, _args_types, _stack) do
|
||||
{:ok, dynamic()}
|
||||
end
|
||||
@@ -1486,10 +1588,10 @@ defmodule Module.Types.Apply do
|
||||
Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} ->
|
||||
case signature(module, fun, arity, meta, stack, context) do
|
||||
{{:strong, _, clauses}, context} ->
|
||||
{union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
|
||||
{opt_union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
|
||||
|
||||
{{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
|
||||
{union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
|
||||
{opt_union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
|
||||
|
||||
{_, context} ->
|
||||
{type, true, context}
|
||||
@@ -1778,8 +1880,8 @@ defmodule Module.Types.Apply do
|
||||
|
||||
defp map_put_new(map, key, value, name, args_types, stack) do
|
||||
fun = fn
|
||||
true, type -> union(type, value)
|
||||
false, type -> if empty?(type), do: value, else: type
|
||||
type, true -> {opt_union(type, value), false}
|
||||
type, false -> {if(empty?(type), do: value, else: type), false}
|
||||
end
|
||||
|
||||
case map_update_fun(map, key, fun, false, true) do
|
||||
@@ -1797,9 +1899,9 @@ defmodule Module.Types.Apply do
|
||||
_ -> map
|
||||
end
|
||||
|
||||
fun_apply = fn optional?, arg_type ->
|
||||
fun_apply = fn arg_type, optional? ->
|
||||
case fun_apply(fun, [arg_type]) do
|
||||
{:ok, res} -> if optional?, do: if_set(res), else: res
|
||||
{:ok, res} -> {res, optional?}
|
||||
reason -> throw({:badapply, reason, [arg_type]})
|
||||
end
|
||||
end
|
||||
@@ -1834,7 +1936,7 @@ defmodule Module.Types.Apply do
|
||||
case filter_domain(clauses, expected, [], true) do
|
||||
:none -> domain(domain, clauses)
|
||||
:all -> domain(domain, clauses)
|
||||
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end)
|
||||
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &opt_union/2) end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1858,7 +1960,7 @@ defmodule Module.Types.Apply do
|
||||
{used, dynamic()}
|
||||
|
||||
{_count, used, returns} ->
|
||||
{used, returns |> Enum.reduce(&union/2) |> dynamic()}
|
||||
{used, returns |> Enum.reduce(&opt_union/2) |> dynamic()}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1875,7 +1977,7 @@ defmodule Module.Types.Apply do
|
||||
# as a non disjoint check. So we skip checking compatibility twice.
|
||||
with true <- zip_compatible_or_only_gradual?(args_types, domain),
|
||||
{count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do
|
||||
{used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)}
|
||||
{used, returns |> Enum.reduce(&opt_union/2) |> return(args_types, stack)}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
@@ -2220,7 +2322,7 @@ defmodule Module.Types.Apply do
|
||||
clauses =
|
||||
case mod.__protocol__(:impls) do
|
||||
{:consolidated, mods} ->
|
||||
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2)
|
||||
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&opt_union/2)
|
||||
[{[domain], dynamic()}]
|
||||
|
||||
_ ->
|
||||
@@ -2430,10 +2532,10 @@ defmodule Module.Types.Apply do
|
||||
term_type?(actual) do
|
||||
actual |> to_quoted() |> Code.Formatter.to_algebra()
|
||||
else
|
||||
common = intersection(actual, expected)
|
||||
common = opt_intersection(actual, expected)
|
||||
|
||||
uncommon_doc =
|
||||
difference(actual, expected)
|
||||
opt_difference(actual, expected)
|
||||
|> to_quoted()
|
||||
|> Code.Formatter.to_algebra()
|
||||
|> ansi_red()
|
||||
@@ -2451,17 +2553,17 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
@composite_types non_empty_list(term(), term())
|
||||
|> union(tuple())
|
||||
|> union(open_map())
|
||||
|> union(fun())
|
||||
|> opt_union(tuple())
|
||||
|> opt_union(open_map())
|
||||
|> opt_union(fun())
|
||||
|
||||
# If actual/expected have a composite type, computing the
|
||||
# `intersection(actual, expected) or difference(actual, expected)`
|
||||
# `opt_intersection(actual, expected) or opt_difference(actual, expected)`
|
||||
# can lead to an explosion of terms that actually make debugging
|
||||
# harder. So we check that at least one of the two operations
|
||||
# return none() (i.e. actual is a subtype or they are disjoint).
|
||||
defp has_simple_difference?(actual, expected) do
|
||||
composite_types = intersection(actual, @composite_types)
|
||||
composite_types = opt_intersection(actual, @composite_types)
|
||||
subtype?(composite_types, expected) or disjoint?(composite_types, expected)
|
||||
end
|
||||
|
||||
|
||||
+2203
-1801
File diff suppressed because it is too large
Load Diff
+128
-126
@@ -11,51 +11,55 @@ defmodule Module.Types.Expr do
|
||||
14 = length(Macro.Env.__info__(:struct))
|
||||
|
||||
aliases = list(tuple([atom(), atom()]))
|
||||
macro_aliases = list(tuple([atom(), tuple([term(), atom()])]))
|
||||
functions_and_macros = list(tuple([atom(), list(tuple([atom(), integer()]))]))
|
||||
list_of_modules = list(atom())
|
||||
|
||||
@try_catch atom([:error, :exit, :throw])
|
||||
|
||||
@caller closed_map(
|
||||
__struct__: atom([Macro.Env]),
|
||||
aliases: aliases,
|
||||
context: atom([:match, :guard, nil]),
|
||||
context_modules: list_of_modules,
|
||||
file: binary(),
|
||||
function: union(tuple([atom(), integer()]), atom([nil])),
|
||||
functions: functions_and_macros,
|
||||
lexical_tracker: union(pid(), atom([nil])),
|
||||
line: integer(),
|
||||
macro_aliases: aliases,
|
||||
macros: functions_and_macros,
|
||||
module: atom(),
|
||||
requires: list_of_modules,
|
||||
tracers: list_of_modules,
|
||||
versioned_vars: open_map()
|
||||
__struct__: {atom([Macro.Env]), false},
|
||||
aliases: {aliases, false},
|
||||
context: {atom([:match, :guard, nil]), false},
|
||||
context_modules: {list_of_modules, false},
|
||||
file: {binary(), false},
|
||||
function: {opt_union(tuple([atom(), integer()]), atom([nil])), false},
|
||||
functions: {functions_and_macros, false},
|
||||
lexical_tracker: {opt_union(pid(), atom([nil])), false},
|
||||
line: {integer(), false},
|
||||
macro_aliases: {macro_aliases, false},
|
||||
macros: {functions_and_macros, false},
|
||||
module: {atom(), false},
|
||||
requires: {list_of_modules, false},
|
||||
tracers: {list_of_modules, false},
|
||||
versioned_vars: {open_map(), false}
|
||||
)
|
||||
|
||||
# We do not make exception dynamic on purpose. If you do a blank rescue,
|
||||
# then we will assume you need to statically handle all possible exceptions.
|
||||
@exception open_map(__struct__: atom(), __exception__: term())
|
||||
@exception open_map(__struct__: {atom(), false}, __exception__: {term(), false})
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
args_or_arity = opt_union(list(term()), integer())
|
||||
|
||||
custom_info_key = opt_difference(atom(), atom([:file, :line, :error_info]))
|
||||
|
||||
extra_info =
|
||||
list(
|
||||
tuple([atom([:file]), list(integer())])
|
||||
|> union(tuple([atom([:line]), integer()]))
|
||||
|> union(tuple([atom([:error_info]), open_map()]))
|
||||
tuple([atom([:file]), opt_union(list(integer()), binary())])
|
||||
|> opt_union(tuple([atom([:line]), integer()]))
|
||||
|> opt_union(tuple([atom([:error_info]), open_map()]))
|
||||
|> opt_union(tuple([custom_info_key, term()]))
|
||||
)
|
||||
|
||||
@stacktrace list(
|
||||
union(
|
||||
opt_union(
|
||||
tuple([atom(), atom(), args_or_arity, extra_info]),
|
||||
tuple([fun(), args_or_arity, extra_info])
|
||||
)
|
||||
)
|
||||
|
||||
@falsy atom([false, nil])
|
||||
@truthy negation(atom([false, nil]))
|
||||
@truthy Module.Types.Descr.opt_negation(atom([false, nil]))
|
||||
|
||||
# :atom
|
||||
def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom),
|
||||
@@ -106,7 +110,7 @@ defmodule Module.Types.Expr do
|
||||
of_expr(suffix, tl_type, expr, stack, context)
|
||||
end
|
||||
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
@@ -150,13 +154,13 @@ defmodule Module.Types.Expr do
|
||||
fn pattern_type, context ->
|
||||
# See if we can use the expected type to further refine the pattern type,
|
||||
# if we cannot, use the pattern type as that will fail later on.
|
||||
type = intersection(pattern_type, expected)
|
||||
type = opt_intersection(pattern_type, expected)
|
||||
type = if empty?(type), do: pattern_type, else: type
|
||||
{result, context} = of_expr(right_expr, type, expr, stack, context)
|
||||
|
||||
# The function may still return a too broad type,
|
||||
# so we refine once again to assign the most appropriate to the pattern.
|
||||
{intersection(result, expected), context}
|
||||
{opt_intersection(result, expected), context}
|
||||
end
|
||||
|
||||
Pattern.of_match(left_expr, type_fun, match, meta, stack, context)
|
||||
@@ -182,12 +186,12 @@ defmodule Module.Types.Expr do
|
||||
expected_pairs =
|
||||
Enum.flat_map(pairs_types, fn {key_type, _value_type} ->
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, [key]} -> [{key, term()}]
|
||||
{:finite, [key]} -> [{key, {term(), false}}]
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
|
||||
expected = intersection(expected, open_map(expected_pairs))
|
||||
expected = opt_intersection(expected, open_map(expected_pairs))
|
||||
{map_type, context} = of_expr(map, expected, expr, stack, context)
|
||||
|
||||
try do
|
||||
@@ -311,7 +315,7 @@ defmodule Module.Types.Expr do
|
||||
maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false)
|
||||
|
||||
{_, truthy_context} =
|
||||
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
# Keep the context except the warnings, and compute the body
|
||||
truthy_context = reset_warnings(truthy_context, context)
|
||||
@@ -323,14 +327,14 @@ defmodule Module.Types.Expr do
|
||||
context = Of.reset_vars(pos_body_context, context)
|
||||
|
||||
{_, falsy_context} =
|
||||
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
falsy_context = reset_warnings(falsy_context, context)
|
||||
|
||||
{neg_body_type, neg_body_context} =
|
||||
of_expr(neg_body, expected, expr, stack, falsy_context)
|
||||
|
||||
body_type = union(pos_body_type, neg_body_type)
|
||||
body_type = opt_union(pos_body_type, neg_body_type)
|
||||
|
||||
context =
|
||||
cond do
|
||||
@@ -355,7 +359,7 @@ defmodule Module.Types.Expr do
|
||||
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
|
||||
|
||||
{_, truthy_context} =
|
||||
of_expr(head, @truthy, head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(head, @truthy, head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
# Keep the context except the warnings, and compute the body
|
||||
truthy_context = reset_warnings(truthy_context, context)
|
||||
@@ -369,12 +373,12 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
else
|
||||
{_, falsy_context} =
|
||||
of_expr(head, @falsy, head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(head, @falsy, head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
reset_warnings(falsy_context, context)
|
||||
end
|
||||
|
||||
{union(body_type, acc), context}
|
||||
{opt_union(body_type, acc), context}
|
||||
end)
|
||||
|
||||
dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack)
|
||||
@@ -382,7 +386,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context)
|
||||
when stack.reverse_arrow == :use do
|
||||
when stack.reverse_arrow in [:except_none, :include_none] do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
clauses = Map.fetch!(context.reverse_arrows, version)
|
||||
original = context
|
||||
@@ -402,7 +406,7 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
|
||||
# If there is a single clause, we assume it is always evaluated
|
||||
# by doing reverse arrows and incorporting all variables into the context.
|
||||
# by doing reverse arrows and incorporating all variables into the context.
|
||||
# We have to evaluate the head again but it might have been preferred
|
||||
# if we could somehow merge a previously computed context.
|
||||
{_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} ->
|
||||
@@ -419,7 +423,7 @@ defmodule Module.Types.Expr do
|
||||
reset_warnings(context, original)
|
||||
|
||||
{_, filtered} ->
|
||||
case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2))
|
||||
case_expected = Enum.reduce(filtered, none(), &opt_union(elem(&1, 0), &2))
|
||||
{_, context} = of_expr(case_expr, case_expected, case_expr, stack, context)
|
||||
reset_warnings(context, original)
|
||||
end
|
||||
@@ -461,7 +465,13 @@ defmodule Module.Types.Expr do
|
||||
|
||||
# Now we refine the case_expr context and use it to compute the body
|
||||
{_, refined_context} =
|
||||
of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(
|
||||
case_expr,
|
||||
arg_type,
|
||||
case_expr,
|
||||
%{stack | reverse_arrow: :except_none},
|
||||
context
|
||||
)
|
||||
|
||||
{body_type, context} =
|
||||
of_expr(body, expected, body, stack, reset_warnings(refined_context, context))
|
||||
@@ -474,16 +484,22 @@ defmodule Module.Types.Expr do
|
||||
else
|
||||
[arg_type] = Pattern.of_domain(trees, stack, context)
|
||||
clauses_acc = [{arg_type, body_type, clause} | clauses_acc]
|
||||
{{none?, union(body_type, body_acc), clauses_acc}, context}
|
||||
{{none?, opt_union(body_type, body_acc), clauses_acc}, context}
|
||||
end
|
||||
end)
|
||||
|
||||
context =
|
||||
if none? or stack.mode != :static do
|
||||
head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2))
|
||||
head_type = Enum.reduce(clauses_acc, none(), &opt_union(elem(&1, 0), &2))
|
||||
|
||||
{_, refined_context} =
|
||||
of_expr(case_expr, head_type, case_expr, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(
|
||||
case_expr,
|
||||
head_type,
|
||||
case_expr,
|
||||
%{stack | reverse_arrow: :except_none},
|
||||
context
|
||||
)
|
||||
|
||||
reset_warnings(refined_context, context)
|
||||
else
|
||||
@@ -546,7 +562,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
{type, context} = of_expr(body, expected, body, stack, context)
|
||||
{union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
|
||||
{opt_union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
|
||||
end)
|
||||
|
||||
{:catch, clauses}, {acc, context} ->
|
||||
@@ -564,7 +580,7 @@ defmodule Module.Types.Expr do
|
||||
end)
|
||||
end
|
||||
|
||||
@timeout_type union(integer(), atom([:infinity]))
|
||||
@timeout_type opt_union(integer(), atom([:infinity]))
|
||||
|
||||
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
|
||||
cache_result(meta, stack, original, fn ->
|
||||
@@ -581,11 +597,11 @@ defmodule Module.Types.Expr do
|
||||
{body_type, context} = of_expr(body, expected, expr, stack, context)
|
||||
|
||||
if compatible?(timeout_type, @timeout_type) do
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
{opt_union(body_type, acc), Of.reset_vars(context, original)}
|
||||
else
|
||||
error = {:badtimeout, timeout_type, timeout, context}
|
||||
context = error(__MODULE__, error, meta, stack, context)
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
{opt_union(body_type, acc), Of.reset_vars(context, original)}
|
||||
end
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
@@ -609,34 +625,41 @@ defmodule Module.Types.Expr do
|
||||
else
|
||||
# TODO: Use the collectable protocol for the output
|
||||
into = Keyword.get(opts, :into, [])
|
||||
{into_type, into_kind, context} = for_into(into, meta, stack, context)
|
||||
|
||||
case into_kind do
|
||||
:bitstring ->
|
||||
{block_type, context} = of_expr(block, bitstring(), block, stack, context)
|
||||
intersection = intersection(block_type, bitstring())
|
||||
{into_kinds, into_type, expected, context} =
|
||||
for_into(into, meta, expected, stack, context)
|
||||
|
||||
{block_type, context} = of_expr(block, expected, block, stack, context)
|
||||
|
||||
result_type =
|
||||
if :non_empty_list in into_kinds do
|
||||
opt_union(into_type, non_empty_list(block_type))
|
||||
else
|
||||
into_type
|
||||
end
|
||||
|
||||
{result_type, context} =
|
||||
if :bitstring in into_kinds do
|
||||
intersection = opt_intersection(block_type, bitstring())
|
||||
|
||||
# TODO: currently if :into is a binary, then we expect the body
|
||||
# to return a binary, even if the binary is a gradual type. This
|
||||
# is ok for now because we only check for bitstring if the type
|
||||
# is a subset of empty_list() or bitstring(), but we may want to
|
||||
# relax in the future.
|
||||
if empty?(intersection) do
|
||||
error = {:badbitbody, block_type, block, context}
|
||||
{error_type(), error(__MODULE__, error, meta, stack, context)}
|
||||
else
|
||||
{union(into_type, intersection), context}
|
||||
# If into_type is a binary but the block is a bitstring_no_binary(),
|
||||
# then the result may be a bitstring_no_binary().
|
||||
{opt_union(intersection, result_type), context}
|
||||
end
|
||||
else
|
||||
{result_type, context}
|
||||
end
|
||||
|
||||
:non_empty_list ->
|
||||
expected =
|
||||
case list_hd(expected) do
|
||||
{:ok, head} -> head
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{block_type, context} = of_expr(block, expected, block, stack, context)
|
||||
{union(into_type, non_empty_list(block_type)), context}
|
||||
|
||||
:none ->
|
||||
{_, context} = of_expr(block, term(), block, stack, context)
|
||||
{into_type, context}
|
||||
end
|
||||
{if(gradual?(into_type), do: dynamic(result_type), else: result_type), context}
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
end)
|
||||
@@ -662,7 +685,7 @@ defmodule Module.Types.Expr do
|
||||
{else_types, context}
|
||||
end
|
||||
|
||||
dynamic_unless_static({union(do_result, else_result), context}, stack)
|
||||
dynamic_unless_static({opt_union(do_result, else_result), context}, stack)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -679,7 +702,9 @@ defmodule Module.Types.Expr do
|
||||
def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context)
|
||||
when not is_atom(callee) and is_atom(key_or_fun) do
|
||||
if Keyword.get(meta, :no_parens, false) do
|
||||
{type, context} = of_expr(callee, open_map([{key_or_fun, expected}]), expr, stack, context)
|
||||
{type, context} =
|
||||
of_expr(callee, open_map([{key_or_fun, {expected, false}}]), expr, stack, context)
|
||||
|
||||
Of.map_fetch(call, type, key_or_fun, stack, context)
|
||||
else
|
||||
{type, context} = of_expr(callee, atom(), call, stack, context)
|
||||
@@ -688,38 +713,6 @@ defmodule Module.Types.Expr do
|
||||
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)
|
||||
@@ -772,7 +765,7 @@ defmodule Module.Types.Expr do
|
||||
defp of_tuple([elem | elems], index, acc, expected, expr, stack, context) do
|
||||
expr_expected =
|
||||
case tuple_fetch(expected, index) do
|
||||
{_, type} -> type
|
||||
{_optional?, type} -> type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
@@ -787,7 +780,7 @@ defmodule Module.Types.Expr do
|
||||
## Try
|
||||
|
||||
defp of_rescue(var, exceptions, expr, info, meta, stack, context) do
|
||||
args = [__exception__: term()]
|
||||
args = [__exception__: {term(), false}]
|
||||
|
||||
{structs, context} =
|
||||
Enum.map_reduce(exceptions, context, fn exception, context ->
|
||||
@@ -807,7 +800,7 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
|
||||
_ ->
|
||||
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
|
||||
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &opt_union/2)
|
||||
expr = {:__block__, [type_check: info], [expr]}
|
||||
context = Of.declare_var(var, context)
|
||||
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
|
||||
@@ -851,15 +844,22 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
end
|
||||
|
||||
@into_compile union(bitstring(), empty_list())
|
||||
@into_compile opt_union(bitstring(), empty_list())
|
||||
|
||||
defp for_into([], _meta, _stack, context),
|
||||
do: {empty_list(), :non_empty_list, context}
|
||||
defp maybe_list_hd_or_term(expected) do
|
||||
case list_hd(expected) do
|
||||
{:ok, head} -> head
|
||||
_ -> term()
|
||||
end
|
||||
end
|
||||
|
||||
defp for_into(binary, _meta, _stack, context) when is_binary(binary),
|
||||
do: {binary(), :bitstring, context}
|
||||
defp for_into([], _meta, expected, _stack, context),
|
||||
do: {[:non_empty_list], empty_list(), maybe_list_hd_or_term(expected), context}
|
||||
|
||||
defp for_into(into, meta, stack, context) do
|
||||
defp for_into(binary, _meta, _expected, _stack, context) when is_binary(binary),
|
||||
do: {[:bitstring], binary(), bitstring(), context}
|
||||
|
||||
defp for_into(into, meta, expected, stack, context) do
|
||||
meta =
|
||||
case into do
|
||||
{_, meta, _} -> meta
|
||||
@@ -876,24 +876,26 @@ defmodule Module.Types.Expr do
|
||||
# We use subtype? instead of compatible because we want to handle
|
||||
# only bitstring/list, even if a dynamic with something else is given.
|
||||
if subtype?(type, @into_compile) do
|
||||
case {bitstring_type?(type), empty_list_type?(type)} do
|
||||
# If they can be both be true, then we don't know
|
||||
# what the contents of the block are for
|
||||
{true, true} ->
|
||||
type = union(bitstring(), list(term()))
|
||||
{if(gradual?(type), do: dynamic(type), else: type), :none, context}
|
||||
cond do
|
||||
bitstring_type?(type) ->
|
||||
kinds = if empty_list_type?(type), do: [:bitstring, :non_empty_list], else: [:bitstring]
|
||||
# A comprehension may concatenate the block an arbitrary number of times.
|
||||
# Even if both the initial value and each block are unaligned bitstrings,
|
||||
# repeated concatenation may eventually produce an aligned binary.
|
||||
{kinds, opt_union(binary(), type), bitstring(), context}
|
||||
|
||||
{false, true} ->
|
||||
{type, :non_empty_list, context}
|
||||
empty_list_type?(type) ->
|
||||
{[:non_empty_list], type, maybe_list_hd_or_term(expected), context}
|
||||
|
||||
{true, false} ->
|
||||
{type, :bitstring, context}
|
||||
# The type is empty...
|
||||
true ->
|
||||
{[], type, term(), context}
|
||||
end
|
||||
else
|
||||
{_type, context} =
|
||||
Apply.remote_apply(info, Collectable, :into, [type], expr, stack, context)
|
||||
|
||||
{dynamic(), :none, context}
|
||||
{[], dynamic(), term(), context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -911,10 +913,10 @@ defmodule Module.Types.Expr do
|
||||
[pattern_type] = Pattern.of_domain(trees, stack, context)
|
||||
|
||||
{_, refined_context} =
|
||||
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
else_type = if precise?, do: difference(type, pattern_type), else: type
|
||||
{union(else_type, else_types), reset_warnings(refined_context, context)}
|
||||
else_type = if precise?, do: opt_difference(type, pattern_type), else: type
|
||||
{opt_union(else_type, else_types), reset_warnings(refined_context, context)}
|
||||
end
|
||||
|
||||
defp with_clause(expr, stack, {else_types, context}) do
|
||||
@@ -938,7 +940,7 @@ defmodule Module.Types.Expr do
|
||||
Of.with_conditional_vars(mods, none(), call, stack, context, fn mod, acc, context ->
|
||||
expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args}
|
||||
{type, context} = Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
|
||||
{union(acc, type), context}
|
||||
{opt_union(acc, type), context}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -981,7 +983,7 @@ defmodule Module.Types.Expr do
|
||||
context = put_in(context.reverse_arrows[version], result)
|
||||
{result, context}
|
||||
|
||||
:use ->
|
||||
_ ->
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
{Map.fetch!(context.reverse_arrows, version), context}
|
||||
end
|
||||
@@ -1002,7 +1004,7 @@ defmodule Module.Types.Expr do
|
||||
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
|
||||
of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc ->
|
||||
{body_type, context} = of_expr(body, expected, body, stack, context)
|
||||
{union(acc, body_type), context}
|
||||
{opt_union(acc, body_type), context}
|
||||
end
|
||||
|
||||
of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc)
|
||||
@@ -1057,7 +1059,7 @@ defmodule Module.Types.Expr do
|
||||
do: {left_expr, right_expr}
|
||||
|
||||
defp add_inferred([{args, existing_return} | tail], args, return),
|
||||
do: [{args, union(existing_return, return)} | tail]
|
||||
do: [{args, opt_union(existing_return, return)} | tail]
|
||||
|
||||
defp add_inferred([head | tail], args, return),
|
||||
do: [head | add_inferred(tail, args, return)]
|
||||
@@ -1066,7 +1068,7 @@ defmodule Module.Types.Expr do
|
||||
do: [{args, return}]
|
||||
|
||||
defp literal_map_update(descr, key_descr, value_descr) do
|
||||
case map_update(descr, key_descr, value_descr, false, false) do
|
||||
case map_update(descr, key_descr, value_descr, false, false, false) do
|
||||
{_type, descr, []} -> {:ok, descr}
|
||||
{_, _, [error | _]} -> error
|
||||
:badmap -> :badmap
|
||||
|
||||
@@ -57,7 +57,7 @@ defmodule Module.Types.Helpers do
|
||||
## Warnings
|
||||
|
||||
@doc """
|
||||
Converts an itneger into ordinal.
|
||||
Converts an integer into ordinal.
|
||||
"""
|
||||
def integer_to_ordinal(i) do
|
||||
case rem(i, 10) do
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Module.Types.Of do
|
||||
@prefix quote(do: ...)
|
||||
@suffix quote(do: ...)
|
||||
|
||||
@integer_or_float union(integer(), float())
|
||||
@integer_or_float opt_union(integer(), float())
|
||||
@integer integer()
|
||||
@float float()
|
||||
@binary binary()
|
||||
@@ -31,7 +31,7 @@ defmodule Module.Types.Of do
|
||||
@doc """
|
||||
Marks a variable with error.
|
||||
|
||||
This purposedly deletes all traces of the variable,
|
||||
This purposely deletes all traces of the variable,
|
||||
as it is often invoked when the cause for error is elsewhere.
|
||||
"""
|
||||
def error_var({_, meta, _}, context) do
|
||||
@@ -106,15 +106,15 @@ defmodule Module.Types.Of do
|
||||
context
|
||||
end
|
||||
|
||||
case context do
|
||||
case stack do
|
||||
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
|
||||
{old_type, context}
|
||||
|
||||
%{pattern_info: %{guard_context: guard_context}} ->
|
||||
new_type = intersection(old_type, type)
|
||||
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] ->
|
||||
new_type = opt_intersection(old_type, type)
|
||||
|
||||
case empty?(new_type) do
|
||||
true when guard_context == :orelse ->
|
||||
true when reverse_arrow == :include_none ->
|
||||
data = %{
|
||||
data
|
||||
| type: none(),
|
||||
@@ -171,7 +171,7 @@ defmodule Module.Types.Of do
|
||||
{:ok, error_type(), context}
|
||||
|
||||
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
|
||||
new_type = intersection(type, old_type)
|
||||
new_type = opt_intersection(type, old_type)
|
||||
|
||||
data = %{
|
||||
data
|
||||
@@ -245,7 +245,7 @@ defmodule Module.Types.Of do
|
||||
type =
|
||||
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
|
||||
%{^version => %{type: type}} = vars
|
||||
union(acc, type)
|
||||
opt_union(acc, type)
|
||||
end)
|
||||
|
||||
{_, context} = refine_body_var(version, type, expr, stack, context)
|
||||
@@ -264,8 +264,8 @@ defmodule Module.Types.Of do
|
||||
{Float, float()},
|
||||
{Function, fun()},
|
||||
{Integer, integer()},
|
||||
{List, union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: if_set(negation(atom())))},
|
||||
{List, opt_union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: {Module.Types.Descr.opt_negation(atom()), true})},
|
||||
{Port, port()},
|
||||
{PID, pid()},
|
||||
{Reference, reference()},
|
||||
@@ -294,7 +294,7 @@ defmodule Module.Types.Of do
|
||||
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
struct_type(struct, info)
|
||||
else
|
||||
open_map(__struct__: atom([struct]))
|
||||
open_map(__struct__: {atom([struct]), false})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -365,7 +365,7 @@ defmodule Module.Types.Of do
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, domain, [{key, value_type} | single], multiple}
|
||||
{dynamic?, domain, [{key, {value_type, false}} | single], multiple}
|
||||
|
||||
_ ->
|
||||
{dynamic?, domain, single, [{pos, value_type} | multiple]}
|
||||
@@ -381,37 +381,47 @@ defmodule Module.Types.Of do
|
||||
closed_map(non_multiple)
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
closed_map(non_multiple ++ [{key, type} | t])
|
||||
products = cartesian_map(tail)
|
||||
|
||||
for key <- keys, product <- products do
|
||||
closed_map(non_multiple ++ [{key, {type, false}} | product])
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
|> Enum.reduce(&opt_union/2)
|
||||
end
|
||||
|
||||
{if(dynamic?, do: dynamic(map), else: map), context}
|
||||
end
|
||||
|
||||
defp union_negated([], new_type, single, multiple) do
|
||||
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
|
||||
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
|
||||
single =
|
||||
Enum.map(single, fn
|
||||
{key, {old_type, optional?}} ->
|
||||
{key, {opt_union(old_type, new_type), optional?}}
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end)
|
||||
|
||||
{single, multiple}
|
||||
end
|
||||
|
||||
defp union_negated(negated, new_type, single, multiple) do
|
||||
{single, matched} =
|
||||
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
|
||||
if key in negated do
|
||||
{{key, old_type}, [key | matched]}
|
||||
else
|
||||
{{key, union(old_type, new_type)}, matched}
|
||||
end
|
||||
Enum.map_reduce(single, [], fn
|
||||
{key, {old_type, optional?}}, matched ->
|
||||
if key in negated do
|
||||
{{key, {old_type, optional?}}, [key | matched]}
|
||||
else
|
||||
{{key, {opt_union(old_type, new_type), optional?}}, matched}
|
||||
end
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} ->
|
||||
{keys, union(old_type, new_type)}
|
||||
{keys, opt_union(old_type, new_type)}
|
||||
end)
|
||||
|
||||
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
|
||||
{Enum.map(negated -- matched, fn key -> {key, {none(), true}} end) ++ single, multiple}
|
||||
end
|
||||
|
||||
defp pairs(pairs, expected, stack, context, of_fun) do
|
||||
@@ -420,7 +430,7 @@ defmodule Module.Types.Of do
|
||||
|
||||
expected_value_type =
|
||||
with {[key], [], []} <- pos_neg_domain,
|
||||
{_, expected_value_type} <- map_fetch_key(expected, key) do
|
||||
{_optional?, expected_value_type} <- map_fetch_key(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
_ -> term()
|
||||
@@ -455,7 +465,8 @@ defmodule Module.Types.Of do
|
||||
[[]]
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
|
||||
products = cartesian_map(tail)
|
||||
for key <- keys, product <- products, do: [{key, {type, false}} | product]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -477,15 +488,15 @@ defmodule Module.Types.Of do
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
case map_fetch_key(expected, key) do
|
||||
{_, expected_value_type} -> expected_value_type
|
||||
{_optional?, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
{{key, {type, false}}, context}
|
||||
end)
|
||||
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
{closed_map([__struct__: {atom([struct]), false}] ++ args_types), context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -524,9 +535,16 @@ defmodule Module.Types.Of do
|
||||
# we introduce typed structs. They are only used by exceptions.
|
||||
def struct_type(struct, info, args_types \\ []) do
|
||||
term = dynamic()
|
||||
pairs = for %{field: field} <- info, do: {field, term}
|
||||
pairs = [{:__struct__, atom([struct])} | pairs]
|
||||
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
|
||||
pairs = for %{field: field} <- info, do: {field, {term, false}}
|
||||
pairs = [{:__struct__, {atom([struct]), false}} | pairs]
|
||||
|
||||
pairs =
|
||||
if args_types == [] do
|
||||
pairs
|
||||
else
|
||||
pairs ++ args_types
|
||||
end
|
||||
|
||||
closed_map(pairs)
|
||||
end
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ defmodule Module.Types.Pattern do
|
||||
do: previous
|
||||
|
||||
defp concat_previous(types, {list, descr}),
|
||||
do: {[types | list], union(descr, args_to_previous(types))}
|
||||
do: {[types | list], opt_union(descr, args_to_previous(types))}
|
||||
|
||||
defp args_to_previous([type]), do: upper_bound(type)
|
||||
defp args_to_previous(types), do: args_to_static_domain(types)
|
||||
@@ -39,8 +39,8 @@ defmodule Module.Types.Pattern do
|
||||
defp of_pattern_previous(types, {_, descr}, stack) do
|
||||
refined_types =
|
||||
case types do
|
||||
[type] -> [difference(type, descr)]
|
||||
[_ | _] -> args_to_domain(types) |> difference(descr) |> domain_to_flat_args(types)
|
||||
[type] -> [opt_difference(type, descr)]
|
||||
[_ | _] -> args_to_domain(types) |> opt_difference(descr) |> domain_to_flat_args(types)
|
||||
end
|
||||
|
||||
# check_previous? is an optimization. If types have not changed,
|
||||
@@ -349,7 +349,7 @@ defmodule Module.Types.Pattern do
|
||||
defp of_pattern_intersect([head | tail], index, acc, pattern_info, tag, stack, context) do
|
||||
{tree, expected, pattern} = head
|
||||
actual = of_pattern_tree(tree, stack, context)
|
||||
type = intersection(actual, expected)
|
||||
type = opt_intersection(actual, expected)
|
||||
|
||||
if empty?(type) do
|
||||
context = badpattern_error(pattern, index, tag, stack, context)
|
||||
@@ -498,7 +498,7 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
defp of_pattern_var([{:subpattern, key} | rest], type, context) do
|
||||
%{^key => subpattern} = context.subpatterns
|
||||
of_pattern_var(rest, intersection(type, subpattern), context)
|
||||
of_pattern_var(rest, opt_intersection(type, subpattern), context)
|
||||
end
|
||||
|
||||
defp of_pattern_var([:tail | rest], type, context) do
|
||||
@@ -518,7 +518,7 @@ defmodule Module.Types.Pattern do
|
||||
# This logic mirrors the code in `Apply.compare`
|
||||
cond do
|
||||
# If it is a singleton, we can always be precise
|
||||
singleton?(type) -> if polarity, do: type, else: negation(type)
|
||||
singleton?(type) -> if polarity, do: type, else: Module.Types.Descr.opt_negation(type)
|
||||
# We are checking for `not x == 1` or similar, we can't say anything about x
|
||||
polarity == false -> term()
|
||||
# We are checking for `x == 1`, make sure x is integer or float
|
||||
@@ -534,14 +534,18 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
defp of_pattern_tree({:open_map, static, dynamic}, stack, context) do
|
||||
dynamic =
|
||||
Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, stack, context)} end)
|
||||
Enum.map(dynamic, fn {key, value} ->
|
||||
{key, {of_pattern_tree(value, stack, context), false}}
|
||||
end)
|
||||
|
||||
open_map(static ++ dynamic)
|
||||
end
|
||||
|
||||
defp of_pattern_tree({:closed_map, static, dynamic}, stack, context) do
|
||||
dynamic =
|
||||
Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, stack, context)} end)
|
||||
Enum.map(dynamic, fn {key, value} ->
|
||||
{key, {of_pattern_tree(value, stack, context), false}}
|
||||
end)
|
||||
|
||||
closed_map(static ++ dynamic)
|
||||
end
|
||||
@@ -550,7 +554,7 @@ defmodule Module.Types.Pattern do
|
||||
tail
|
||||
|> Enum.reduce(
|
||||
of_pattern_tree(head, stack, context),
|
||||
&union(of_pattern_tree(&1, stack, context), &2)
|
||||
&opt_union(of_pattern_tree(&1, stack, context), &2)
|
||||
)
|
||||
|> non_empty_list(of_pattern_tree(suffix, stack, context))
|
||||
end
|
||||
@@ -558,7 +562,7 @@ defmodule Module.Types.Pattern do
|
||||
defp of_pattern_tree({:intersection, entries}, stack, context) do
|
||||
entries
|
||||
|> Enum.map(&of_pattern_tree(&1, stack, context))
|
||||
|> Enum.reduce(&intersection/2)
|
||||
|> Enum.reduce(&opt_intersection/2)
|
||||
end
|
||||
|
||||
defp of_pattern_tree({:var, version}, _stack, context) do
|
||||
@@ -690,7 +694,7 @@ defmodule Module.Types.Pattern do
|
||||
if static == [] do
|
||||
{:intersection, dynamic}
|
||||
else
|
||||
{:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}
|
||||
{:intersection, [Enum.reduce(static, &opt_intersection/2) | dynamic]}
|
||||
end
|
||||
|
||||
context = of_var(var, version, [%{root: return, expr: match}], context)
|
||||
@@ -721,20 +725,20 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
pairs = Map.new(pairs)
|
||||
term = term()
|
||||
static = [__struct__: atom([struct])]
|
||||
static = [__struct__: {atom([struct]), false}]
|
||||
dynamic = []
|
||||
|
||||
{static, dynamic} =
|
||||
Enum.reduce(info, {static, dynamic}, fn %{field: field}, {static, dynamic} ->
|
||||
case pairs do
|
||||
%{^field => value_type} when is_descr(value_type) ->
|
||||
{[{field, value_type} | static], dynamic}
|
||||
{[{field, {value_type, false}} | static], dynamic}
|
||||
|
||||
%{^field => value_type} ->
|
||||
{static, [{field, value_type} | dynamic]}
|
||||
|
||||
_ ->
|
||||
{[{field, term} | static], dynamic}
|
||||
{[{field, {term, false}} | static], dynamic}
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -772,8 +776,9 @@ defmodule Module.Types.Pattern do
|
||||
{refined, context} = of_match_var(var, atom(), expr, stack, context)
|
||||
|
||||
if compatible?(refined, atom()) do
|
||||
fields = [__struct__: {refined, false}]
|
||||
precise? = match?({:_, _, _}, var) or singleton?(refined)
|
||||
of_open_map(args, precise?, [__struct__: refined], [], path, stack, context)
|
||||
of_open_map(args, precise?, fields, [], path, stack, context)
|
||||
else
|
||||
error = {:badstruct, refined, expr, context}
|
||||
{error_type(), false, error(__MODULE__, error, meta, stack, context)}
|
||||
@@ -807,8 +812,12 @@ defmodule Module.Types.Pattern do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
|
||||
case Enum.reverse(reverse_path) do
|
||||
[%{root: :key} | _] -> {Of.var(var, context), false, context}
|
||||
path -> {{:var, version}, false, of_shared_var(var, version, true, path, context)}
|
||||
[%{root: :key} | _] ->
|
||||
{Of.var(var, context), false, context}
|
||||
|
||||
path ->
|
||||
precise? = singleton?(context.vars[version].type)
|
||||
{{:var, version}, precise?, of_shared_var(var, version, true, path, context)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -862,7 +871,7 @@ defmodule Module.Types.Pattern do
|
||||
precise? = precise? and value_precise?
|
||||
|
||||
if is_descr(value_type) do
|
||||
{precise?, [{key, value_type} | static], dynamic, context}
|
||||
{precise?, [{key, {value_type, false}} | static], dynamic, context}
|
||||
else
|
||||
{precise?, static, [{key, value_type} | dynamic], context}
|
||||
end
|
||||
@@ -952,13 +961,13 @@ defmodule Module.Types.Pattern do
|
||||
type =
|
||||
case {static, dynamic} do
|
||||
{static, []} when is_descr(suffix_type) ->
|
||||
non_empty_list(Enum.reduce(static, &union/2), suffix_type)
|
||||
non_empty_list(Enum.reduce(static, &opt_union/2), suffix_type)
|
||||
|
||||
{[], dynamic} ->
|
||||
{:non_empty_list, dynamic, suffix_type}
|
||||
|
||||
{static, dynamic} ->
|
||||
{:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix_type}
|
||||
{:non_empty_list, [Enum.reduce(static, &opt_union/2) | dynamic], suffix_type}
|
||||
end
|
||||
|
||||
{type, false, context}
|
||||
@@ -1027,9 +1036,10 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
|
||||
defp of_guards(guards, vars, stack, context) do
|
||||
stack = %{stack | reverse_arrow: :except_none}
|
||||
|
||||
context =
|
||||
init_pattern_info(context, %{
|
||||
guard_context: :andalso,
|
||||
parent_version: nil,
|
||||
vars: vars,
|
||||
subpatterns_vars: %{},
|
||||
@@ -1074,10 +1084,6 @@ defmodule Module.Types.Pattern do
|
||||
%{context | pattern_info: %{pattern_info | parent_version: parent_version}}}
|
||||
end
|
||||
|
||||
defp enable_conditional_mode(%{pattern_info: pattern_info} = context) do
|
||||
%{context | pattern_info: %{pattern_info | guard_context: :orelse}, conditional_vars: %{}}
|
||||
end
|
||||
|
||||
defp maybe_badguard(type, guard, stack, context) do
|
||||
case booleaness(type) do
|
||||
:maybe_both ->
|
||||
@@ -1129,7 +1135,7 @@ defmodule Module.Types.Pattern do
|
||||
Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2))
|
||||
|
||||
{suffix, context} = of_guard(suffix, term(), expr, stack, context)
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
@@ -1169,7 +1175,7 @@ defmodule Module.Types.Pattern do
|
||||
# var.field
|
||||
def of_guard({{:., _, [callee, key]}, _, []} = map_fetch, expected, expr, stack, context)
|
||||
when not is_atom(callee) do
|
||||
{type, context} = of_guard(callee, open_map([{key, expected}]), expr, stack, context)
|
||||
{type, context} = of_guard(callee, open_map([{key, {expected, false}}]), expr, stack, context)
|
||||
Of.map_fetch(map_fetch, type, key, stack, context)
|
||||
end
|
||||
|
||||
@@ -1270,7 +1276,7 @@ defmodule Module.Types.Pattern do
|
||||
# `:fail` as dynamic as its whole purpose is to cause failures.
|
||||
defp of_remote(:orelse, [left, :fail], _call, expected, stack, context) do
|
||||
{type, context} = of_guard(left, expected, left, stack, context)
|
||||
{union(type, @dynamic_fail), context}
|
||||
{opt_union(type, @dynamic_fail), context}
|
||||
end
|
||||
|
||||
defp of_remote(fun, _args, call, expected, stack, context)
|
||||
@@ -1303,7 +1309,8 @@ defmodule Module.Types.Pattern do
|
||||
of_guard(left, expected, left, stack, context)
|
||||
|
||||
true ->
|
||||
cond_context = enable_conditional_mode(context)
|
||||
cond_context = %{context | conditional_vars: %{}}
|
||||
stack = %{stack | reverse_arrow: :include_none}
|
||||
|
||||
# Compute the sure types, which are stored directly in the context
|
||||
{_type, context} = of_guard(left, boolean(), left, stack, context)
|
||||
@@ -1395,7 +1402,7 @@ defmodule Module.Types.Pattern do
|
||||
Apply.remote(:erlang, fun, args, expected, call, stack, context, &of_guard/5)
|
||||
end
|
||||
|
||||
defp unpack_op({{:., _, [:erlang, fun]}, _, [left, right]}, fun, acc) do
|
||||
defp unpack_op({{:., _, [_mod, fun]}, _, [left, right]}, fun, acc) do
|
||||
unpack_op(left, fun, unpack_op(right, fun, acc))
|
||||
end
|
||||
|
||||
@@ -1441,7 +1448,7 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
case disjoint? do
|
||||
true -> {type, context}
|
||||
false -> {union(to_abort, type), context}
|
||||
false -> {opt_union(to_abort, type), context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1456,7 +1463,7 @@ defmodule Module.Types.Pattern do
|
||||
of_guard(head, expected, head, stack, context)
|
||||
|
||||
acc_vars = [{vars, cond_vars} | acc_vars]
|
||||
of_logical_cond(tail, union(acc_type, type), acc_vars, expected, stack, context)
|
||||
of_logical_cond(tail, opt_union(acc_type, type), acc_vars, expected, stack, context)
|
||||
end
|
||||
|
||||
defp of_logical_cond([], acc_type, acc_vars, _expected, _stack, _context) do
|
||||
|
||||
@@ -39,7 +39,7 @@ defmodule Node do
|
||||
Currently supported options are:
|
||||
|
||||
* `:name_domain` - determines the host name part of the node name. If `:longnames`,
|
||||
fully qualified domain names will be used which also is the default.
|
||||
fully qualified domain names will be used, which is also the default.
|
||||
If `:shortnames`, only the short name of the host will be used.
|
||||
|
||||
* `:net_ticktime` - The tick time to use in seconds. Defaults to the value of the
|
||||
@@ -47,7 +47,7 @@ defmodule Node do
|
||||
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
|
||||
for more information.
|
||||
|
||||
* `net_tickintensity` - The tick intensity to use. Defaults to the value of the
|
||||
* `:net_tickintensity` - The tick intensity to use. Defaults to the value of the
|
||||
`net_tickintensity` configuration under Erlang's `kernel` application.
|
||||
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
|
||||
for more information.
|
||||
@@ -338,8 +338,6 @@ defmodule Node do
|
||||
|
||||
The default node is `Node.self/0`, the local node. If `node` is the local node,
|
||||
the function also sets the cookie of all other unknown nodes to `cookie`.
|
||||
|
||||
This function will raise `FunctionClauseError` if the given `node` is not alive.
|
||||
"""
|
||||
@spec set_cookie(t, atom) :: true
|
||||
def set_cookie(node \\ Node.self(), cookie) when is_atom(cookie) do
|
||||
|
||||
@@ -43,7 +43,7 @@ defmodule OptionParser do
|
||||
|
||||
defmodule ParseError do
|
||||
@moduledoc """
|
||||
An exception raised when parsing option fails.
|
||||
An exception raised when parsing an option fails.
|
||||
|
||||
For example, see `OptionParser.parse!/2`.
|
||||
"""
|
||||
@@ -387,7 +387,7 @@ defmodule OptionParser do
|
||||
do_parse(rest, config, new_opts, args, invalid, all?)
|
||||
|
||||
{:invalid, option, value, rest} ->
|
||||
# the option exist but it has wrong value
|
||||
# the option exists but it has wrong value
|
||||
do_parse(rest, config, opts, args, [{option, value} | invalid], all?)
|
||||
|
||||
{:undefined, option, _value, rest} ->
|
||||
@@ -463,7 +463,7 @@ defmodule OptionParser do
|
||||
defp next_with_config(["--" <> option | rest], config) do
|
||||
{option, value} = split_option(option)
|
||||
|
||||
if String.contains?(option, ["_"]) do
|
||||
if String.contains?(option, "_") do
|
||||
{:undefined, "--" <> option, value, rest}
|
||||
else
|
||||
tagged = tag_option(option, config)
|
||||
@@ -852,7 +852,7 @@ defmodule OptionParser do
|
||||
|> to_existing_key(allow_nonexistent_atoms?)
|
||||
end
|
||||
|
||||
defp to_existing_key(option, true), do: String.to_atom(option)
|
||||
defp to_existing_key(option, true), do: String.to_unsafe_atom(option)
|
||||
|
||||
defp to_existing_key(option, false) do
|
||||
try do
|
||||
|
||||
@@ -699,6 +699,32 @@ defmodule Path do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Safely joins two paths.
|
||||
|
||||
Returns `{:ok, path}` if `right` is safe to append to `left`, or `:error`
|
||||
otherwise. See `safe_relative/2` for the exact safety rules applied to `right`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.safe_join("foo", "bar")
|
||||
{:ok, "foo/bar"}
|
||||
|
||||
iex> Path.safe_join("foo", "../bar")
|
||||
:error
|
||||
|
||||
iex> Path.safe_join("foo", "/bar")
|
||||
:error
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec safe_join(t, t) :: {:ok, t} | :error
|
||||
def safe_join(left, right) do
|
||||
with {:ok, right} <- safe_relative(right, left) do
|
||||
{:ok, join(left, right)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Splits the path into a list at the path separator.
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ defmodule Port do
|
||||
* `{pid, {:connect, new_pid}}` - sets the `new_pid` as the new owner of
|
||||
the port. Once a port is opened, the port is linked and connected to the
|
||||
caller process and communication to the port only happens through the
|
||||
connected process. This message makes `new_pid` the new connected processes.
|
||||
connected process. This message makes `new_pid` the new connected process.
|
||||
Unless the port is dead, the port will reply to the old owner with
|
||||
`{port, :connected}`. See `connect/2`.
|
||||
|
||||
@@ -88,10 +88,10 @@ defmodule Port do
|
||||
> On Unix systems, arguments are passed to a new operating system
|
||||
> process as an array of strings but on Windows it is up to the child
|
||||
> process to parse them and some Windows programs may apply their own
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing.
|
||||
>
|
||||
> This is particularly troublesome when invoking `.bat` or `.com` files
|
||||
> as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> This is particularly troublesome when invoking `.bat`, `.cmd`, or `.com`
|
||||
> files as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> vulnerable to malicious input and can be used to run arbitrary shell
|
||||
> commands.
|
||||
>
|
||||
@@ -99,7 +99,7 @@ defmodule Port do
|
||||
> files or `.com` applications, you must not pass untrusted input as
|
||||
> arguments to the program. You may avoid accidentally executing them
|
||||
> by explicitly passing the extension of the program you want to run,
|
||||
> such as `.exe`, and double check the program is indeed not a batch
|
||||
> such as `.exe`, and double-checking the program is indeed not a batch
|
||||
> file or `.com` application.
|
||||
>
|
||||
> This affects both `spawn` and `spawn_executable`.
|
||||
@@ -156,7 +156,7 @@ defmodule Port do
|
||||
|
||||
We do not always have control over how third-party software terminates.
|
||||
If necessary, one workaround is to wrap the child application in a script that
|
||||
checks whether stdin has been closed. Here is such a script that has been
|
||||
checks whether stdin has been closed. Here is such a script that has been
|
||||
verified to work on bash shells:
|
||||
|
||||
#!/usr/bin/env bash
|
||||
|
||||
@@ -79,10 +79,11 @@ defmodule Process do
|
||||
@typedoc """
|
||||
A process destination.
|
||||
|
||||
A remote or local PID, a local port, a locally registered name, or a tuple in
|
||||
the form of `{registered_name, node}` for a registered name at another node.
|
||||
A remote or local PID, a local port, a process alias, a locally registered name,
|
||||
or a tuple in the form of `{registered_name, node}` for a registered name at
|
||||
another node.
|
||||
"""
|
||||
@type dest :: pid | port | (registered_name :: atom) | {registered_name :: atom, node}
|
||||
@type dest :: pid | port | alias | (registered_name :: atom) | {registered_name :: atom, node}
|
||||
|
||||
@doc """
|
||||
Tells whether the given process is alive on the local node.
|
||||
@@ -226,7 +227,7 @@ defmodule Process do
|
||||
>
|
||||
> The functions `Kernel.exit/1` and `Process.exit/2` are
|
||||
> named similarly but provide very different functionalities. The
|
||||
> `Kernel:exit/1` function should be used when the intent is to stop the current
|
||||
> `Kernel.exit/1` function should be used when the intent is to stop the current
|
||||
> process while `Process.exit/2` should be used when the intent is to send an
|
||||
> exit signal to another process. Note also that `Kernel.exit/1` can be caught
|
||||
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and
|
||||
@@ -339,7 +340,7 @@ defmodule Process do
|
||||
@doc """
|
||||
Sends a message to the given `dest`.
|
||||
|
||||
`dest` may be a remote or local PID, a local port, a locally
|
||||
`dest` may be a remote or local PID, a local port, a process alias, a locally
|
||||
registered name, or a tuple in the form of `{registered_name, node}` for a
|
||||
registered name at another node.
|
||||
|
||||
@@ -830,12 +831,16 @@ defmodule Process do
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec flag(:async_dist, boolean) :: boolean
|
||||
@spec flag(:error_handler, module) :: module
|
||||
@spec flag(:fullsweep_after, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:max_heap_size, heap_size) :: heap_size
|
||||
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
|
||||
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
|
||||
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:monitor_nodes, term) :: term
|
||||
@spec flag({:monitor_nodes, term}, term) :: term
|
||||
@spec flag(:priority, priority_level) :: priority_level
|
||||
@spec flag(:save_calls, 0..10_000) :: 0..10_000
|
||||
@spec flag(:sensitive, boolean) :: boolean
|
||||
@@ -1002,7 +1007,7 @@ defmodule Process do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add a descriptive term to the current process.
|
||||
Adds a descriptive term to the current process.
|
||||
|
||||
The term does not need to be unique, and will be shown in Observer and in
|
||||
crash logs.
|
||||
|
||||
@@ -215,7 +215,7 @@ defmodule Protocol do
|
||||
...
|
||||
end
|
||||
|
||||
If you are using `Mix.install/2`, you can do by passing the `consolidate_protocols`
|
||||
If you are using `Mix.install/2`, you can do so by passing the `consolidate_protocols`
|
||||
option:
|
||||
|
||||
Mix.install(
|
||||
@@ -282,7 +282,9 @@ defmodule Protocol do
|
||||
type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity))
|
||||
type_args = [quote(do: t) | type_args]
|
||||
|
||||
to_var = fn pos -> Macro.var(String.to_atom("arg" <> Integer.to_string(pos)), __MODULE__) end
|
||||
to_var = fn pos ->
|
||||
Macro.var(String.to_unsafe_atom("arg" <> Integer.to_string(pos)), __MODULE__)
|
||||
end
|
||||
|
||||
call_args = :lists.map(to_var, :lists.seq(2, arity))
|
||||
call_args = [quote(do: term) | call_args]
|
||||
@@ -689,15 +691,15 @@ defmodule Protocol do
|
||||
structs_domain =
|
||||
case structs do
|
||||
[] -> Descr.none()
|
||||
_ -> Descr.open_map(__struct__: Descr.atom(structs))
|
||||
_ -> Descr.open_map(__struct__: {Descr.atom(structs), false})
|
||||
end
|
||||
|
||||
domain =
|
||||
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
|
||||
Descr.union(Module.Types.Of.impl(impl, :open), acc)
|
||||
Descr.opt_union(Module.Types.Of.impl(impl, :open), acc)
|
||||
end)
|
||||
|
||||
not_domain = Descr.negation(domain)
|
||||
not_domain = Descr.opt_negation(domain)
|
||||
|
||||
if Any in types do
|
||||
clauses =
|
||||
@@ -1228,7 +1230,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
def __concat__(left, right) when is_binary(right) do
|
||||
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
|
||||
String.to_unsafe_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
|
||||
end
|
||||
|
||||
defp ensure_prefix("Elixir." <> _ = left), do: left
|
||||
|
||||
+20
-9
@@ -258,7 +258,7 @@ defmodule Range do
|
||||
def size(range)
|
||||
def size(first..last//step) when step > 0 and first > last, do: 0
|
||||
def size(first..last//step) when step < 0 and first < last, do: 0
|
||||
def size(first..last//step), do: abs(div(last - first, step)) + 1
|
||||
def size(first..last//step), do: div(last - first, step) + 1
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def size(%{__struct__: Range, first: first, last: last} = range) do
|
||||
@@ -476,8 +476,8 @@ defmodule Range do
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec disjoint?(t, t) :: boolean
|
||||
def disjoint?(first1..last1//step1 = range1, first2..last2//step2 = range2) do
|
||||
if size(range1) == 0 or size(range2) == 0 do
|
||||
def disjoint?(first1..last1//step1, first2..last2//step2) do
|
||||
if empty?(first1, last1, step1) or empty?(first2, last2, step2) do
|
||||
true
|
||||
else
|
||||
{first1, last1, step1} = normalize(first1, last1, step1)
|
||||
@@ -487,7 +487,7 @@ defmodule Range do
|
||||
last2 < first1 or last1 < first2 ->
|
||||
true
|
||||
|
||||
abs(step1) == 1 and abs(step2) == 1 ->
|
||||
step1 == 1 and step2 == 1 ->
|
||||
false
|
||||
|
||||
true ->
|
||||
@@ -498,9 +498,9 @@ defmodule Range do
|
||||
|
||||
if rem(first2 - first1, gcd) == 0 do
|
||||
c = first1 - first2 + step2 - step1
|
||||
t1 = -c / step2 * u
|
||||
t2 = -c / step1 * v
|
||||
t = max(floor(t1) + 1, floor(t2) + 1)
|
||||
t1 = Integer.floor_div(-c * u, step2)
|
||||
t2 = Integer.floor_div(-c * v, step1)
|
||||
t = max(t1 + 1, t2 + 1)
|
||||
x = div(c * u + t * step2, gcd) - 1
|
||||
y = div(c * v + t * step1, gcd) - 1
|
||||
|
||||
@@ -513,9 +513,20 @@ defmodule Range do
|
||||
end
|
||||
end
|
||||
|
||||
@compile inline: [normalize: 3]
|
||||
@compile inline: [empty?: 3, normalize: 3]
|
||||
defp empty?(first, last, step) do
|
||||
case step > 0 do
|
||||
true -> first > last
|
||||
false -> first < last
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize(first, last, step) when first > last,
|
||||
do: {first - abs(div(first - last, step) * step), first, -step}
|
||||
do: {first - div(first - last, step) * step, first, -step}
|
||||
|
||||
# A single-element range holds the same element regardless of the step, so
|
||||
# make the step positive to keep the progression in disjoint?/2 increasing.
|
||||
defp normalize(first, last, step) when first == last, do: {first, last, abs(step)}
|
||||
|
||||
defp normalize(first, last, step), do: {first, last, step}
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ defmodule Record do
|
||||
fields
|
||||
end
|
||||
|
||||
# Normalizes of record fields to have default values.
|
||||
# Normalizes record fields to have default values.
|
||||
defp fields(kind, fields) do
|
||||
normalizer_fun = fn
|
||||
{key, value} when is_atom(key) ->
|
||||
|
||||
@@ -40,7 +40,7 @@ defmodule Record.Extractor do
|
||||
defp from_lib_file(file) do
|
||||
[app | path] = :filename.split(String.to_charlist(file))
|
||||
|
||||
case :code.lib_dir(List.to_atom(app)) do
|
||||
case :code.lib_dir(List.to_unsafe_atom(app)) do
|
||||
{:error, _} ->
|
||||
raise ArgumentError, "lib file #{file} could not be found"
|
||||
|
||||
|
||||
@@ -615,8 +615,8 @@ defmodule Regex do
|
||||
defp safe_run(%Regex{re_pattern: re_pattern} = regex, string, options) do
|
||||
# TODO: Remove me when Erlang/OTP 28+ is required
|
||||
# This allows regexes precompiled on Erlang/OTP 27- to work on Erlang/OTP 28+
|
||||
with true <- :erlang.system_info(:otp_release) >= [?2, ?8],
|
||||
{:re_pattern, _, _, _, <<_::bitstring>>} <- re_pattern do
|
||||
with {:re_pattern, _, _, _, <<_::bitstring>>} <- re_pattern,
|
||||
true <- :erlang.system_info(:otp_release) >= [?2, ?8] do
|
||||
%Regex{source: source, opts: compile_opts} = regex
|
||||
:re.run(string, source, compile_opts ++ options)
|
||||
else
|
||||
|
||||
+88
-50
@@ -21,8 +21,8 @@ defmodule Registry do
|
||||
the `:via` option), storing properties, custom dispatching rules, or a pubsub
|
||||
implementation. We explore some of those use cases below.
|
||||
|
||||
The registry may also be transparently partitioned, which provides
|
||||
more scalable behavior for running registries on highly concurrent
|
||||
The registry may also be transparently partitioned with the `:partitions` option,
|
||||
which provides more scalable behavior for running registries in highly concurrent
|
||||
environments with thousands or millions of entries.
|
||||
|
||||
## Using in `:via`
|
||||
@@ -79,7 +79,7 @@ defmodule Registry do
|
||||
|
||||
`Registry` has a dispatch mechanism that allows developers to implement custom
|
||||
dispatch logic triggered from the caller. For example, let's say we have a
|
||||
duplicate registry started as so:
|
||||
duplicate registry started as follows:
|
||||
|
||||
{:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.DispatcherTest)
|
||||
|
||||
@@ -172,7 +172,7 @@ defmodule Registry do
|
||||
the cost of delayed unsubscription. For example, if a process crashes,
|
||||
its keys are automatically removed from the registry but the change may
|
||||
not propagate immediately. This means certain operations may return processes
|
||||
that are already dead. When such may happen, it will be explicitly stated
|
||||
that are already dead. When this can happen, it will be explicitly stated
|
||||
in the function documentation.
|
||||
|
||||
However, keep in mind those cases are typically not an issue. After all, a
|
||||
@@ -218,7 +218,7 @@ defmodule Registry do
|
||||
@typedoc "A list of guards to be evaluated when matching on objects in a registry"
|
||||
@type guards :: [guard]
|
||||
|
||||
@typedoc "A pattern used to representing the output format part of a match spec"
|
||||
@typedoc "A pattern used to represent the output format part of a match spec"
|
||||
@type body :: [term]
|
||||
|
||||
@typedoc "A full match spec used when selecting objects in the registry"
|
||||
@@ -230,6 +230,7 @@ defmodule Registry do
|
||||
| {:name, registry}
|
||||
| {:partitions, pos_integer}
|
||||
| {:listeners, [atom]}
|
||||
| {:compressed, boolean}
|
||||
| {:meta, [{meta_key, meta_value}]}
|
||||
|
||||
@typedoc """
|
||||
@@ -239,8 +240,8 @@ defmodule Registry do
|
||||
"""
|
||||
@typedoc since: "1.15.0"
|
||||
@type listener_message ::
|
||||
{:register, registry, key, registry_partition :: pid, value}
|
||||
| {:unregister, registry, key, registry_partition :: pid}
|
||||
{:register, registry, key, pid, value}
|
||||
| {:unregister, registry, key, pid}
|
||||
|
||||
@typedoc """
|
||||
Options used for `dispatch/4`.
|
||||
@@ -346,7 +347,7 @@ defmodule Registry do
|
||||
|
||||
The registry requires the following keys:
|
||||
|
||||
* `:keys` - chooses if keys are `:unique`, `:duplicate`,
|
||||
* `:keys` - controls whether keys are `:unique`, `:duplicate`,
|
||||
`{:duplicate, :key}`, or `{:duplicate, :pid}`
|
||||
* `:name` - the name of the registry and its tables
|
||||
|
||||
@@ -357,6 +358,8 @@ defmodule Registry do
|
||||
and unregister events. The registered process must be monitored by the
|
||||
listener if the listener wants to be notified if the registered process
|
||||
crashes. Messages sent to listeners are of type `t:listener_message/0`.
|
||||
* `:compressed` - turns on compression for registry's keys ETS tables, reducing
|
||||
memory usage at the cost of additional CPU usage. Defaults to `false`.
|
||||
* `:meta` - a keyword list of metadata to be attached to the registry.
|
||||
|
||||
For `:duplicate` registries, you can specify the partitioning strategy
|
||||
@@ -636,7 +639,7 @@ defmodule Registry do
|
||||
@doc """
|
||||
Finds the `{pid, value}` pair for the given `key` in `registry` in no particular order.
|
||||
|
||||
An empty list if there is no match.
|
||||
Returns an empty list if there is no match.
|
||||
|
||||
For unique registries, a single partition lookup is necessary. For
|
||||
duplicate registries, all partitions must be looked up.
|
||||
@@ -734,7 +737,7 @@ defmodule Registry do
|
||||
|
||||
This function is useful only when spawning processes is not an option,
|
||||
for example, when copying the data to another process could be too
|
||||
expensive. Or when the work must be done within the current process
|
||||
expensive, or when the work must be done within the current process
|
||||
for other reasons. In such cases, this function provides a scalable
|
||||
mechanism for managing locks on top of the registry's infrastructure.
|
||||
|
||||
@@ -748,6 +751,7 @@ defmodule Registry do
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec lock(registry, term, (-> result)) :: result when result: var
|
||||
def lock(registry, lock_key, function)
|
||||
when is_atom(registry) and is_function(function, 0) do
|
||||
{_kind, partitions, _, pid_ets, _} = info!(registry)
|
||||
@@ -768,7 +772,7 @@ defmodule Registry do
|
||||
For example the `$1 > 1` guard condition would be expressed as the `{:>, :"$1", 1}` tuple.
|
||||
Please note that guard conditions will work only for assigned
|
||||
variables like `:"$1"`, `:"$2"`, and so forth.
|
||||
Avoid usage of special match variables `:"$_"` and `:"$$"`, because it might not work as expected.
|
||||
Avoid using of special match variables `:"$_"` and `:"$$"`, because they might not work as expected.
|
||||
|
||||
An empty list will be returned if there is no match.
|
||||
|
||||
@@ -838,7 +842,7 @@ defmodule Registry do
|
||||
iex> Registry.keys(Registry.UniqueKeysTest, self())
|
||||
["hello"]
|
||||
|
||||
Such is possible for duplicate registries though:
|
||||
This is possible for duplicate registries though:
|
||||
|
||||
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateKeysTest)
|
||||
iex> Registry.keys(Registry.DuplicateKeysTest, self())
|
||||
@@ -1127,22 +1131,26 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
|
||||
else
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
|
||||
|
||||
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
|
||||
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
|
||||
else
|
||||
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
|
||||
end
|
||||
end
|
||||
|
||||
defp unregister_match_specs(_kind, key, self, pattern, guards) do
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
if exact_match_key?(key) do
|
||||
{[{{key, {self, :_}}, [], [true]}], [{{key, {self, pattern}}, guards, [true]}]}
|
||||
else
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
|
||||
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
|
||||
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
|
||||
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
|
||||
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1456,12 +1464,12 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Select key, pid, and values registered using full match specs.
|
||||
Selects key, pid, and values registered using full match specs.
|
||||
|
||||
The `spec` consists of a list of three part tuples, in the shape of `[{match_pattern, guards, body}]`.
|
||||
The `spec` consists of a list of three-part tuples, in the shape of `[{match_pattern, guards, body}]`.
|
||||
|
||||
The first part, the match pattern, must be a tuple that will match the structure of the
|
||||
the data stored in the registry, which is `{key, pid, value}`. The atom `:_` can be used to
|
||||
data stored in the registry, which is `{key, pid, value}`. The atom `:_` can be used to
|
||||
ignore a given value or tuple element, while the atom `:"$1"` can be used to temporarily
|
||||
assign part of pattern to a variable for a subsequent comparison. This can be combined
|
||||
like `{:"$1", :_, :_}`.
|
||||
@@ -1473,7 +1481,7 @@ defmodule Registry do
|
||||
variables like `:"$1"`, `:"$2"`, and so forth.
|
||||
|
||||
The third part, the body, is a list of shapes of the returned entries. Like guards, you have access to
|
||||
assigned variables like `:"$1"`, which you can combine with hard-coded values to freely shape entries
|
||||
assigned variables like `:"$1"`, which you can combine with hard-coded values to freely shape entries.
|
||||
Note that tuples have to be wrapped in an additional tuple. To get a result format like
|
||||
`%{key: key, pid: pid, value: value}`, assuming you bound those variables in order in the match part,
|
||||
you would provide a body like `[%{key: :"$1", pid: :"$2", value: :"$3"}]`. Like guards, you can use
|
||||
@@ -1484,8 +1492,9 @@ defmodule Registry do
|
||||
that reference the underlying entry structure via `:"$_"` will return different results.
|
||||
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
|
||||
|
||||
Note that for large registries with many partitions this will be costly as it builds the result by
|
||||
concatenating all the partitions.
|
||||
Note that for registries with many partitions, the result of each partition is concatenated.
|
||||
Therefore selecting thousands of entries across partitions may become an expensive operation
|
||||
and we recommend profiling/benchmarking whenever selecting too many entries.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1618,11 +1627,11 @@ defmodule Registry do
|
||||
|
||||
defp lookup_second({:duplicate, :key}, ets, key) do
|
||||
spec =
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
|
||||
else
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
|
||||
else
|
||||
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
|
||||
end
|
||||
|
||||
try do
|
||||
@@ -1664,25 +1673,24 @@ defmodule Registry do
|
||||
def __unregister__(table, match, pos, ordered \\ false)
|
||||
|
||||
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
:ets.match_delete(table, {{key, pid, :_}, :_})
|
||||
else
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
|
||||
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
|
||||
else
|
||||
:ets.match_delete(table, {{key, pid, :_}, :_})
|
||||
end
|
||||
end
|
||||
|
||||
def __unregister__(table, match, pos, false = _ordered) do
|
||||
key = :erlang.element(pos, match)
|
||||
|
||||
# We need to perform an element comparison if we have a special atom key.
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
:ets.match_delete(table, match)
|
||||
else
|
||||
match = :erlang.setelement(pos, match, :_)
|
||||
guard = {:"=:=", {:element, pos, :"$_"}, {:const, key}}
|
||||
:ets.select_delete(table, [{match, [guard], [true]}]) >= 0
|
||||
else
|
||||
:ets.match_delete(table, match)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1693,42 +1701,72 @@ defmodule Registry do
|
||||
defp match_spec({:duplicate, :key}, key, pattern, guards) do
|
||||
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
|
||||
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
[{{{key, :_, :_}, pattern}, guards, body}]
|
||||
else
|
||||
guards = [
|
||||
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
|
||||
]
|
||||
|
||||
[{{{:_, :_, :_}, pattern}, guards, body}]
|
||||
else
|
||||
[{{{key, :_, :_}, pattern}, guards, body}]
|
||||
end
|
||||
end
|
||||
|
||||
defp match_spec(_kind, key, pattern, guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
if exact_match_key?(key) do
|
||||
[{{key, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
else
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
end
|
||||
end
|
||||
|
||||
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
[{{{key, :_, :_}, pattern}, guards, [true]}]
|
||||
else
|
||||
guards = [
|
||||
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
|
||||
]
|
||||
|
||||
[{{{:_, :_, :_}, pattern}, guards, [true]}]
|
||||
else
|
||||
[{{{key, :_, :_}, pattern}, guards, [true]}]
|
||||
end
|
||||
end
|
||||
|
||||
defp count_match_spec(_kind, key, pattern, guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [true]}]
|
||||
if exact_match_key?(key) do
|
||||
[{{key, {:_, pattern}}, guards, [true]}]
|
||||
else
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [true]}]
|
||||
end
|
||||
end
|
||||
|
||||
defp reserved_atom?("_"), do: true
|
||||
defp reserved_atom?("$" <> _), do: true
|
||||
defp reserved_atom?(_), do: false
|
||||
# Only keys that remain exact when embedded in an ETS match head can use
|
||||
# the optimized direct-key match path.
|
||||
defp exact_match_key?(term) when is_map(term), do: false
|
||||
defp exact_match_key?([]), do: true
|
||||
defp exact_match_key?([head | tail]), do: exact_match_key?(head) and exact_match_key?(tail)
|
||||
|
||||
defp exact_match_key?(term) when is_tuple(term) do
|
||||
exact_match_tuple?(term, tuple_size(term))
|
||||
end
|
||||
|
||||
defp exact_match_key?(term) when is_atom(term) do
|
||||
case Atom.to_string(term) do
|
||||
"_" -> false
|
||||
"$" <> _ -> false
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
defp exact_match_key?(_term), do: true
|
||||
|
||||
defp exact_match_tuple?(_term, 0), do: true
|
||||
|
||||
defp exact_match_tuple?(term, index) do
|
||||
exact_match_key?(:erlang.element(index, term)) and exact_match_tuple?(term, index - 1)
|
||||
end
|
||||
|
||||
defp ordered?({:duplicate, :key}), do: true
|
||||
defp ordered?(_), do: false
|
||||
@@ -1785,7 +1823,7 @@ defmodule Registry.Partition do
|
||||
@key_info -2
|
||||
|
||||
@doc """
|
||||
Returns the name of key partition table.
|
||||
Returns the name of the key partition table.
|
||||
"""
|
||||
@spec key_name(atom, non_neg_integer) :: atom
|
||||
def key_name(registry, partition) do
|
||||
@@ -1793,7 +1831,7 @@ defmodule Registry.Partition do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the name of pid partition table.
|
||||
Returns the name of the pid partition table.
|
||||
"""
|
||||
@spec pid_name(atom, non_neg_integer) :: atom
|
||||
def pid_name(name, partition) do
|
||||
@@ -1810,7 +1848,7 @@ defmodule Registry.Partition do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs function with a lock.
|
||||
Runs the function with a lock.
|
||||
"""
|
||||
def lock(pid, key, lock) do
|
||||
ref = GenServer.call(pid, {:lock, key})
|
||||
|
||||
+134
-35
@@ -234,12 +234,12 @@ defmodule String do
|
||||
to the definition of the encoding) is encountered, only one
|
||||
code point needs to be rejected.
|
||||
|
||||
This module relies on this behavior to ignore such invalid
|
||||
characters. For example, `length/1` will return
|
||||
a correct result even if an invalid code point is fed into it.
|
||||
Most functions in this module perform self-synchronization too.
|
||||
For example, `downcase/1` will return a downcased string, with
|
||||
any invalid codepoints preserved at their location.
|
||||
|
||||
In other words, this module expects invalid data to be detected
|
||||
elsewhere, usually when retrieving data from the external source.
|
||||
at the boundary, typically when retrieving data from the external source.
|
||||
For example, a driver that reads strings from a database will be
|
||||
responsible to check the validity of the encoding. `String.chunk/2`
|
||||
can be used for breaking a string into valid and invalid parts.
|
||||
@@ -935,18 +935,28 @@ defmodule String do
|
||||
end
|
||||
|
||||
def upcase(string, :ascii) when is_binary(string) do
|
||||
IO.iodata_to_binary(upcase_ascii(string))
|
||||
upcase_ascii(string, string, 0)
|
||||
end
|
||||
|
||||
def upcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
|
||||
String.Unicode.upcase(string, [], mode)
|
||||
end
|
||||
|
||||
defp upcase_ascii(<<char, rest::bits>>) when char >= ?a and char <= ?z,
|
||||
do: [char - 32 | upcase_ascii(rest)]
|
||||
defp upcase_ascii(<<char, rest::bits>>, original, skip) when char >= ?a and char <= ?z do
|
||||
prefix = binary_part(original, 0, skip)
|
||||
upcase_ascii(rest, <<prefix::binary, char - 32>>)
|
||||
end
|
||||
|
||||
defp upcase_ascii(<<char, rest::bits>>), do: [char | upcase_ascii(rest)]
|
||||
defp upcase_ascii(<<>>), do: []
|
||||
defp upcase_ascii(<<_char, rest::bits>>, original, skip),
|
||||
do: upcase_ascii(rest, original, skip + 1)
|
||||
|
||||
defp upcase_ascii(<<>>, original, _skip), do: original
|
||||
|
||||
defp upcase_ascii(<<char, rest::bits>>, acc) when char >= ?a and char <= ?z,
|
||||
do: upcase_ascii(rest, <<acc::binary, char - 32>>)
|
||||
|
||||
defp upcase_ascii(<<char, rest::bits>>, acc), do: upcase_ascii(rest, <<acc::binary, char>>)
|
||||
defp upcase_ascii(<<>>, acc), do: acc
|
||||
|
||||
@doc """
|
||||
Converts all characters in the given string to lowercase according to `mode`.
|
||||
@@ -1005,18 +1015,32 @@ defmodule String do
|
||||
end
|
||||
|
||||
def downcase(string, :ascii) when is_binary(string) do
|
||||
IO.iodata_to_binary(downcase_ascii(string))
|
||||
downcase_ascii(string, string, 0)
|
||||
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
|
||||
|
||||
defp downcase_ascii(<<char, rest::bits>>) when char >= ?A and char <= ?Z,
|
||||
do: [char + 32 | downcase_ascii(rest)]
|
||||
defp downcase_ascii(<<char, rest::bits>>, original, skip) when char >= ?A and char <= ?Z do
|
||||
prefix = binary_part(original, 0, skip)
|
||||
downcase_ascii(rest, <<prefix::binary, char + 32>>)
|
||||
end
|
||||
|
||||
defp downcase_ascii(<<char, rest::bits>>), do: [char | downcase_ascii(rest)]
|
||||
defp downcase_ascii(<<>>), do: []
|
||||
defp downcase_ascii(<<_char, rest::bits>>, original, skip),
|
||||
do: downcase_ascii(rest, original, skip + 1)
|
||||
|
||||
defp downcase_ascii(<<>>, original, _skip), do: original
|
||||
|
||||
defp downcase_ascii(<<char, rest::bits>>, acc) when char >= ?A and char <= ?Z,
|
||||
do: downcase_ascii(rest, <<acc::binary, char + 32>>)
|
||||
|
||||
defp downcase_ascii(<<char, rest::bits>>, acc), do: downcase_ascii(rest, <<acc::binary, char>>)
|
||||
defp downcase_ascii(<<>>, acc), do: acc
|
||||
|
||||
@doc """
|
||||
Converts the first character in the given string to
|
||||
@@ -1058,19 +1082,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
|
||||
@@ -1082,7 +1126,7 @@ defmodule String do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Replaces all leading occurrences of `match` by `replacement` of `match` in `string`.
|
||||
Replaces all leading occurrences of `match` by `replacement` in `string`.
|
||||
|
||||
Returns the string untouched if there are no occurrences.
|
||||
|
||||
@@ -1726,7 +1770,7 @@ defmodule String do
|
||||
end
|
||||
|
||||
defp do_replace(subject, [], _, n) do
|
||||
[binary_part(subject, n, byte_size(subject) - n)]
|
||||
binary_part(subject, n, byte_size(subject) - n)
|
||||
end
|
||||
|
||||
defp do_replace(subject, [{start, length} | matches], replacement, n) do
|
||||
@@ -1784,7 +1828,7 @@ defmodule String do
|
||||
do: :unicode.characters_to_binary(acc)
|
||||
|
||||
defp do_reverse({:error, <<byte, rest::bits>>}, acc),
|
||||
do: :unicode.characters_to_binary(acc) <> <<byte>> <> do_reverse(:unicode_util.gc(rest), [])
|
||||
do: do_reverse(:unicode_util.gc(rest), []) <> <<byte>> <> :unicode.characters_to_binary(acc)
|
||||
|
||||
@doc """
|
||||
Returns a string `subject` repeated `n` times.
|
||||
@@ -2410,7 +2454,7 @@ defmodule String do
|
||||
""
|
||||
|
||||
"""
|
||||
@spec slice(t, integer, non_neg_integer) :: grapheme
|
||||
@spec slice(t, integer, non_neg_integer) :: t
|
||||
|
||||
def slice(_, _, 0) do
|
||||
""
|
||||
@@ -2962,29 +3006,38 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate String.to_atom, List.to_atom, and atom interpolation in v1.25
|
||||
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
|
||||
def to_atom(string) when is_binary(string) do
|
||||
:erlang.binary_to_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a string to an existing atom or creates a new one.
|
||||
|
||||
Warning: this function creates atoms dynamically and atoms are
|
||||
not garbage-collected. Therefore, `string` should not be an
|
||||
untrusted value, such as input received from a socket or during
|
||||
a web request. Consider using `to_existing_atom/1` instead.
|
||||
> #### Dynamic Atom Creation {: .warning}
|
||||
>
|
||||
> This function creates atoms dynamically and atoms are
|
||||
> not garbage-collected. Therefore, `string` should not be an
|
||||
> untrusted value, such as input received from a socket or during
|
||||
> a web request. Consider using `to_existing_atom/1` instead.
|
||||
|
||||
By default, the maximum number of atoms is `1_048_576`. This limit
|
||||
can be raised or lowered using the VM option `+t`.
|
||||
|
||||
The maximum atom size is of 255 Unicode code points.
|
||||
The maximum atom size is 255 Unicode code points.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.to_atom("my_atom")
|
||||
iex> String.to_unsafe_atom("my_atom")
|
||||
:my_atom
|
||||
|
||||
"""
|
||||
@spec to_atom(String.t()) :: atom
|
||||
def to_atom(string) when is_binary(string) do
|
||||
@doc since: "1.21.0"
|
||||
@spec to_unsafe_atom(String.t()) :: atom
|
||||
def to_unsafe_atom(string) when is_binary(string) do
|
||||
:erlang.binary_to_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@@ -2992,7 +3045,9 @@ defmodule String do
|
||||
Converts a string to an existing atom or raises if
|
||||
the atom does not exist.
|
||||
|
||||
The maximum atom size is of 255 Unicode code points.
|
||||
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
|
||||
|
||||
The maximum atom size is 255 Unicode code points.
|
||||
Raises an `ArgumentError` if the atom does not exist.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -3021,6 +3076,47 @@ defmodule String do
|
||||
:erlang.binary_to_existing_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a string to one of the `allowed_atoms` or raises.
|
||||
|
||||
Raises an `ArgumentError` if the atom either does not exist or is not within
|
||||
the existing list.
|
||||
|
||||
This should be preferred to `to_existing_atom/1` if the list is known upfront,
|
||||
since there is no risk that the atom has not been loaded.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.to_existing_atom("foo", [:foo, :bar])
|
||||
:foo
|
||||
|
||||
iex> String.to_existing_atom("unknown", [:foo, :bar])
|
||||
** (ArgumentError) unexpected value: \"unknown\", the allowed atoms are: [:foo, :bar]
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_existing_atom(String.t(), nonempty_list(a)) :: a when a: atom()
|
||||
def to_existing_atom(string, [_ | _] = allowed_atoms) when is_binary(string) do
|
||||
atom = :erlang.binary_to_existing_atom(string, :utf8)
|
||||
|
||||
if atom not in allowed_atoms do
|
||||
to_existing_atom_unexpected(string, allowed_atoms)
|
||||
end
|
||||
|
||||
atom
|
||||
end
|
||||
|
||||
# used just to have a less cryptic stacktrace and consistent error
|
||||
@doc false
|
||||
def __to_existing_atom__(string, allowed_atoms) do
|
||||
to_existing_atom_unexpected(string, allowed_atoms)
|
||||
end
|
||||
|
||||
defp to_existing_atom_unexpected(string, allowed_atoms) do
|
||||
raise ArgumentError,
|
||||
"unexpected value: #{inspect(string)}, the allowed atoms are: #{inspect(allowed_atoms)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an integer whose text representation is `string`.
|
||||
|
||||
@@ -3118,10 +3214,13 @@ defmodule String do
|
||||
0.75
|
||||
iex> String.bag_distance("abcd", "abcd")
|
||||
1.0
|
||||
iex> String.bag_distance("", "")
|
||||
1.0
|
||||
|
||||
"""
|
||||
@spec bag_distance(t, t) :: float
|
||||
@doc since: "1.8.0"
|
||||
def bag_distance("", ""), do: 1.0
|
||||
def bag_distance(_string, ""), do: 0.0
|
||||
def bag_distance("", _string), do: 0.0
|
||||
|
||||
@@ -3130,9 +3229,9 @@ defmodule String do
|
||||
{bag2, length2} = string_to_bag(string2, %{}, 0)
|
||||
|
||||
diff1 = bag_difference(bag1, bag2)
|
||||
diff2 = bag_difference(bag2, bag1)
|
||||
max_diff = if length1 >= length2, do: diff1, else: diff1 - length1 + length2
|
||||
|
||||
1 - max(diff1, diff2) / max(length1, length2)
|
||||
1 - max_diff / max(length1, length2)
|
||||
end
|
||||
|
||||
defp string_to_bag(string, bag, length) do
|
||||
|
||||
@@ -88,7 +88,7 @@ defmodule StringIO do
|
||||
If options are provided, the result will be `{:ok, pid}`, returning the
|
||||
IO device created. The option `:capture_prompt`, when set to `true`, causes
|
||||
prompts (which are specified as arguments to `IO.get*` functions) to be
|
||||
included in the device's output. See `options/3` for the list of supported
|
||||
included in the device's output. See `open/3` for the list of supported
|
||||
options.
|
||||
|
||||
If a function is provided, the device will be created and sent to the
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user