Compare commits
425
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
759443e724 | ||
|
|
0bba588757 | ||
|
|
04c31907c5 | ||
|
|
153489f9a6 | ||
|
|
3ecb083d5d | ||
|
|
d2689c839e | ||
|
|
309623adf1 | ||
|
|
ae432598e4 | ||
|
|
31288d2267 | ||
|
|
6efe0fc7cb | ||
|
|
b3f2e0def7 | ||
|
|
fa6205cc20 | ||
|
|
ea1519bcc0 | ||
|
|
bfd4dd35fa | ||
|
|
0564a597c6 | ||
|
|
eded6248eb | ||
|
|
6bb46c30fc | ||
|
|
aa3dbdb289 | ||
|
|
a55c1f7b6a | ||
|
|
76df4c6f58 | ||
|
|
96f28d4362 | ||
|
|
f894bc480e | ||
|
|
51d6cfdb93 | ||
|
|
83ae474c61 | ||
|
|
2fbf952cad | ||
|
|
67772adcb2 | ||
|
|
be1234799d | ||
|
|
4918e2d955 | ||
|
|
cbf3836e8b | ||
|
|
989165cabb | ||
|
|
05d03543f6 | ||
|
|
545dddf138 | ||
|
|
bc30e7d696 | ||
|
|
73130012bb | ||
|
|
f55abe5ff9 | ||
|
|
394ab4bead | ||
|
|
f491a7eee4 | ||
|
|
a64284921e | ||
|
|
6bef18d523 | ||
|
|
8516737ea0 | ||
|
|
c52eb2a323 | ||
|
|
527fb26bcd | ||
|
|
5c5909c2a1 | ||
|
|
3c7ee9ff20 | ||
|
|
51dce31a54 | ||
|
|
0dd4cb4e0b | ||
|
|
4d1de1d971 | ||
|
|
b098c0406c | ||
|
|
1ba39f3501 | ||
|
|
0f9072a1d4 | ||
|
|
6dcc4cd2dc | ||
|
|
40b6f012fe | ||
|
|
68267627dc | ||
|
|
57766dd29b | ||
|
|
1938d8a957 | ||
|
|
3bebe1599a | ||
|
|
2beac77406 | ||
|
|
85a23a1244 | ||
|
|
97c1e1ec0c | ||
|
|
64e6707233 | ||
|
|
657a7b394a | ||
|
|
ac76c108ce | ||
|
|
2e629aa414 | ||
|
|
2f2fc9a7f9 | ||
|
|
bb12270c6b | ||
|
|
6d2e8241c9 | ||
|
|
a371208104 | ||
|
|
3a366c2f0f | ||
|
|
d33ea6e963 | ||
|
|
853b7cd719 | ||
|
|
0bc04768bf | ||
|
|
c354a77946 | ||
|
|
766ece7e3f | ||
|
|
8bc9a2ed15 | ||
|
|
7150d493ad | ||
|
|
7a9ea30d9c | ||
|
|
3a038d1763 | ||
|
|
e5e66831ac | ||
|
|
285c15f1ab | ||
|
|
909903962d | ||
|
|
1f19a053cf | ||
|
|
af6d6b2cbe | ||
|
|
4dd6c3cf6a | ||
|
|
862ff780b8 | ||
|
|
edc61ce9c0 | ||
|
|
d1e91dee7f | ||
|
|
7e7b4a8f4e | ||
|
|
f9573159ab | ||
|
|
3d4bffe097 | ||
|
|
694930ec25 | ||
|
|
4725e1100c | ||
|
|
76fbc0ccbc | ||
|
|
28734f03d0 | ||
|
|
0dd4399293 | ||
|
|
0aa8c1fc2c | ||
|
|
315279701a | ||
|
|
57b1685cac | ||
|
|
6962dc2d91 | ||
|
|
ca13b750ce | ||
|
|
c3e1aa706d | ||
|
|
b58e843528 | ||
|
|
0418986004 | ||
|
|
210f6fc3ac | ||
|
|
536e53b173 | ||
|
|
64d381bd05 | ||
|
|
87582af546 | ||
|
|
63708d313c | ||
|
|
e7a5a667bf | ||
|
|
367e38cbe6 | ||
|
|
cb4020e692 | ||
|
|
909b099d44 | ||
|
|
7495a03535 | ||
|
|
5509b95437 | ||
|
|
0792999f63 | ||
|
|
d50b0321ab | ||
|
|
41332ac3d6 | ||
|
|
46f7b37916 | ||
|
|
6b114ee7c1 | ||
|
|
71148aa953 | ||
|
|
b35e210321 | ||
|
|
56336feb2f | ||
|
|
bf47be54bc | ||
|
|
aad63173b8 | ||
|
|
6050b8400a | ||
|
|
42a005e4df | ||
|
|
c2d39715ff | ||
|
|
472a49d84a | ||
|
|
a631da4fb0 | ||
|
|
7f0670e574 | ||
|
|
8bdd041122 | ||
|
|
8f264f3ced | ||
|
|
0eabf48524 | ||
|
|
b0db1a26e1 | ||
|
|
35eeab3511 | ||
|
|
aae39c87dc | ||
|
|
8a2c815c85 | ||
|
|
0b9b5b962b | ||
|
|
41353c6cf8 | ||
|
|
7e49585d26 | ||
|
|
7081e7223e | ||
|
|
3e3ce13c1d | ||
|
|
4816d3773a | ||
|
|
5b8b8e3589 | ||
|
|
aafc248179 | ||
|
|
b8723fea1e | ||
|
|
9412f483d7 | ||
|
|
8a35ffa811 | ||
|
|
bb21320928 | ||
|
|
a5435a6ead | ||
|
|
35497755b5 | ||
|
|
c3f7e3473e | ||
|
|
68c413f76c | ||
|
|
16f5174580 | ||
|
|
1254dae84f | ||
|
|
651bdb485d | ||
|
|
46ebf7cc83 | ||
|
|
abf9ba1ae2 | ||
|
|
46811eccc9 | ||
|
|
9199e5595c | ||
|
|
b8a29e0c1a | ||
|
|
ab5b33d8ac | ||
|
|
c376c0feb6 | ||
|
|
5858e7c470 | ||
|
|
2fd350672b | ||
|
|
002e382794 | ||
|
|
46841a22e4 | ||
|
|
bdf8e4481d | ||
|
|
3120d4539b | ||
|
|
facde14525 | ||
|
|
40e930607a | ||
|
|
03c123a149 | ||
|
|
81975a2de1 | ||
|
|
133dfa8d46 | ||
|
|
39d646f459 | ||
|
|
ad262dc8e4 | ||
|
|
a0e1604fe6 | ||
|
|
11f7d8f84a | ||
|
|
f4e1b34617 | ||
|
|
55a3899e75 | ||
|
|
ada2817d72 | ||
|
|
89e0106afc | ||
|
|
e39a1ca796 | ||
|
|
007efde2d5 | ||
|
|
eb8bbc9c29 | ||
|
|
c5332235ab | ||
|
|
e5d8b00e5b | ||
|
|
2ba06cc40b | ||
|
|
68aae43703 | ||
|
|
2019de2d82 | ||
|
|
d02e61d75e | ||
|
|
f2f2da9b40 | ||
|
|
13d412864c | ||
|
|
76d3fbad88 | ||
|
|
b8a9dc60a5 | ||
|
|
870d28fe64 | ||
|
|
2d6e61c4f5 | ||
|
|
81dc36568b | ||
|
|
99e0fe50a3 | ||
|
|
8a4915008d | ||
|
|
0fe5b13320 | ||
|
|
808075d87a | ||
|
|
c14f3c7acb | ||
|
|
5e7190c39f | ||
|
|
4ad7497d0e | ||
|
|
25cbbfe5d2 | ||
|
|
a5e19ad385 | ||
|
|
983e033863 | ||
|
|
890d9bbf00 | ||
|
|
20fef8cd1c | ||
|
|
9231515db8 | ||
|
|
d5e032efd1 | ||
|
|
5e44c78fff | ||
|
|
4d56bdd898 | ||
|
|
2e9981d98c | ||
|
|
ed478e816b | ||
|
|
370e7db14a | ||
|
|
d67cb9634b | ||
|
|
a37db60f78 | ||
|
|
116f8f472f | ||
|
|
9c1bd76b26 | ||
|
|
3cb0b51836 | ||
|
|
5b97e9c132 | ||
|
|
8ba8ad709f | ||
|
|
37dc125f2a | ||
|
|
9fb8fb1216 | ||
|
|
5a1fa351dc | ||
|
|
20cd669a0d | ||
|
|
03b45e521d | ||
|
|
bcfefa18ed | ||
|
|
b7a832ea32 | ||
|
|
8011552eac | ||
|
|
ebb7c73353 | ||
|
|
898f1a2f69 | ||
|
|
603602e67b | ||
|
|
92221937d3 | ||
|
|
245066b405 | ||
|
|
4d30464ec4 | ||
|
|
139da8fa5f | ||
|
|
8fdef5c48f | ||
|
|
d0514dfd15 | ||
|
|
325608b518 | ||
|
|
c4275e39b2 | ||
|
|
1bf7bb0682 | ||
|
|
7d5d1e27ca | ||
|
|
50cc7ef182 | ||
|
|
5ec106a5e2 | ||
|
|
5c845c796c | ||
|
|
469acb110f | ||
|
|
7061c37b88 | ||
|
|
74bb90a3cd | ||
|
|
5d794ab17c | ||
|
|
c1256f161c | ||
|
|
8f40bf117a | ||
|
|
8d691697ff | ||
|
|
6f2e24ca63 | ||
|
|
8cc693864a | ||
|
|
0f8baa3041 | ||
|
|
017162a975 | ||
|
|
f74b3648e2 | ||
|
|
28b6616a59 | ||
|
|
7c2d4f3d8e | ||
|
|
ba4eb73734 | ||
|
|
fbece98a5d | ||
|
|
9e4eb3dc49 | ||
|
|
2916f20189 | ||
|
|
03b9fde67a | ||
|
|
f16d2a32c1 | ||
|
|
cdbf9b7394 | ||
|
|
a58a40ce48 | ||
|
|
a24d3ff221 | ||
|
|
b77e65dd8a | ||
|
|
37bbba9d58 | ||
|
|
2a7a37f598 | ||
|
|
972df19e13 | ||
|
|
02deafde99 | ||
|
|
bafb68db8e | ||
|
|
24ebb96a27 | ||
|
|
8eb702aea3 | ||
|
|
415cd20100 | ||
|
|
5979c42de7 | ||
|
|
5949bad336 | ||
|
|
cfd62cec73 | ||
|
|
447427a141 | ||
|
|
a9ce579829 | ||
|
|
8df8f92030 | ||
|
|
b5d66beae7 | ||
|
|
e17069e559 | ||
|
|
4aa837d255 | ||
|
|
d8c921fabc | ||
|
|
9c00059f18 | ||
|
|
630d5ca629 | ||
|
|
5bf4df5520 | ||
|
|
a7b2d6c679 | ||
|
|
4064905654 | ||
|
|
6cb9140d9f | ||
|
|
bffb6965a6 | ||
|
|
c39d9ea762 | ||
|
|
98468bd8e3 | ||
|
|
c8c7abc3b2 | ||
|
|
690323e174 | ||
|
|
528f8e7d7b | ||
|
|
127901b8f8 | ||
|
|
aec5d53284 | ||
|
|
445895c98a | ||
|
|
e9adf01e3f | ||
|
|
bc7af21c02 | ||
|
|
7945446cb4 | ||
|
|
2e7a49d426 | ||
|
|
5bad452d0a | ||
|
|
ec8156aad9 | ||
|
|
7967ff985b | ||
|
|
fb2657aa55 | ||
|
|
4021272d42 | ||
|
|
28ffbcfe84 | ||
|
|
f9310524b1 | ||
|
|
57b0e29135 | ||
|
|
b18637fb97 | ||
|
|
7f3d95b60c | ||
|
|
df0553bd08 | ||
|
|
3452e6ab2f | ||
|
|
ababaf8b08 | ||
|
|
a064a00601 | ||
|
|
352bc46c8e | ||
|
|
74d61833ff | ||
|
|
9b80ab584d | ||
|
|
526d00f949 | ||
|
|
51207406d0 | ||
|
|
78a5151865 | ||
|
|
c464eb89c5 | ||
|
|
10902df940 | ||
|
|
44e0541df1 | ||
|
|
71642e381a | ||
|
|
d0ce39e1ce | ||
|
|
e3c74e1e1e | ||
|
|
c184b82b5f | ||
|
|
90b5266ca7 | ||
|
|
382b5b06de | ||
|
|
2c54f9a64a | ||
|
|
2817a70680 | ||
|
|
acd76b68ab | ||
|
|
6f9afad9ca | ||
|
|
13024f43c0 | ||
|
|
8914dcdcbb | ||
|
|
2db87ebc70 | ||
|
|
5e7671a035 | ||
|
|
52c58c72d3 | ||
|
|
d56669789f | ||
|
|
92e1830bf7 | ||
|
|
4468a12222 | ||
|
|
c6820a1297 | ||
|
|
93734a5c86 | ||
|
|
e5811283af | ||
|
|
2639028da8 | ||
|
|
9add53318a | ||
|
|
27aadff38d | ||
|
|
64a5c1742f | ||
|
|
b98d823c54 | ||
|
|
ea672ccd81 | ||
|
|
4ad3e0afb9 | ||
|
|
a5e83ebc04 | ||
|
|
dd1f52917c | ||
|
|
07e6e1a985 | ||
|
|
663c4ab57e | ||
|
|
0dbdadae4a | ||
|
|
d4495ae22a | ||
|
|
09c90ee5fc | ||
|
|
bbe700151e | ||
|
|
d61ba915b0 | ||
|
|
2973af6c97 | ||
|
|
47d72d94d8 | ||
|
|
9a663cf3fa | ||
|
|
7d3863fcf6 | ||
|
|
29f572643e | ||
|
|
80df1fe3dc | ||
|
|
6fd161d67d | ||
|
|
576f123b16 | ||
|
|
77e6687eef | ||
|
|
de4b1610a9 | ||
|
|
0df45fdba1 | ||
|
|
8e5e62653e | ||
|
|
f7c4047d30 | ||
|
|
6e6ed6a3b5 | ||
|
|
0ea809e720 | ||
|
|
cd38841a18 | ||
|
|
132197a254 | ||
|
|
8cda8e1bae | ||
|
|
978e4047f4 | ||
|
|
b792fb41c5 | ||
|
|
d7865ecfa7 | ||
|
|
1f211d121e | ||
|
|
aed240bf40 | ||
|
|
bac6ef9d30 | ||
|
|
7b0ae43231 | ||
|
|
677afeced3 | ||
|
|
2292f3bcb6 | ||
|
|
a2a669eb7b | ||
|
|
b85d1c84d3 | ||
|
|
4602240192 | ||
|
|
a71a4e8f7a | ||
|
|
af34af86aa | ||
|
|
920b7bef2e | ||
|
|
01212cefc8 | ||
|
|
31d62fddb5 | ||
|
|
32d20fd77c | ||
|
|
dfe857e7df | ||
|
|
6d5f43318c | ||
|
|
43dde95ed2 | ||
|
|
565f468d68 | ||
|
|
c903033097 | ||
|
|
7caa954690 | ||
|
|
4c203429d6 | ||
|
|
a0ef7f0c3e | ||
|
|
34a50002f9 | ||
|
|
38dcc453b8 | ||
|
|
34fda89acb | ||
|
|
13846bd25f | ||
|
|
b05fec0c87 | ||
|
|
9ba89f4da6 | ||
|
|
85b1ad0764 | ||
|
|
23633f7723 | ||
|
|
ab7c019d24 | ||
|
|
f2e61f6a04 | ||
|
|
5abcc33267 | ||
|
|
52296e00c6 | ||
|
|
d8eed7812b |
@@ -3,8 +3,7 @@
|
||||
|
||||
---
|
||||
name: Report an issue
|
||||
description:
|
||||
Tell us about something that is not working the way we (probably) intend
|
||||
description: Tell us about something that is not working the way we (probably) intend
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -13,11 +12,17 @@ body:
|
||||
|
||||
|
||||
Please, do not use this form for guidance, questions or support.
|
||||
Try instead in [Elixir Forum](https://elixirforum.com),
|
||||
the [IRC Chat](https://web.libera.chat/#elixir),
|
||||
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
|
||||
[Slack](https://elixir-slackin.herokuapp.com),
|
||||
[Discord](https://discord.gg/elixir) or in other online communities.
|
||||
Try instead in [Elixir Forum](https://elixirforum.com) or any of
|
||||
our online communities (Slack, Discord, etc).
|
||||
|
||||
- type: checkboxes
|
||||
id: existing-issue
|
||||
attributes:
|
||||
label: Existing issue
|
||||
description: Please search [existing issues](https://github.com/elixir-lang/elixir/issues) before continuing.
|
||||
options:
|
||||
- label: I have searched existing issues and could not find a duplicate.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: elixir-and-otp-version
|
||||
|
||||
@@ -7,3 +7,5 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
+12
-14
@@ -26,31 +26,27 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: "28.1"
|
||||
- otp_version: "29.0"
|
||||
deterministic: true
|
||||
- otp_version: "28.1"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "28.4"
|
||||
docs: true
|
||||
coverage: true
|
||||
- otp_version: "28.1"
|
||||
- otp_version: "27.3"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "27.0"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "26.0"
|
||||
- otp_version: master
|
||||
development: true
|
||||
- otp_version: maint
|
||||
development: true
|
||||
|
||||
# Earlier Erlang/OTP versions ignored compiler directives
|
||||
# when using warnings_as_errors. So we only set ERLC_OPTS
|
||||
# from Erlang/OTP 27+.
|
||||
env:
|
||||
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
@@ -96,7 +92,7 @@ jobs:
|
||||
|
||||
- name: "Upload Coverage Artifact"
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: TestCoverage
|
||||
path: cover/*
|
||||
@@ -119,17 +115,19 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version:
|
||||
- "29.0"
|
||||
- "28.1"
|
||||
- "27.3"
|
||||
- "26.2"
|
||||
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2026 The Elixir Team
|
||||
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "29 8 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
zizmor:
|
||||
name: Zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
@@ -28,6 +28,8 @@ jobs:
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
|
||||
@@ -34,6 +34,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
|
||||
|
||||
@@ -46,6 +46,7 @@ runs:
|
||||
repository: oss-review-toolkit/ort-config
|
||||
ref: "main"
|
||||
path: ".ort-config"
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup ORT Config
|
||||
id: setup-ort-config
|
||||
|
||||
@@ -33,6 +33,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: |
|
||||
|
||||
@@ -37,30 +37,31 @@ jobs:
|
||||
if: github.ref_type != 'branch'
|
||||
run: |
|
||||
gh release create \
|
||||
--repo ${{ github.repository }} \
|
||||
--title ${{ github.ref_name }} \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$GITHUB_REF_NAME" \
|
||||
--notes '' \
|
||||
--draft \
|
||||
${{ github.ref_name }}
|
||||
"$GITHUB_REF_NAME"
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# zizmor: ignore[artipacked]
|
||||
if: github.ref_type == 'branch'
|
||||
|
||||
- name: Update ${{ github.ref_name }}-latest
|
||||
if: github.ref_type == 'branch'
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}-latest
|
||||
ref_name="${GITHUB_REF_NAME}-latest"
|
||||
|
||||
if ! gh release view $ref_name; then
|
||||
if ! gh release view "$ref_name"; then
|
||||
gh release create \
|
||||
--latest=false \
|
||||
--title $ref_name \
|
||||
--notes "Automated release for latest ${{ github.ref_name }}." \
|
||||
$ref_name
|
||||
--title "$ref_name" \
|
||||
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
|
||||
"$ref_name"
|
||||
fi
|
||||
|
||||
git tag $ref_name --force
|
||||
git push origin $ref_name --force
|
||||
git tag "$ref_name" --force
|
||||
git push origin "$ref_name" --force
|
||||
|
||||
build:
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
|
||||
@@ -70,9 +71,6 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- otp: 26
|
||||
otp_version: "26.0"
|
||||
|
||||
- otp: 27
|
||||
otp_version: "27.0"
|
||||
|
||||
@@ -80,8 +78,13 @@ jobs:
|
||||
otp_version: "28.0"
|
||||
build_docs: build_docs
|
||||
|
||||
- otp: 29
|
||||
otp_version: "29.0"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Build Release"
|
||||
uses: ./.github/workflows/release_pre_built
|
||||
@@ -97,19 +100,19 @@ jobs:
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: build-linux-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.zip
|
||||
|
||||
- name: "Upload Windows release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: build-windows-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.exe
|
||||
|
||||
- name: "Upload doc artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: matrix.build_docs
|
||||
with:
|
||||
name: Docs
|
||||
@@ -122,7 +125,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
otp: [26, 27, 28]
|
||||
otp: [27, 28, 29]
|
||||
flavor: [windows, linux]
|
||||
|
||||
env:
|
||||
@@ -136,20 +139,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Download build"
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
|
||||
- name: Log in to Azure
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
|
||||
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@db7a3a6bd3912025c705162fb7475389f5b69ec6 # v1.0.0
|
||||
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
@@ -179,7 +182,7 @@ jobs:
|
||||
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
path: ${{ env.RELEASE_FILE }}*
|
||||
@@ -203,10 +206,12 @@ jobs:
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Download Build Artifacts"
|
||||
id: download-build-artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -221,7 +226,7 @@ jobs:
|
||||
|
||||
- name: Attest Distribution Assets with SBoM
|
||||
id: attest-sbom
|
||||
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
|
||||
@@ -241,15 +246,19 @@ jobs:
|
||||
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
|
||||
done
|
||||
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
|
||||
env:
|
||||
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
|
||||
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
|
||||
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
|
||||
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
|
||||
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
|
||||
|
||||
- name: "Assemble Release SBoM Artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "SBoM"
|
||||
path: |
|
||||
@@ -259,7 +268,7 @@ jobs:
|
||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
||||
|
||||
- name: "Assemble Distribution Attestations"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "Attestations"
|
||||
path: "attestations/*.sigstore"
|
||||
@@ -273,7 +282,7 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
|
||||
merge-multiple: true
|
||||
@@ -283,14 +292,14 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" == "branch" ]; then
|
||||
tag=${{ github.ref_name }}-latest
|
||||
if [ "$GITHUB_REF_TYPE" == "branch" ]; then
|
||||
tag="${GITHUB_REF_NAME}-latest"
|
||||
else
|
||||
tag="${{ github.ref_name }}"
|
||||
tag="$GITHUB_REF_NAME"
|
||||
fi
|
||||
|
||||
gh release upload \
|
||||
--repo ${{ github.repository }} \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--clobber \
|
||||
"$tag" \
|
||||
elixir-otp-*.zip \
|
||||
@@ -324,7 +333,7 @@ jobs:
|
||||
echo "Required variables for uploading to hex.pm are not set up, skipping..."
|
||||
exit 1
|
||||
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -335,12 +344,10 @@ jobs:
|
||||
|
||||
- name: Upload Precompiled to S3
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
|
||||
|
||||
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
|
||||
dest=${zip/elixir/${ref_name}}
|
||||
dest=${zip/elixir/${GITHUB_REF_NAME}}
|
||||
surrogate_key=${dest/.zip$/}
|
||||
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
|
||||
@@ -349,16 +356,16 @@ jobs:
|
||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
||||
|
||||
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo builds/elixir/${ref_name} >> purge_keys.txt
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Docs to S3
|
||||
run: |
|
||||
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
|
||||
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
|
||||
|
||||
unzip Docs.zip
|
||||
|
||||
@@ -379,7 +386,7 @@ jobs:
|
||||
- name: Update builds txt
|
||||
run: |
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ref_name=${{ github.ref_name }}
|
||||
ref_name="$GITHUB_REF_NAME"
|
||||
|
||||
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: "27.3"
|
||||
elixir-version: "1.18.3"
|
||||
@@ -29,4 +31,4 @@ jobs:
|
||||
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
||||
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
|
||||
run: |
|
||||
elixir .github/workflows/notify.exs ${{ github.ref_name }}
|
||||
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
|
||||
|
||||
@@ -25,10 +25,12 @@ runs:
|
||||
|
||||
- name: Build Elixir Release
|
||||
shell: bash
|
||||
run: |
|
||||
run: | # zizmor: ignore[github-env]
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
env:
|
||||
INPUT_OTP: ${{ inputs.otp }}
|
||||
|
||||
- name: Install NSIS
|
||||
shell: bash
|
||||
@@ -39,15 +41,18 @@ runs:
|
||||
- name: Build Elixir Windows Installer
|
||||
shell: bash
|
||||
run: |
|
||||
export OTP_VERSION=${{ inputs.otp_version }}
|
||||
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
|
||||
export OTP_VERSION="$INPUT_OTP_VERSION"
|
||||
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
|
||||
(cd lib/elixir/scripts/windows_installer && ./build.sh)
|
||||
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
|
||||
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
|
||||
env:
|
||||
INPUT_OTP: ${{ inputs.otp }}
|
||||
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
|
||||
- name: Get ExDoc ref
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||
run: | # zizmor: ignore[github-env]
|
||||
if [ "$GITHUB_REF_NAME" = "main" ]; then
|
||||
ref=main
|
||||
else
|
||||
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
@@ -59,6 +64,7 @@ runs:
|
||||
repository: elixir-lang/ex_doc
|
||||
ref: ${{ env.EX_DOC_REF }}
|
||||
path: ex_doc
|
||||
persist-credentials: false
|
||||
- name: Build ex_doc
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
|
||||
+256
-44
@@ -6,31 +6,13 @@
|
||||
|
||||
# Changelog for Elixir v1.20
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
## Type system improvements
|
||||
|
||||
This release includes type inference of all constructs.
|
||||
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.
|
||||
|
||||
### Type inference of function calls
|
||||
|
||||
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. 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.
|
||||
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
|
||||
|
||||
@@ -68,7 +50,44 @@ 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).
|
||||
|
||||
### Complete typing of maps keys
|
||||
### 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()`.
|
||||
|
||||
@@ -158,50 +177,239 @@ The code above has a type violation, which is now caught by the type system:
|
||||
|
||||
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/).
|
||||
|
||||
## v1.20.0-rc.1 (2026-01-13)
|
||||
## Compile-time improvements
|
||||
|
||||
### 1. Bug fixes
|
||||
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.4 (2026-08-28)
|
||||
|
||||
### 1. Security
|
||||
|
||||
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Improve the performance of the type system when working with large unions of open maps
|
||||
* [Kernel] Do not crash on map types with struct keys when performing type operations
|
||||
* [Kernel] Mark the outcome of bitstring types as dynamic
|
||||
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary
|
||||
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable
|
||||
* [Base] Fix false positives in Base64 validation
|
||||
* [Kernel] Fix inferred type of `:erlang.split_binary/2` when given a bitstring
|
||||
* [Kernel] Fix type checker crash on comprehensions with a non-returning `:into` expression
|
||||
* [String] Fix final sigma handling in Greek casing
|
||||
* [Task] Fix `Task.yield_many/2` waiting indefinitely when `:limit` exceeds the task count
|
||||
|
||||
## v1.20.0-rc.0 (2026-01-09)
|
||||
## v1.20.3 (2026-08-04)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
|
||||
* [Enum] Add `Enum.min_max` sorter
|
||||
* [Integer] Add `Integer.ceil_div/2`
|
||||
* [IO] Add `IO.iodata_empty?/1`
|
||||
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
|
||||
* [Kernel] Print intermediate results of `dbg` for pipes
|
||||
* [Kernel] Warn on unused requires
|
||||
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
|
||||
* [Kernel.ParallelCompiler] Batch type checker cache operations to improve compilation times
|
||||
|
||||
#### ExUnit
|
||||
### 2. Bug fixes
|
||||
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Improve type precision of the `++/2` operator
|
||||
* [Kernel] Improve type precision of the `send/2` return type
|
||||
* [Kernel] Improve type refinement for `and/2` and `or/2`
|
||||
* [Kernel] Fix type checking of `Map.put/3` on empty map types
|
||||
* [Kernel] Fix type checking of tuple insertion on equivalent tuple types
|
||||
* [Kernel] Fix type checking of tuple insertion on gradual types with empty static parts
|
||||
* [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 deps] Support filtering `mix deps` output
|
||||
* [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.
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Optimize compiler by flattening expr list only once
|
||||
|
||||
#### 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
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
|
||||
#### 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
|
||||
|
||||
#### Elixir
|
||||
|
||||
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note that while Elixir does not guarantee macros will expand to certain constructs, 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
|
||||
* [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. Hard deprecations
|
||||
### 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
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
|
||||
|
||||
#### 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`
|
||||
|
||||
#### 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
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
@@ -214,6 +422,10 @@ The type system was made possible thanks to a partnership between [CNRS](https:/
|
||||
* [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`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
|
||||
|
||||
## 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).
|
||||
|
||||
+68
-25
@@ -99,15 +99,11 @@ case you are looking for some examples:
|
||||
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 approved by two committers 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.
|
||||
|
||||
Committers may also push style changes directly to your branch. If you would
|
||||
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
|
||||
feature when submitting your pull request.
|
||||
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
|
||||
@@ -118,6 +114,29 @@ into the repository. If you have carefully organized your commits and
|
||||
believe they should be merged without squashing, please mention it in
|
||||
a comment.
|
||||
|
||||
## Building documentation
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
|
||||
```sh
|
||||
elixir_dir=$(pwd)
|
||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
||||
|
||||
# Now we will go back to Elixir's root directory,
|
||||
cd "${elixir_dir}"
|
||||
|
||||
# and generate HTML and EPUB documents:
|
||||
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).
|
||||
|
||||
## Licensing and Compliance Requirements
|
||||
|
||||
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
|
||||
@@ -181,25 +200,49 @@ affecting **all external contributors**:
|
||||
See <https://developercertificate.org/> for a copy of the Developer Certificate
|
||||
of Origin license.
|
||||
|
||||
## Building documentation
|
||||
## Using AI and coding agents
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir.
|
||||
While we allow the use of AI on contributions and discussions, please be mindful
|
||||
when doing so. Generally speaking, Elixir maintainers already have access to AI
|
||||
(like many other developers). Therefore, if we need the feedback or help of a
|
||||
coding agent, we can request so ourselves. For this reason, we often find
|
||||
the point of view of the human behind the agent more valuable.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
That said, here are examples of how one might (or might not) use AI and coding
|
||||
agents in Elixir spaces:
|
||||
|
||||
```sh
|
||||
elixir_dir=$(pwd)
|
||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
||||
* When it comes to discussions, using AI to help express yourself is welcome,
|
||||
but avoid directly copy and pasting AI generated content. If there is a language
|
||||
barrier, use AI to translate, review, and improve your text, but do not use AI
|
||||
to respond on your behalf.
|
||||
|
||||
# Now we will go back to Elixir's root directory,
|
||||
cd "${elixir_dir}"
|
||||
* Do not use coding agents to tackle existing issues unless they have the
|
||||
"Contributions Welcome" label.
|
||||
|
||||
# and generate HTML and EPUB documents:
|
||||
make docs
|
||||
```
|
||||
* If you request a feature on the mailing list and it is accepted, you may
|
||||
use coding agents to implement it, as long as it follows the AI Contributions
|
||||
guidelines below.
|
||||
|
||||
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).
|
||||
* 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.
|
||||
|
||||
If any code is written by AI, then you must follow the guidelines below.
|
||||
|
||||
### AI contributions
|
||||
|
||||
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
|
||||
Certificate of Origin (DCO). The human submitter is responsible for:
|
||||
|
||||
* Reviewing all AI-generated code
|
||||
* Ensuring compliance with licensing requirements
|
||||
* Adding their own Signed-off-by tag to certify the DCO
|
||||
* Taking full responsibility for the contribution
|
||||
* Disclosing use of AI for comments and code contributions
|
||||
|
||||
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
|
||||
AI in the development process. Contributions should include an Assisted-by tag in the
|
||||
following format:
|
||||
|
||||
Assisted-by: AGENT_NAME:MODEL_VERSION
|
||||
|
||||
@@ -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
|
||||
@@ -32,9 +32,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
||||
#==> Functions
|
||||
|
||||
define CHECK_ERLANG_RELEASE
|
||||
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 26)])' -s erlang halt | grep -q '^true'; \
|
||||
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 27)])' -s erlang halt | grep -q '^true'; \
|
||||
if [ $$? != 0 ]; then \
|
||||
echo "At least Erlang/OTP 26.0 is required to build Elixir"; \
|
||||
echo "At least Erlang/OTP 27.0 is required to build Elixir"; \
|
||||
exit 1; \
|
||||
fi
|
||||
endef
|
||||
|
||||
@@ -122,9 +122,10 @@ variable `ERL_COMPILER_OPTIONS=deterministic`.
|
||||
Contributions to Elixir are always welcome! Before you get started, please check
|
||||
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
|
||||
guidelines on how to set up your environment, run the test suite, format your
|
||||
code, and submit pull requests. We also include information on our review
|
||||
process, licensing requirements, and helpful tips to ensure a smooth
|
||||
contribution experience.
|
||||
code, and submit pull requests.
|
||||
|
||||
Note you must disclose the use of coding agents and AI written code in your
|
||||
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Development links
|
||||
|
||||
|
||||
+2
-3
@@ -12,12 +12,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.20 | Development
|
||||
1.19 | Bug fixes and security patches
|
||||
1.20 | Bug fixes and security patches
|
||||
1.19 | Security patches only
|
||||
1.18 | Security patches only
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.20.0-rc.1
|
||||
ELIXIR_VERSION=1.20.4
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
set ELIXIR_VERSION=1.20.0-rc.1
|
||||
set ELIXIR_VERSION=1.20.4
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+25
-12
@@ -303,7 +303,7 @@ defmodule EEx.Compiler do
|
||||
file: file,
|
||||
source: source,
|
||||
line: line,
|
||||
quoted: [],
|
||||
quoted: %{},
|
||||
parser_options: [indentation: indentation] ++ parser_options,
|
||||
indentation: indentation
|
||||
}
|
||||
@@ -347,7 +347,7 @@ defmodule EEx.Compiler do
|
||||
state.parser_options
|
||||
|
||||
expr = Code.string_to_quoted!(chars, options)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
buffer = handle_expr(buffer, mark, expr, meta, state)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -366,7 +366,7 @@ defmodule EEx.Compiler do
|
||||
rest,
|
||||
state.engine.handle_begin(buffer),
|
||||
[{contents, start_line, start_column} | scope],
|
||||
%{state | quoted: [], line: line}
|
||||
%{state | quoted: %{}, line: line}
|
||||
)
|
||||
|
||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
||||
@@ -376,7 +376,7 @@ defmodule EEx.Compiler do
|
||||
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
|
||||
end
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
buffer = handle_expr(buffer, mark, contents, meta, state)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -410,7 +410,7 @@ defmodule EEx.Compiler do
|
||||
) do
|
||||
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
|
||||
options = [file: state.file, line: line, column: column] ++ state.parser_options
|
||||
tuples = Code.string_to_quoted!(wrapped, options)
|
||||
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options)
|
||||
buffer = insert_quoted(tuples, state.quoted)
|
||||
{buffer, rest}
|
||||
end
|
||||
@@ -426,7 +426,7 @@ defmodule EEx.Compiler do
|
||||
|
||||
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
|
||||
message = "expected a closing '<% end %>' for block expression in EEx"
|
||||
expr_meta = non_whitespace_meta(content, line, column, state)
|
||||
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state)
|
||||
syntax_error!(message, expr_meta, state)
|
||||
end
|
||||
|
||||
@@ -443,10 +443,10 @@ defmodule EEx.Compiler do
|
||||
|
||||
defp wrap_expr(current, line, buffer, chars, state) do
|
||||
new_lines = List.duplicate(?\n, line - state.line)
|
||||
key = length(state.quoted)
|
||||
placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
|
||||
count = current ++ placeholder ++ new_lines ++ chars
|
||||
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
|
||||
key = map_size(state.quoted)
|
||||
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"]
|
||||
count = [current, placeholder, new_lines, chars]
|
||||
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))}
|
||||
|
||||
{count, new_state}
|
||||
end
|
||||
@@ -479,8 +479,7 @@ defmodule EEx.Compiler do
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
{^key, value} = List.keyfind(quoted, key, 0)
|
||||
value
|
||||
Map.fetch!(quoted, key)
|
||||
end
|
||||
|
||||
defp insert_quoted({left, line, right}, quoted) do
|
||||
@@ -513,6 +512,20 @@ defmodule EEx.Compiler do
|
||||
column: meta.column
|
||||
end
|
||||
|
||||
defp handle_expr(buffer, mark, expr, meta, state) do
|
||||
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
rescue
|
||||
e in EEx.SyntaxError ->
|
||||
reraise %{
|
||||
e
|
||||
| file: e.file || state.file,
|
||||
line: e.line || meta.line,
|
||||
column: e.column || meta.column,
|
||||
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
|
||||
},
|
||||
__STACKTRACE__
|
||||
end
|
||||
|
||||
defp code_snippet(source, indentation, meta) do
|
||||
line_start = max(meta.line - 3, 1)
|
||||
line_end = meta.line
|
||||
|
||||
@@ -575,6 +575,18 @@ defmodule EExTest do
|
||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||
end
|
||||
end
|
||||
|
||||
test "unsupported marker error carries template location metadata" do
|
||||
error =
|
||||
assert_raise EEx.SyntaxError, fn ->
|
||||
EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
|
||||
end
|
||||
|
||||
assert error.file == "sample.eex"
|
||||
assert error.line == 7
|
||||
assert error.column == 1
|
||||
assert Exception.message(error) =~ "sample.eex:7:1:"
|
||||
end
|
||||
end
|
||||
|
||||
describe "warnings" do
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
|
||||
CoverageRecorder.maybe_record("eex")
|
||||
|
||||
ExUnit.start(
|
||||
trace: !!System.get_env("TRACE"),
|
||||
include: line_include,
|
||||
exclude: line_exclude
|
||||
)
|
||||
maybe_seed_opt = if seed = System.get_env("SEED"), do: [seed: String.to_integer(seed)], else: []
|
||||
|
||||
ex_unit_opts =
|
||||
[
|
||||
trace: !!System.get_env("TRACE"),
|
||||
include: line_include,
|
||||
exclude: line_exclude
|
||||
] ++ maybe_seed_opt
|
||||
|
||||
ExUnit.start(ex_unit_opts)
|
||||
|
||||
@@ -1091,6 +1091,11 @@ defmodule Access do
|
||||
|
||||
defp normalize_range(range, _list), do: range
|
||||
|
||||
defp get_and_update_slice(rest, %Range{last: last}, _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
|
||||
case next.(head) do
|
||||
|
||||
+278
-54
@@ -155,6 +155,255 @@ defmodule Base do
|
||||
for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>>
|
||||
end
|
||||
|
||||
# SWAR (SIMD Within A Register) fast paths for valid16?/2 and valid32?/2
|
||||
# (non-hex). Each chunk of 8 bytes is validated in one guard: 7 bytes via
|
||||
# bitwise arithmetic on a single 56-bit integer, plus a per-byte range
|
||||
# check for the 8th byte. 56 bits is the largest width that fits in a BEAM
|
||||
# small int on 64-bit (fixnum range is 59-bit signed); at 64 bits every
|
||||
# `w + 0x80..` would allocate a bignum on the heap and the optimisation
|
||||
# would collapse. See https://github.com/erlang/otp/pull/10938 for the
|
||||
# corresponding pattern in OTP.
|
||||
@swar_mask80 0x80808080808080
|
||||
|
||||
# Per-range SWAR constants, broadcast across 7 lanes. Naming convention:
|
||||
# @swar_ge_X = 0x80 - X → high bit of `(w + @swar_ge_X)` lane is set
|
||||
# iff that byte is ≥ X
|
||||
# @swar_gt_X = 0x7F - X → high bit of `(w + @swar_gt_X)` lane is set
|
||||
# iff that byte is > X
|
||||
# A byte is in range [lo, hi] iff
|
||||
# `bxor(w + @swar_ge_lo, w + @swar_gt_hi)` has its high bit set.
|
||||
@swar_ge_0 0x50505050505050
|
||||
@swar_gt_9 0x46464646464646
|
||||
@swar_ge_2 0x4E4E4E4E4E4E4E
|
||||
@swar_gt_7 0x48484848484848
|
||||
@swar_ge_A 0x3F3F3F3F3F3F3F
|
||||
@swar_gt_F 0x39393939393939
|
||||
@swar_gt_V 0x29292929292929
|
||||
@swar_gt_Z 0x25252525252525
|
||||
@swar_ge_a 0x1F1F1F1F1F1F1F
|
||||
@swar_gt_f 0x19191919191919
|
||||
@swar_gt_v 0x09090909090909
|
||||
@swar_gt_z 0x05050505050505
|
||||
|
||||
# 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 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
|
||||
|
||||
# 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).
|
||||
@compile {:inline,
|
||||
valid_char16upper?: 1,
|
||||
valid_char16lower?: 1,
|
||||
valid_char16mixed?: 1,
|
||||
valid_char32upper?: 1,
|
||||
valid_char32lower?: 1,
|
||||
valid_char32mixed?: 1,
|
||||
valid_char32hexupper?: 1,
|
||||
valid_char32hexlower?: 1,
|
||||
valid_char32hexmixed?: 1,
|
||||
valid_char64base?: 1,
|
||||
valid_char64url?: 1,
|
||||
valid_word16upper?: 1,
|
||||
valid_word16lower?: 1,
|
||||
valid_word16mixed?: 1,
|
||||
valid_word32upper?: 1,
|
||||
valid_word32lower?: 1,
|
||||
valid_word32mixed?: 1,
|
||||
valid_word32hexupper?: 1,
|
||||
valid_word32hexlower?: 1,
|
||||
valid_word32hexmixed?: 1,
|
||||
valid_word64base?: 1,
|
||||
valid_word64url?: 1}
|
||||
|
||||
defp valid_char16upper?(c), do: c in ?0..?9 or c in ?A..?F
|
||||
defp valid_char16lower?(c), do: c in ?0..?9 or c in ?a..?f
|
||||
defp valid_char16mixed?(c), do: c in ?0..?9 or c in ?A..?F or c in ?a..?f
|
||||
|
||||
defp valid_char32upper?(c), do: c in ?A..?Z or c in ?2..?7
|
||||
defp valid_char32lower?(c), do: c in ?a..?z or c in ?2..?7
|
||||
defp valid_char32mixed?(c), do: c in ?A..?Z or c in ?a..?z or c in ?2..?7
|
||||
|
||||
# Most common range first — letters dominate (22/32) over digits (10/32)
|
||||
# in hex base32, so letters go first in the OR short-circuit.
|
||||
defp valid_char32hexupper?(c), do: c in ?A..?V or c in ?0..?9
|
||||
defp valid_char32hexlower?(c), do: c in ?a..?v or c in ?0..?9
|
||||
defp valid_char32hexmixed?(c), do: c in ?A..?V or c in ?a..?v or c in ?0..?9
|
||||
|
||||
defp valid_char64base?(c),
|
||||
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?+ or c == ?/
|
||||
|
||||
defp valid_char64url?(c),
|
||||
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?- or c == ?_
|
||||
|
||||
# SWAR 7-byte word validity. Structure for each function:
|
||||
# 1. ASCII gate `band(w, MASK80) == 0` — every byte < 0x80 so the
|
||||
# additions below cannot carry across lanes.
|
||||
# 2. "Each byte is in range A OR range B (OR range C)" gate — OR per-
|
||||
# range XOR masks (high bit set in lane iff byte in that range), AND
|
||||
# with MASK80, demand all 7 high bits set.
|
||||
defp valid_word16upper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_F)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word16lower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_f)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word16mixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_F)
|
||||
),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_f)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32upper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32lower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32mixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexupper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_V)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexlower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_v)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexmixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_V)
|
||||
),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_v)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
# 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 singleton. For url,
|
||||
# '-' and '_' are two more singletons.
|
||||
defp valid_word64base?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_slash, w + @swar_gt_9)
|
||||
),
|
||||
bxor(w + @swar_ge_plus, w + @swar_gt_plus)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word64url?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9)
|
||||
),
|
||||
bor(
|
||||
bxor(w + @swar_ge_dash, w + @swar_gt_dash),
|
||||
bxor(w + @swar_ge_underscore, w + @swar_gt_underscore)
|
||||
)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 16 encoded string.
|
||||
|
||||
@@ -371,25 +620,21 @@ defmodule Base do
|
||||
decode_name = :"decode16#{base}!"
|
||||
validate_name = :"validate16#{base}?"
|
||||
valid_char_name = :"valid_char16#{base}?"
|
||||
valid_word_name = :"valid_word16#{base}?"
|
||||
|
||||
{min, decoded} = to_decode_list.(alphabet)
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated entirely via
|
||||
# `valid_word16<base>?` in the body. The `and` short-circuits when SWAR
|
||||
# fails on any byte. Tail bytes (1-6 leftover) recurse through the
|
||||
# single-byte clause below.
|
||||
defp unquote(validate_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_name)(rest)
|
||||
|
||||
defp unquote(validate_name)(<<>>), do: true
|
||||
|
||||
defp unquote(validate_name)(<<c1, c2, rest::binary>>) do
|
||||
unquote(valid_char_name)(c1) and
|
||||
unquote(valid_char_name)(c2) and
|
||||
unquote(validate_name)(rest)
|
||||
end
|
||||
|
||||
defp unquote(validate_name)(<<_char, _rest::binary>>), do: false
|
||||
|
||||
@compile {:inline, [{valid_char_name, 1}]}
|
||||
defp unquote(valid_char_name)(char)
|
||||
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
|
||||
do: true
|
||||
|
||||
defp unquote(valid_char_name)(_char), do: false
|
||||
defp unquote(validate_name)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest)
|
||||
|
||||
defp unquote(decode_name)(char) do
|
||||
index = char - unquote(min)
|
||||
@@ -761,23 +1006,19 @@ defmodule Base do
|
||||
validate_name = :"validate64#{base}?"
|
||||
validate_main_name = :"validate_main64#{validate_name}?"
|
||||
valid_char_name = :"valid_char64#{base}?"
|
||||
valid_word_name = :"valid_word64#{base}?"
|
||||
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<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.
|
||||
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_main_name)(<<>>), do: true
|
||||
|
||||
defp unquote(validate_main_name)(
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
|
||||
) do
|
||||
unquote(valid_char_name)(c1) and
|
||||
unquote(valid_char_name)(c2) and
|
||||
unquote(valid_char_name)(c3) and
|
||||
unquote(valid_char_name)(c4) and
|
||||
unquote(valid_char_name)(c5) and
|
||||
unquote(valid_char_name)(c6) and
|
||||
unquote(valid_char_name)(c7) and
|
||||
unquote(valid_char_name)(c8) and
|
||||
unquote(validate_main_name)(rest)
|
||||
end
|
||||
defp unquote(validate_main_name)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_name)(<<>>, _pad?), do: true
|
||||
|
||||
@@ -863,13 +1104,6 @@ defmodule Base do
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, [{valid_char_name, 1}]}
|
||||
defp unquote(valid_char_name)(char)
|
||||
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
|
||||
do: true
|
||||
|
||||
defp unquote(valid_char_name)(_char), do: false
|
||||
|
||||
defp unquote(decode_name)(char) do
|
||||
index = char - unquote(min)
|
||||
|
||||
@@ -1425,21 +1659,18 @@ defmodule Base do
|
||||
valid_char_name = :"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}?"
|
||||
|
||||
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_main_name)(<<>>), do: true
|
||||
|
||||
defp unquote(validate_main_name)(
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
|
||||
) do
|
||||
unquote(valid_char_name)(c1) and
|
||||
unquote(valid_char_name)(c2) and
|
||||
unquote(valid_char_name)(c3) and
|
||||
unquote(valid_char_name)(c4) and
|
||||
unquote(valid_char_name)(c5) and
|
||||
unquote(valid_char_name)(c6) and
|
||||
unquote(valid_char_name)(c7) and
|
||||
unquote(valid_char_name)(c8) and
|
||||
unquote(validate_main_name)(rest)
|
||||
end
|
||||
defp unquote(validate_main_name)(<<char, rest::binary>>),
|
||||
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
|
||||
|
||||
defp unquote(validate_name)(<<>>, _pad?), do: true
|
||||
|
||||
@@ -1519,13 +1750,6 @@ defmodule Base do
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, [{valid_char_name, 1}]}
|
||||
defp unquote(valid_char_name)(char)
|
||||
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
|
||||
do: true
|
||||
|
||||
defp unquote(valid_char_name)(_char), do: false
|
||||
|
||||
defp unquote(decode_name)(char) do
|
||||
index = char - unquote(min)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Calendar do
|
||||
@strftime_max_width 1024
|
||||
|
||||
@moduledoc """
|
||||
This module defines the responsibilities for working with
|
||||
calendars, dates, times and datetimes in Elixir.
|
||||
@@ -529,6 +531,7 @@ defmodule Calendar do
|
||||
* `%`: indicates the start of a formatted section
|
||||
* `<padding>`: set the padding (see below)
|
||||
* `<width>`: a number indicating the minimum size of the formatted section
|
||||
(maximum #{@strftime_max_width})
|
||||
* `<format>`: the format itself (see below)
|
||||
|
||||
### Accepted padding options
|
||||
@@ -667,9 +670,13 @@ defmodule Calendar do
|
||||
end
|
||||
|
||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||
new_width = (width || 0) * 10 + (digit - ?0)
|
||||
width = (width || 0) * 10 + (digit - ?0)
|
||||
|
||||
parse_modifiers(rest, new_width, pad, parser_data)
|
||||
if width > @strftime_max_width do
|
||||
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
|
||||
end
|
||||
|
||||
parse_modifiers(rest, width, pad, parser_data)
|
||||
end
|
||||
|
||||
# set default padding if none was specified
|
||||
|
||||
@@ -59,11 +59,20 @@ defmodule Date.Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def member?(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range,
|
||||
date
|
||||
) do
|
||||
member? =
|
||||
quote generated: true do
|
||||
member?(
|
||||
%{
|
||||
__struct__: Date.Range,
|
||||
first_in_iso_days: var!(first_days),
|
||||
last_in_iso_days: var!(last_days)
|
||||
} =
|
||||
var!(date_range),
|
||||
var!(date)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(member?) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
member?(Map.put(date_range, :step, step), date)
|
||||
end
|
||||
@@ -218,11 +227,11 @@ defmodule Date.Range do
|
||||
defimpl Inspect do
|
||||
import Kernel, except: [inspect: 2]
|
||||
|
||||
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
|
||||
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do
|
||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
|
||||
end
|
||||
|
||||
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
|
||||
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do
|
||||
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
|
||||
end
|
||||
|
||||
|
||||
@@ -671,7 +671,7 @@ defmodule Calendar.ISO do
|
||||
{{year, month, day}, time_from_day_fraction(day_fraction)}
|
||||
|
||||
{extra_days, day_fraction} ->
|
||||
base_days = date_to_iso_days(year, month, day)
|
||||
base_days = valid_date_to_iso_days(year, month, day)
|
||||
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
|
||||
end
|
||||
|
||||
@@ -892,12 +892,15 @@ defmodule Calendar.ISO do
|
||||
|
||||
# Converts year, month, day to count of days since 0000-01-01.
|
||||
@doc false
|
||||
def date_to_iso_days(0, 1, 1), do: 0
|
||||
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
|
||||
|
||||
def date_to_iso_days(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
valid_date_to_iso_days(year, month, day)
|
||||
end
|
||||
|
||||
defp valid_date_to_iso_days(0, 1, 1), do: 0
|
||||
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
|
||||
|
||||
defp valid_date_to_iso_days(year, month, day) do
|
||||
y = if month <= 2, do: year - 1, else: year
|
||||
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
|
||||
year_of_era = y - era * @years_per_era
|
||||
@@ -1875,7 +1878,7 @@ defmodule Calendar.ISO do
|
||||
@doc false
|
||||
def shift_days({year, month, day}, days) do
|
||||
{year, month, day} =
|
||||
date_to_iso_days(year, month, day)
|
||||
valid_date_to_iso_days(year, month, day)
|
||||
|> Kernel.+(days)
|
||||
|> date_from_iso_days()
|
||||
|
||||
|
||||
@@ -715,16 +715,18 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@spec to_date(Calendar.naive_datetime()) :: Date.t()
|
||||
def to_date(%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
calendar: calendar,
|
||||
hour: _,
|
||||
minute: _,
|
||||
second: _,
|
||||
microsecond: _
|
||||
}) do
|
||||
def to_date(
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
calendar: calendar,
|
||||
hour: _,
|
||||
minute: _,
|
||||
second: _,
|
||||
microsecond: _
|
||||
} = _naive_datetime
|
||||
) do
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
@@ -741,16 +743,18 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@spec to_time(Calendar.naive_datetime()) :: Time.t()
|
||||
def to_time(%{
|
||||
year: _,
|
||||
month: _,
|
||||
day: _,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}) do
|
||||
def to_time(
|
||||
%{
|
||||
year: _,
|
||||
month: _,
|
||||
day: _,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
} = _naive_datetime
|
||||
) do
|
||||
%Time{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
@@ -1146,16 +1150,18 @@ defmodule NaiveDateTime do
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
|
||||
def to_gregorian_seconds(%{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}) do
|
||||
def to_gregorian_seconds(
|
||||
%{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
} = _naive_datetime
|
||||
) do
|
||||
{days, day_fraction} =
|
||||
calendar.naive_datetime_to_iso_days(
|
||||
year,
|
||||
|
||||
+122
-71
@@ -286,19 +286,25 @@ defmodule Code do
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
static_atoms_encoder: (atom() -> term()),
|
||||
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
|
||||
emit_warnings: boolean()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
|
||||
Options for evaluation environment, accepted by `env_for_eval/1`.
|
||||
"""
|
||||
@type env_eval_opts :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
module: module(),
|
||||
prune_binding: boolean()
|
||||
]
|
||||
@type env_eval_opt ::
|
||||
{:file, binary()}
|
||||
| {:line, pos_integer()}
|
||||
| {:module, module()}
|
||||
|
||||
@typedoc """
|
||||
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
|
||||
and `eval_quoted_with_env/4`.
|
||||
"""
|
||||
@type eval_opt ::
|
||||
{:prune_binding, boolean()}
|
||||
| {:dbg_callback, {module(), atom(), list()}}
|
||||
|
||||
@boolean_compiler_options [
|
||||
:docs,
|
||||
@@ -312,7 +318,12 @@ defmodule Code do
|
||||
|
||||
@available_compiler_options @boolean_compiler_options ++
|
||||
@list_compiler_options ++
|
||||
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
|
||||
[
|
||||
:on_undefined_variable,
|
||||
:infer_signatures,
|
||||
:no_warn_undefined,
|
||||
:module_definition
|
||||
]
|
||||
|
||||
@doc """
|
||||
Lists all required files.
|
||||
@@ -568,9 +579,11 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
It accepts the same options as `env_for_eval/1`. Additionally, you may
|
||||
also pass an environment as second argument, so the evaluation happens
|
||||
within that environment.
|
||||
It accepts the same options as both `env_for_eval/1` and
|
||||
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
|
||||
as third argument, so the evaluation happens within that environment.
|
||||
|
||||
## Return
|
||||
|
||||
Returns a tuple of the form `{value, binding}`, where `value` is the value
|
||||
returned from evaluating `string`. If an error occurs while evaluating
|
||||
@@ -600,7 +613,7 @@ defmodule Code do
|
||||
iex> Enum.sort(binding)
|
||||
[a: 3, b: 2]
|
||||
|
||||
For convenience, you can pass `__ENV__/0` as the `opts` argument and
|
||||
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
|
||||
all imports, requires and aliases defined in the current environment
|
||||
will be automatically carried over:
|
||||
|
||||
@@ -612,21 +625,28 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_string(string, binding \\ [], opts \\ [])
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
|
||||
{term, binding}
|
||||
def eval_string(string, binding \\ [], opts_or_env \\ [])
|
||||
|
||||
def eval_string(string, binding, %Macro.Env{} = env) do
|
||||
validated_eval_string(string, binding, env)
|
||||
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
|
||||
end
|
||||
|
||||
def eval_string(string, binding, opts) when is_list(opts) do
|
||||
validated_eval_string(string, binding, opts)
|
||||
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
|
||||
end
|
||||
|
||||
defp validated_eval_string(string, binding, opts_or_env) do
|
||||
%{line: line, file: file} = env = env_for_eval(opts_or_env)
|
||||
defp validate_binding(binding) when is_list(binding), do: binding
|
||||
|
||||
defp validate_binding(binding) do
|
||||
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
|
||||
end
|
||||
|
||||
defp validated_eval_string(string, binding, env, opts) do
|
||||
%{line: line, file: file} = env
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
|
||||
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
|
||||
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -1129,7 +1149,8 @@ defmodule Code do
|
||||
returned quoted expressions (instead of evaluated).
|
||||
|
||||
See `eval_string/3` for a description of arguments and return types.
|
||||
The options are described under `env_for_eval/1`.
|
||||
It accepts the same options as both `env_for_eval/1` and
|
||||
`eval_quoted_with_env/4`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1151,11 +1172,20 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
|
||||
{value, binding, _env} =
|
||||
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
|
||||
{term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])
|
||||
|
||||
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
|
||||
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
|
||||
end
|
||||
|
||||
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
||||
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
|
||||
end
|
||||
|
||||
defp eval_quoted(quoted, binding, env, opts) do
|
||||
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -1183,14 +1213,9 @@ defmodule Code do
|
||||
|
||||
* `:module` - the module to run the environment on
|
||||
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
|
||||
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
|
||||
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
|
||||
|
||||
@doc """
|
||||
@@ -1204,11 +1229,19 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
It accepts the same options as `env_for_eval/1`.
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
|
||||
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
|
||||
used in the evaluated code. It must be a `{module, function, args}`
|
||||
tuple, see `dbg/2` for more details.
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
|
||||
{term, binding, Macro.Env.t()}
|
||||
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
|
||||
when is_list(binding) do
|
||||
@@ -1286,7 +1319,7 @@ defmodule Code do
|
||||
and keyword lists.
|
||||
|
||||
The encoder function will receive the atom name (as a binary) and a
|
||||
keyword list with the current file, line and column. It must return
|
||||
keyword list with the current line and column. It must return
|
||||
`{:ok, token :: term} | {:error, reason :: binary}`.
|
||||
|
||||
The encoder function is supposed to create an atom from the given
|
||||
@@ -1325,14 +1358,7 @@ defmodule Code do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
|
||||
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
|
||||
{:ok, tokens} ->
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
{:error, _error_msg} = error ->
|
||||
error
|
||||
end
|
||||
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1408,8 +1434,7 @@ defmodule Code do
|
||||
Process.put(:code_formatter_comments, [])
|
||||
opts = [preserve_comments: &preserve_comments/5] ++ opts
|
||||
|
||||
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
|
||||
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
with {:ok, forms} <- :elixir.string_to_quoted(charlist, line, column, file, opts) do
|
||||
comments = Enum.reverse(Process.get(:code_formatter_comments))
|
||||
{:ok, forms, comments}
|
||||
end
|
||||
@@ -1609,13 +1634,19 @@ defmodule Code do
|
||||
nil
|
||||
|
||||
:proceed ->
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
try do
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
catch
|
||||
kind, reason ->
|
||||
:elixir_code_server.call({:release, file})
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1710,9 +1741,6 @@ defmodule Code do
|
||||
|
||||
Available options are:
|
||||
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:debug_info` - when `true`, retains debug information in the compiled
|
||||
module. This option can also be overridden per module using the `@compile`
|
||||
directive. Defaults to `true`.
|
||||
@@ -1728,6 +1756,9 @@ defmodule Code do
|
||||
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`.
|
||||
|
||||
* `: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`.
|
||||
@@ -1739,24 +1770,37 @@ defmodule Code do
|
||||
should be using 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.
|
||||
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
|
||||
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]`.
|
||||
|
||||
When setting this option, we recommend running `mix clean` so the modules can be
|
||||
recompiled with the new behaviour. `mix test` automatically disables this option
|
||||
via the `:test_elixirc_options` project configuration, as there is typically no
|
||||
need to infer signatures for test files.
|
||||
* `:module_definition` (since v1.20.0) - stores 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:
|
||||
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
* 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
|
||||
|
||||
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
|
||||
tuples that will not emit warnings that the module or function does not exist
|
||||
at compilation time. Pass atom `:all` to skip warning for all undefined
|
||||
functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
|
||||
|
||||
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
|
||||
compilation. See the module docs for more information. Defaults to `[]`.
|
||||
* `: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
|
||||
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
|
||||
with old dependencies, its usage is discouraged and it will be removed
|
||||
in future releases.
|
||||
|
||||
* `:parser_options` (since v1.10.0) - a keyword list of options to be given
|
||||
to the parser when compiling files. It accepts the same options as
|
||||
@@ -1767,14 +1811,12 @@ defmodule Code do
|
||||
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
|
||||
latter is used for other purposes beyond compilation.
|
||||
|
||||
* `: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
|
||||
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
|
||||
with old dependencies, its usage is discouraged and it will be removed
|
||||
in future releases.
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
|
||||
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
|
||||
compilation. See the module docs for more information. Defaults to `[]`.
|
||||
|
||||
It always returns `:ok`. Raises an error for invalid options.
|
||||
|
||||
@@ -1814,6 +1856,15 @@ defmodule Code do
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(:module_definition, value) do
|
||||
if value not in [:interpreted, :compiled] do
|
||||
raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
:elixir_config.put(:module_definition, value)
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(:infer_signatures, value) do
|
||||
value =
|
||||
cond do
|
||||
|
||||
@@ -204,6 +204,7 @@ defmodule Code.Fragment do
|
||||
| {:local_arity, charlist}
|
||||
| {:local_call, charlist}
|
||||
| {:anonymous_call, inside_caller}
|
||||
| {:capture_arg, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| {:operator, charlist}
|
||||
| {:operator_arity, charlist}
|
||||
@@ -1234,14 +1235,43 @@ defmodule Code.Fragment do
|
||||
the cursor. This is necessary to correctly complete anonymous functions
|
||||
and the left-hand side of `->`
|
||||
|
||||
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
|
||||
(see "Tracking sigils" section below)
|
||||
|
||||
## Tracking sigils
|
||||
|
||||
The `:preserve_sigils` option can be used to track cursor positions inside
|
||||
a sigil.
|
||||
|
||||
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
|
||||
as the second argument:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
|
||||
{:ok,
|
||||
{:sigil_r, [delimiter: "/", line: 1],
|
||||
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
|
||||
|
||||
In case the sigil is completed and has zero or more modifiers, the cursor will
|
||||
be nested in the list, with all previous delimiters specified:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
|
||||
{:ok,
|
||||
{:sigil_r, [delimiter: "/", line: 1],
|
||||
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
|
||||
|
||||
If the cursor is after the sigil, then it is discarded as everything else:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
|
||||
{:ok, {:__cursor__, [line: 1], []}}
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
||||
def container_cursor_to_quoted(fragment, opts \\ []) do
|
||||
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
|
||||
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
|
||||
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
|
||||
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
|
||||
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
|
||||
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
@@ -1261,7 +1291,10 @@ defmodule Code.Fragment do
|
||||
end
|
||||
|
||||
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
{:ok, forms}
|
||||
end
|
||||
|
||||
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
|
||||
tokens =
|
||||
@@ -1269,7 +1302,7 @@ defmodule Code.Fragment do
|
||||
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
|
||||
true <- maybe_missing_stab?(rev_tokens, true),
|
||||
opts =
|
||||
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
|
||||
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
|
||||
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
|
||||
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
|
||||
trailing_tokens =
|
||||
@@ -1288,10 +1321,12 @@ defmodule Code.Fragment do
|
||||
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
|
||||
end
|
||||
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
{:ok, forms}
|
||||
end
|
||||
|
||||
{:error, info, _rest, _warnings, _so_far} ->
|
||||
{:error, :elixir.format_token_error(info)}
|
||||
{:error, :elixir_tokenizer.format_error(info)}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule Code.Normalizer do
|
||||
|
||||
# Bit containers
|
||||
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
||||
normalize_bitstring(quoted, state)
|
||||
normalize_bitstring(quoted, state, false)
|
||||
end
|
||||
|
||||
# Atoms with interpolations
|
||||
@@ -89,13 +89,7 @@ defmodule Code.Normalizer do
|
||||
normalize_literal(:utf8, [], state)
|
||||
end
|
||||
|
||||
string =
|
||||
if state.escape do
|
||||
normalize_bitstring(string, state, true)
|
||||
else
|
||||
normalize_bitstring(string, state)
|
||||
end
|
||||
|
||||
string = normalize_bitstring(string, state, state.escape)
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
||||
end
|
||||
|
||||
@@ -118,6 +112,7 @@ defmodule Code.Normalizer do
|
||||
end
|
||||
end)
|
||||
|
||||
parts = maybe_add_trailing_newline(call_meta, parts, state)
|
||||
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
|
||||
else
|
||||
normalize_call(quoted, state)
|
||||
@@ -405,7 +400,8 @@ defmodule Code.Normalizer do
|
||||
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 \\ false) do
|
||||
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 =
|
||||
@@ -427,6 +423,17 @@ defmodule Code.Normalizer do
|
||||
{:<<>>, meta, parts}
|
||||
end
|
||||
|
||||
defp maybe_add_trailing_newline(meta, parts, state) do
|
||||
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
|
||||
last = List.last(parts),
|
||||
true <- is_binary(last) and not String.ends_with?(last, "\n") do
|
||||
[_last | rest] = Enum.reverse(parts)
|
||||
Enum.reverse([last <> "\n" | rest])
|
||||
else
|
||||
_ -> parts
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
|
||||
Enum.map(parts, fn
|
||||
{:"::", interpolation_meta,
|
||||
|
||||
@@ -69,6 +69,22 @@ defprotocol Collectable do
|
||||
iex> Enum.into([1, 2, 3], MapSet.new())
|
||||
MapSet.new([1, 2, 3])
|
||||
|
||||
## Halting
|
||||
|
||||
The `:halt` flag will be given whenever the collection won't
|
||||
terminate correctly and must be used to clean up existing resources
|
||||
(such as sockets, file handles, etc).
|
||||
|
||||
Note it is not guaranteed that the accumulator given to halt will
|
||||
be the latest version of the accumulator returned by a previous call
|
||||
with `{:cont, elem}`. Therefore, you must track the collected results
|
||||
within the resource you intend to halt.
|
||||
|
||||
This is by design: ensuring halt is always called with the latest
|
||||
accumulator would make pure collectables (the ones that do not implement
|
||||
halt) expensive. However, given the collectables that must implement halt
|
||||
already need to track state, the burden of tracking the accumulator
|
||||
across invocations is put on them.
|
||||
"""
|
||||
|
||||
@type command :: {:cont, term} | :done | :halt
|
||||
|
||||
@@ -377,21 +377,27 @@ defmodule Config do
|
||||
end
|
||||
end
|
||||
|
||||
defp validate!(config, file) do
|
||||
Enum.all?(config, fn
|
||||
defp validate!(config, file) when is_list(config) do
|
||||
Enum.each(config, fn
|
||||
{app, value} when is_atom(app) ->
|
||||
if Keyword.keyword?(value) do
|
||||
true
|
||||
else
|
||||
if not Keyword.keyword?(value) do
|
||||
raise ArgumentError,
|
||||
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
|
||||
"to return keyword list, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
other ->
|
||||
raise ArgumentError,
|
||||
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
|
||||
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
|
||||
end)
|
||||
|
||||
config
|
||||
end
|
||||
|
||||
defp validate!(config, file) do
|
||||
raise ArgumentError,
|
||||
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
|
||||
"of {atom, keyword} pairs, got: #{inspect(config)}"
|
||||
end
|
||||
end
|
||||
|
||||
+35
-25
@@ -829,7 +829,7 @@ defmodule Enum do
|
||||
"""
|
||||
@spec dedup(t) :: list
|
||||
def dedup(enumerable) when is_list(enumerable) do
|
||||
dedup_list(enumerable, []) |> :lists.reverse()
|
||||
dedup_list(enumerable)
|
||||
end
|
||||
|
||||
def dedup(enumerable) do
|
||||
@@ -2023,11 +2023,16 @@ defmodule Enum do
|
||||
operators work by using this function.
|
||||
"""
|
||||
@spec member?(t, element) :: boolean
|
||||
def member?(enumerable, element) when is_list(enumerable) do
|
||||
def member?(enumerable, element) do
|
||||
__in__(element, enumerable)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __in__(element, enumerable) when is_list(enumerable) do
|
||||
:lists.member(element, enumerable)
|
||||
end
|
||||
|
||||
def member?(enumerable, element) do
|
||||
def __in__(element, enumerable) do
|
||||
case Enumerable.member?(enumerable, element) do
|
||||
{:ok, element} when is_boolean(element) ->
|
||||
element
|
||||
@@ -4507,19 +4512,9 @@ defmodule Enum do
|
||||
|
||||
# dedup
|
||||
|
||||
defp dedup_list([value | tail], acc) do
|
||||
acc =
|
||||
case acc do
|
||||
[^value | _] -> acc
|
||||
_ -> [value | acc]
|
||||
end
|
||||
|
||||
dedup_list(tail, acc)
|
||||
end
|
||||
|
||||
defp dedup_list([], acc) do
|
||||
acc
|
||||
end
|
||||
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
|
||||
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
|
||||
defp dedup_list([]), do: []
|
||||
|
||||
## drop
|
||||
|
||||
@@ -5168,7 +5163,16 @@ defimpl Enumerable, for: Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
|
||||
reduce =
|
||||
quote generated: true do
|
||||
reduce(
|
||||
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
|
||||
var!(acc),
|
||||
var!(fun)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(reduce) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
reduce(Map.put(range, :step, step), acc, fun)
|
||||
end
|
||||
@@ -5191,12 +5195,12 @@ defimpl Enumerable, for: Range do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
def member?(first..last//step, value) when is_integer(value) do
|
||||
if step > 0 do
|
||||
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
||||
else
|
||||
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
||||
end
|
||||
def member?(first..last//step, value) when is_integer(value) and step > 0 do
|
||||
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
||||
end
|
||||
|
||||
def member?(first..last//step, value) when is_integer(value) and step < 0 do
|
||||
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
@@ -5215,11 +5219,17 @@ defimpl Enumerable, for: Range do
|
||||
end
|
||||
|
||||
def slice(first.._//step = range) do
|
||||
{:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
|
||||
{:ok, Range.size(range), &slice(first + &1 * step, step * &3, &2)}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def slice(%{__struct__: Range, first: first, last: last} = range) do
|
||||
|
||||
slice =
|
||||
quote generated: true do
|
||||
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
|
||||
end
|
||||
|
||||
def unquote(slice) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
slice(Map.put(range, :step, step))
|
||||
end
|
||||
|
||||
@@ -26,7 +26,7 @@ defmodule Exception do
|
||||
@typedoc "The exception type"
|
||||
@type t :: %{
|
||||
required(:__struct__) => module,
|
||||
required(:__exception__) => true,
|
||||
required(:__exception__) => term,
|
||||
optional(atom) => any
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule Exception do
|
||||
@doc false
|
||||
@deprecated "Use Kernel.is_exception/1 instead"
|
||||
def exception?(term)
|
||||
def exception?(%_{__exception__: true}), do: true
|
||||
def exception?(%_{__exception__: _}), do: true
|
||||
def exception?(_), do: false
|
||||
|
||||
@doc """
|
||||
@@ -89,7 +89,7 @@ defmodule Exception do
|
||||
return a descriptive error message instead.
|
||||
"""
|
||||
@spec message(t) :: String.t()
|
||||
def message(%module{__exception__: true} = exception) do
|
||||
def message(%module{__exception__: _} = exception) do
|
||||
try do
|
||||
module.message(exception)
|
||||
rescue
|
||||
@@ -123,7 +123,7 @@ defmodule Exception do
|
||||
@spec normalize(:error, any, stacktrace) :: t
|
||||
@spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var
|
||||
def normalize(kind, payload, stacktrace \\ [])
|
||||
def normalize(:error, %_{__exception__: true} = payload, _stacktrace), do: payload
|
||||
def normalize(:error, %_{__exception__: _} = payload, _stacktrace), do: payload
|
||||
def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace)
|
||||
def normalize(_kind, payload, _stacktrace), do: payload
|
||||
|
||||
|
||||
+105
-20
@@ -400,6 +400,13 @@ defmodule File do
|
||||
|
||||
You can use `:file.format_error/1` to get a descriptive string of the error.
|
||||
|
||||
## Options (since v1.20)
|
||||
|
||||
The supported options are:
|
||||
|
||||
* `:raw` - a single atom to bypass the file server and only check
|
||||
for the file locally
|
||||
|
||||
## Examples
|
||||
|
||||
File.read("hello.txt")
|
||||
@@ -408,15 +415,24 @@ defmodule File do
|
||||
File.read("non_existing.txt")
|
||||
#=> {:error, :enoent}
|
||||
"""
|
||||
@spec read(Path.t()) :: {:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
|
||||
def read(path) do
|
||||
:file.read_file(IO.chardata_to_string(path))
|
||||
@spec read(Path.t(), [exists_option]) ::
|
||||
{:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
|
||||
when exists_option: :raw
|
||||
def read(path, opts \\ []) do
|
||||
:file.read_file(IO.chardata_to_string(path), opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary with the contents of the given filename,
|
||||
or raises a `File.Error` exception if an error occurs.
|
||||
|
||||
## Options (since v1.20)
|
||||
|
||||
The supported options are:
|
||||
|
||||
* `:raw` - a single atom to bypass the file server and only check
|
||||
for the file locally
|
||||
|
||||
## Examples
|
||||
|
||||
File.read!("hello.txt")
|
||||
@@ -425,9 +441,9 @@ defmodule File do
|
||||
File.read!("non_existing.txt")
|
||||
** (File.Error) could not read file "non_existing.txt": no such file or directory
|
||||
"""
|
||||
@spec read!(Path.t()) :: binary
|
||||
def read!(path) do
|
||||
case read(path) do
|
||||
@spec read!(Path.t(), [exists_option]) :: binary when exists_option: :raw
|
||||
def read!(path, opts \\ []) do
|
||||
case read(path, opts) do
|
||||
{:ok, binary} ->
|
||||
binary
|
||||
|
||||
@@ -1087,7 +1103,7 @@ defmodule File do
|
||||
|
||||
@doc ~S"""
|
||||
Copies the contents in `source` to `destination` recursively, maintaining the
|
||||
source directory structure and modes.
|
||||
source directory structure and regular file modes.
|
||||
|
||||
If `source` is a file or a symbolic link to it, `destination` must be a path
|
||||
to an existent file, a symbolic link to one, or a path to a non-existent file.
|
||||
@@ -1098,7 +1114,9 @@ defmodule File do
|
||||
If the source is a file, it copies `source` to `destination`. If the `source`
|
||||
is a directory, it copies the contents inside source into the `destination` directory.
|
||||
|
||||
If a file already exists in the destination, it invokes the optional `on_conflict`
|
||||
For regular files, their respective file modes are preserved in the destination.
|
||||
Directory modes are preserved only when `:preserve_directory_permissions` is `true`.
|
||||
If a file already exists in the destination, it invokes the optional `:on_conflict`
|
||||
callback given as an option. See "Options" for more information.
|
||||
|
||||
This function may fail while copying files, in such cases, it will leave the
|
||||
@@ -1116,6 +1134,12 @@ defmodule File do
|
||||
|
||||
Special files such as device files, sockets, and named pipes are not copied.
|
||||
|
||||
Typical error reasons are:
|
||||
|
||||
* `:enoent` - `source` does not exist
|
||||
* `:eisdir` - `source` is a file and `destination` is a directory
|
||||
* `:einval` - `destination` is the same as or a subdirectory of `source`
|
||||
|
||||
## Options
|
||||
|
||||
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
|
||||
@@ -1129,6 +1153,11 @@ defmodule File do
|
||||
dereferenced and have their contents copied instead when set to `true`. If the dereferenced
|
||||
files do not exist, than the operation fails. The default is `false`.
|
||||
|
||||
* `:preserve_directory_permissions` - (since v1.20.0) when `true`, the permissions of
|
||||
source directories are copied to the destination directories after their contents are
|
||||
written. This is useful when source directories are read-only or have restricted
|
||||
permissions that must be preserved. The default is `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
# Copies file "a.txt" to "b.txt"
|
||||
@@ -1146,11 +1175,16 @@ defmodule File do
|
||||
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
|
||||
|
||||
File.cp_r("non_existing.txt", "copy.txt")
|
||||
#=> {:error, :enoent}
|
||||
#=> {:error, :enoent, "non_existing.txt"}
|
||||
|
||||
# Copying into a subdirectory of source is not allowed
|
||||
File.cp_r("src", "src/dest")
|
||||
#=> {:error, :einval, "src/dest"}
|
||||
"""
|
||||
@spec cp_r(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean()
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
) ::
|
||||
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
|
||||
|
||||
@@ -1172,6 +1206,7 @@ defmodule File do
|
||||
def cp_r(source, destination, options) when is_list(options) do
|
||||
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
|
||||
dereference? = Keyword.get(options, :dereference_symlinks, false)
|
||||
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
|
||||
|
||||
source =
|
||||
source
|
||||
@@ -1183,9 +1218,25 @@ defmodule File do
|
||||
|> IO.chardata_to_string()
|
||||
|> assert_no_null_byte!("File.cp_r/3")
|
||||
|
||||
case do_cp_r(source, destination, on_conflict, dereference?, []) do
|
||||
{:error, _, _} = error -> error
|
||||
res -> {:ok, res}
|
||||
source_parts = source |> Path.expand() |> Path.split()
|
||||
dest_parts = destination |> Path.expand() |> Path.split()
|
||||
|
||||
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
|
||||
{:error, :einval, destination}
|
||||
else
|
||||
dereference = if dereference?, do: MapSet.new(), else: nil
|
||||
|
||||
case do_cp_r(
|
||||
source,
|
||||
destination,
|
||||
on_conflict,
|
||||
dereference,
|
||||
preserve_directory_permissions?,
|
||||
[]
|
||||
) do
|
||||
{:error, _, _} = error -> error
|
||||
res -> {:ok, res}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1206,7 +1257,8 @@ defmodule File do
|
||||
"""
|
||||
@spec cp_r!(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean()
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
) :: [binary]
|
||||
def cp_r!(source, destination, options \\ []) do
|
||||
case cp_r(source, destination, options) do
|
||||
@@ -1223,7 +1275,7 @@ defmodule File do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
|
||||
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
|
||||
case :elixir_utils.read_link_type(src) do
|
||||
{:ok, :regular} ->
|
||||
case do_cp_file(src, dest, on_conflict, acc) do
|
||||
@@ -1236,8 +1288,15 @@ defmodule File do
|
||||
|
||||
{:ok, :symlink} ->
|
||||
case :file.read_link(src) do
|
||||
{:ok, link} when dereference? ->
|
||||
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
|
||||
{:ok, link} when dereference != nil ->
|
||||
resolved = Path.expand(link, Path.dirname(src))
|
||||
|
||||
if MapSet.member?(dereference, resolved) do
|
||||
{:error, :eloop, src}
|
||||
else
|
||||
dereference = MapSet.put(dereference, resolved)
|
||||
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
|
||||
end
|
||||
|
||||
{:ok, link} ->
|
||||
do_cp_link(link, src, dest, on_conflict, acc)
|
||||
@@ -1251,9 +1310,35 @@ defmodule File do
|
||||
{:ok, files} ->
|
||||
case mkdir(dest) do
|
||||
success when success in [:ok, {:error, :eexist}] ->
|
||||
Enum.reduce(files, [dest | acc], fn x, acc ->
|
||||
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
|
||||
files
|
||||
|> Enum.reduce_while([dest | acc], fn x, acc ->
|
||||
case do_cp_r(
|
||||
Path.join(src, x),
|
||||
Path.join(dest, x),
|
||||
on_conflict,
|
||||
dereference,
|
||||
preserve_dir_perms?,
|
||||
acc
|
||||
) do
|
||||
{:error, _, _} = error -> {:halt, error}
|
||||
acc -> {:cont, acc}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
files when preserve_dir_perms? ->
|
||||
# Change the directory after writing files in case
|
||||
# it was originally read only
|
||||
case copy_file_mode(src, dest) do
|
||||
:ok -> files
|
||||
{:error, reason} -> {:error, reason, src}
|
||||
end
|
||||
|
||||
files ->
|
||||
files
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, dest}
|
||||
@@ -1272,7 +1357,7 @@ defmodule File do
|
||||
end
|
||||
|
||||
# If we reach this clause, there was an error while processing a file.
|
||||
defp do_cp_r(_, _, _, _, acc) do
|
||||
defp do_cp_r(_, _, _, _, _, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ defmodule File.Stream do
|
||||
|
||||
counter = fn device ->
|
||||
device = skip_bom_and_offset(device, raw, modes)
|
||||
count_lines(device, path, pattern, read_function(stream), 0)
|
||||
count_lines(device, path, pattern, read_function(stream), 0, :empty)
|
||||
end
|
||||
|
||||
{:ok, open!(stream, modes, counter)}
|
||||
@@ -229,21 +229,28 @@ defmodule File.Stream do
|
||||
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
|
||||
end
|
||||
|
||||
defp count_lines(device, path, pattern, read, count) do
|
||||
defp count_lines(device, path, pattern, read, count, last_byte) do
|
||||
case read.(device) do
|
||||
data when is_binary(data) and byte_size(data) > 0 ->
|
||||
newlines = length(:binary.matches(data, pattern))
|
||||
last = :binary.last(data)
|
||||
count_lines(device, path, pattern, read, count + newlines, last)
|
||||
|
||||
data when is_binary(data) ->
|
||||
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
|
||||
count_lines(device, path, pattern, read, count, last_byte)
|
||||
|
||||
:eof ->
|
||||
count
|
||||
case last_byte do
|
||||
:empty -> 0
|
||||
?\n -> count
|
||||
_ -> count + 1
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
end
|
||||
end
|
||||
|
||||
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
|
||||
|
||||
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
|
||||
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
|
||||
end
|
||||
|
||||
+136
-120
@@ -186,10 +186,9 @@ defmodule Float do
|
||||
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?)])
|
||||
|
||||
# When floats are expressed in scientific notation, :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?, true = _e?, acc) do
|
||||
# :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()
|
||||
@@ -200,16 +199,6 @@ defmodule Float do
|
||||
float -> {float, rest}
|
||||
end
|
||||
|
||||
defp parse_unsigned(rest, dot?, false = _e?, acc) do
|
||||
float =
|
||||
acc
|
||||
|> add_dot(dot?)
|
||||
|> :lists.reverse()
|
||||
|> :erlang.list_to_float()
|
||||
|
||||
{float, rest}
|
||||
end
|
||||
|
||||
defp add_dot(acc, true), do: acc
|
||||
defp add_dot(acc, false), do: [?0, ?. | acc]
|
||||
|
||||
@@ -355,15 +344,12 @@ defmodule Float do
|
||||
|
||||
"""
|
||||
@spec round(float, precision_range) :: float
|
||||
# This implementation is slow since it relies on big integers.
|
||||
# Faster implementations are available on more recent papers
|
||||
# and could be implemented in the future.
|
||||
def round(float, precision \\ 0)
|
||||
|
||||
def round(float, 0) when float == 0.0, do: float
|
||||
|
||||
def round(float, 0) when is_float(float) do
|
||||
case float |> :erlang.round() |> :erlang.float() do
|
||||
case :erlang.round(float) * 1.0 do
|
||||
zero when zero == 0.0 and float < 0.0 -> -0.0
|
||||
rounded -> rounded
|
||||
end
|
||||
@@ -377,140 +363,170 @@ defmodule Float do
|
||||
raise ArgumentError, invalid_precision_message(precision)
|
||||
end
|
||||
|
||||
# Decimal-place rounding via exact rational scaling. This is the bignum
|
||||
# core used by reference implementations like David M. Gay's "Correctly
|
||||
# Rounded Binary-Decimal and Decimal-Binary Conversions" (cited in the
|
||||
# @doc above), Python's round(), and Java's BigDecimal.setScale.
|
||||
#
|
||||
# 1. Decompose float exactly: |float| = mantissa / 2^shift.
|
||||
# 2. Scale exactly: |float| * 10^precision = mantissa * 10^precision / 2^shift.
|
||||
# Because precision is bounded to 0..15, the product fits in ~103 bits
|
||||
# (53-bit mantissa + ~50-bit power of ten) and BEAM bignums handle it
|
||||
# directly without approximation.
|
||||
# 3. Round the exact rational to an integer per the requested mode
|
||||
# (half_up / floor / ceil) using quotient and remainder.
|
||||
# 4. Emit the float closest to rounded_int / 10^precision:
|
||||
# - fast path: when rounded_int < 2^53, both operands are exactly
|
||||
# representable as floats and IEEE division is correctly rounded.
|
||||
# - slow path: bignum alignment + manual mantissa extraction with
|
||||
# round-to-nearest-even for the trailing bit.
|
||||
#
|
||||
# The integer-rounding decision (step 3) and the binary-emission decision
|
||||
# (step 4) are deliberately independent: step 3 picks the exact rational
|
||||
# the user asked for, step 4 picks the closest float to that rational.
|
||||
# Conflating them is the classic source of double-rounding bugs.
|
||||
#
|
||||
# Faster algorithms exist (Cox 2026's table-based uscale; Ryū / Schubfach
|
||||
# for round-trip printing) but target different problems or assume
|
||||
# fixed-width machine arithmetic that BEAM doesn't expose efficiently.
|
||||
# At precision <= 15, the exact path is small, easy to audit, and fast
|
||||
# enough that a more complex algorithm has not been justified by benchmarks.
|
||||
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
|
||||
|
||||
defp round(float, precision, rounding) do
|
||||
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
|
||||
{num, count} = decompose(significant, 1)
|
||||
count = count - exp + 1023
|
||||
defp round(float, precision, mode) do
|
||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
||||
|
||||
cond do
|
||||
# Precision beyond 15 digits
|
||||
count >= 104 ->
|
||||
case rounding do
|
||||
:ceil when sign === 0 -> 1 / power_of_10(precision)
|
||||
:floor when sign === 1 -> -1 / power_of_10(precision)
|
||||
:ceil when sign === 1 -> minus_zero()
|
||||
:half_up when sign === 1 -> minus_zero()
|
||||
_ -> 0.0
|
||||
end
|
||||
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump
|
||||
# to 10^-precision; everything else rounds to signed zero).
|
||||
exp == 0 ->
|
||||
tiny_round(sign, precision, mode)
|
||||
|
||||
# We are asking more precision than we have
|
||||
count <= precision ->
|
||||
# |float| >= 2^52 — has no fractional bits, return unchanged.
|
||||
exp - 1075 >= 0 ->
|
||||
float
|
||||
|
||||
true ->
|
||||
# Difference in precision between float and asked precision
|
||||
# We subtract 1 because we need to calculate the remainder too
|
||||
diff = count - precision - 1
|
||||
|
||||
# Get up to latest so we calculate the remainder
|
||||
power_of_10 = power_of_10(diff)
|
||||
|
||||
# Convert the numerand to decimal base
|
||||
num = num * power_of_5(count)
|
||||
|
||||
# Move to the given precision - 1
|
||||
num = div(num, power_of_10)
|
||||
div = div(num, 10)
|
||||
num = rounding(rounding, sign, num, div)
|
||||
|
||||
# Convert back to float without loss
|
||||
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
|
||||
den = power_of_10(precision)
|
||||
boundary = den <<< 52
|
||||
|
||||
cond do
|
||||
num == 0 and sign == 1 ->
|
||||
minus_zero()
|
||||
|
||||
num == 0 ->
|
||||
0.0
|
||||
|
||||
num >= boundary ->
|
||||
{den, exp} = scale_down(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
|
||||
true ->
|
||||
{num, exp} = scale_up(num, boundary, 52)
|
||||
decimal_to_float(sign, num, den, exp)
|
||||
end
|
||||
mantissa = @power_of_2_to_52 ||| mantissa
|
||||
shift = 1075 - exp
|
||||
do_round(sign, mantissa, shift, precision, mode)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO remove once we require Erlang/OTP 27+
|
||||
# This function tricks the compiler to avoid this bug in previous versions:
|
||||
# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
|
||||
defp minus_zero, do: -0.0
|
||||
|
||||
defp decompose(significant, initial) do
|
||||
decompose(significant, 1, 0, initial)
|
||||
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign.
|
||||
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do
|
||||
tiny_round(sign, precision, mode)
|
||||
end
|
||||
|
||||
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
|
||||
end
|
||||
defp do_round(sign, mantissa, shift, precision, mode) do
|
||||
power = power_of_10(precision)
|
||||
product = mantissa * power
|
||||
half = 1 <<< (shift - 1)
|
||||
quotient = product >>> shift
|
||||
remainder = product - (quotient <<< shift)
|
||||
rounded_int = round_step(mode, sign, quotient, remainder, half)
|
||||
|
||||
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, last_count, acc)
|
||||
end
|
||||
cond do
|
||||
rounded_int == 0 ->
|
||||
signed_zero(sign)
|
||||
|
||||
defp decompose(<<>>, _count, last_count, acc) do
|
||||
{acc, last_count}
|
||||
end
|
||||
rounded_int < @power_of_2_to_52 <<< 1 ->
|
||||
# Both rounded_int and power fit in 53 bits, so IEEE float division
|
||||
# is correctly rounded.
|
||||
result = rounded_int / power
|
||||
if sign == 1, do: -result, else: result
|
||||
|
||||
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
|
||||
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
|
||||
|
||||
defp scale_down(num, den, exp) do
|
||||
new_den = den <<< 1
|
||||
|
||||
if num < new_den do
|
||||
{den >>> 52, exp}
|
||||
else
|
||||
scale_down(num, new_den, exp + 1)
|
||||
true ->
|
||||
bignum_to_float(sign, rounded_int, power)
|
||||
end
|
||||
end
|
||||
|
||||
defp decimal_to_float(sign, num, den, exp) do
|
||||
quo = div(num, den)
|
||||
rem = num - quo * den
|
||||
defp round_step(:half_up, _sign, quotient, remainder, half) do
|
||||
if remainder >= half, do: quotient + 1, else: quotient
|
||||
end
|
||||
|
||||
tmp =
|
||||
case den >>> 1 do
|
||||
den when rem > den -> quo + 1
|
||||
den when rem < den -> quo
|
||||
_ when (quo &&& 1) === 1 -> quo + 1
|
||||
_ -> quo
|
||||
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient
|
||||
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1
|
||||
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient
|
||||
|
||||
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1
|
||||
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient
|
||||
defp round_step(:ceil, 1, quotient, _remainder, _half), do: quotient
|
||||
|
||||
defp signed_zero(0), do: 0.0
|
||||
defp signed_zero(1), do: -0.0
|
||||
|
||||
# Result of rounding a non-zero float whose |float * 10^precision| < 0.5.
|
||||
# ceil(+) → +10^-precision, floor(-) → -10^-precision, others → signed 0.
|
||||
defp tiny_round(0, precision, :ceil), do: 1.0 / power_of_10(precision)
|
||||
defp tiny_round(1, precision, :floor), do: -1.0 / power_of_10(precision)
|
||||
defp tiny_round(sign, _precision, _mode), do: signed_zero(sign)
|
||||
|
||||
# Slow path: emit float closest to `sign * rounded_int / power` when
|
||||
# rounded_int >= 2^53. The binary emission step is always IEEE
|
||||
# round-to-nearest-even, regardless of the integer-rounding mode.
|
||||
defp bignum_to_float(sign, rounded_int, power) do
|
||||
shift_adjust = bit_length(rounded_int) - bit_length(power) - 53
|
||||
{numerator, denominator, exp} = align(rounded_int, power, shift_adjust)
|
||||
|
||||
quotient = div(numerator, denominator)
|
||||
remainder = numerator - quotient * denominator
|
||||
half = denominator >>> 1
|
||||
|
||||
mantissa =
|
||||
cond do
|
||||
remainder > half -> quotient + 1
|
||||
remainder < half -> quotient
|
||||
(quotient &&& 1) === 1 -> quotient + 1
|
||||
true -> quotient
|
||||
end
|
||||
|
||||
tmp = tmp - @power_of_2_to_52
|
||||
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
|
||||
tmp
|
||||
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper
|
||||
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient
|
||||
# or when rounding carries. Rebalance into the canonical [2^52, 2^53)
|
||||
# range so the 52-bit packing below doesn't silently truncate.
|
||||
{mantissa, exp} =
|
||||
if mantissa == @power_of_2_to_52 <<< 1,
|
||||
do: {@power_of_2_to_52, exp + 1},
|
||||
else: {mantissa, exp}
|
||||
|
||||
<<result::float>> = <<sign::1, exp + 1023::11, mantissa - @power_of_2_to_52::52>>
|
||||
result
|
||||
end
|
||||
|
||||
defp rounding(:floor, 1, _num, div), do: div + 1
|
||||
defp rounding(:ceil, 0, _num, div), do: div + 1
|
||||
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53)
|
||||
# and the resulting float = numerator/denominator * 2^(exp-52).
|
||||
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
|
||||
new_power = power <<< shift_adjust
|
||||
|
||||
defp rounding(:half_up, _sign, num, div) do
|
||||
case rem(num, 10) do
|
||||
rem when rem < 5 -> div
|
||||
rem when rem >= 5 -> div + 1
|
||||
if rounded_int < new_power <<< 53,
|
||||
do: {rounded_int, new_power, 52 + shift_adjust},
|
||||
else: {rounded_int, new_power <<< 1, 53 + shift_adjust}
|
||||
end
|
||||
|
||||
defp align(rounded_int, power, shift_adjust) do
|
||||
shifted = rounded_int <<< -shift_adjust
|
||||
|
||||
cond do
|
||||
shifted >= power <<< 53 -> {shifted, power <<< 1, 53 + shift_adjust}
|
||||
shifted >= power <<< 52 -> {shifted, power, 52 + shift_adjust}
|
||||
true -> {shifted <<< 1, power, 51 + shift_adjust}
|
||||
end
|
||||
end
|
||||
|
||||
defp rounding(_, _, _, div), do: div
|
||||
defp bit_length(0), do: 0
|
||||
defp bit_length(integer) when integer > 0, do: bit_length(integer, 0)
|
||||
defp bit_length(integer, acc) when integer >= 1 <<< 64, do: bit_length(integer >>> 64, acc + 64)
|
||||
defp bit_length(integer, acc) when integer >= 1 <<< 16, do: bit_length(integer >>> 16, acc + 16)
|
||||
defp bit_length(integer, acc) when integer >= 1 <<< 4, do: bit_length(integer >>> 4, acc + 4)
|
||||
defp bit_length(integer, acc) when integer >= 1, do: bit_length(integer >>> 1, acc + 1)
|
||||
defp bit_length(_integer, acc), do: acc
|
||||
|
||||
Enum.reduce(0..104, 1, fn x, acc ->
|
||||
defp power_of_10(unquote(x)), do: unquote(acc)
|
||||
Enum.reduce(0..15, 1, fn exponent, acc ->
|
||||
defp power_of_10(unquote(exponent)), do: unquote(acc)
|
||||
acc * 10
|
||||
end)
|
||||
|
||||
Enum.reduce(0..104, 1, fn x, acc ->
|
||||
defp power_of_5(unquote(x)), do: unquote(acc)
|
||||
acc * 5
|
||||
end)
|
||||
|
||||
@doc """
|
||||
Returns a pair of integers whose ratio is exactly equal
|
||||
to the original float and with a positive denominator.
|
||||
|
||||
@@ -232,6 +232,8 @@ defmodule GenServer do
|
||||
a name on start via the `:name` option. Registered names are also
|
||||
automatically cleaned up on termination. The supported values are:
|
||||
|
||||
* `nil` (default) - the GenServer is not registered with a name.
|
||||
|
||||
* an atom - the GenServer is registered locally (to the current node)
|
||||
with the given name using `Process.register/2`.
|
||||
|
||||
@@ -565,9 +567,12 @@ defmodule GenServer do
|
||||
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
|
||||
after a delay to attempt a restart.
|
||||
|
||||
Returning `{:stop, reason}` will cause `start_link/3` to return
|
||||
`{:error, reason}` and the process to exit with reason `reason` without
|
||||
entering the loop or calling `c:terminate/2`.
|
||||
Returning `{:error, reason}` will cause `start_link/3` to return
|
||||
`{:error, reason}`.
|
||||
|
||||
Returning `{:stop, reason}` will the process to exit with reason `reason`,
|
||||
without entering the loop or calling `c:terminate/2`. `start_link/3` will
|
||||
return `{:error, reason}`, but only if the caller is trapping exits.
|
||||
"""
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok, state}
|
||||
@@ -858,7 +863,7 @@ defmodule GenServer do
|
||||
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
|
||||
|
||||
@typedoc "The GenServer name"
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
@type name :: nil | atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc "Options used by the `start*` functions"
|
||||
@type options :: [option]
|
||||
|
||||
@@ -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)
|
||||
@@ -669,6 +689,11 @@ defimpl Inspect, for: Any do
|
||||
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
|
||||
end
|
||||
|
||||
# A temporary clause to deal with native records until they are officially supported
|
||||
def inspect(native_record, _opts) do
|
||||
:io_lib.format("~p", [native_record]) |> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
def inspect_as_struct(map, name, infos, opts) do
|
||||
open = color_doc("#" <> name <> "<", :map, opts)
|
||||
sep = color_doc(",", :map, opts)
|
||||
@@ -696,7 +721,15 @@ defimpl Inspect, for: Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
|
||||
inspect =
|
||||
quote generated: true do
|
||||
inspect(
|
||||
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
|
||||
var!(opts)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(inspect) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
inspect(Map.put(range, :step, step), opts)
|
||||
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.
|
||||
|
||||
@@ -46,8 +46,8 @@ defmodule Inspect.Opts do
|
||||
* `:limit` - limits the number of items that are inspected for tuples,
|
||||
bitstrings, maps, lists and any other collection of items, with the exception of
|
||||
printable strings and printable charlists which use the `:printable_limit` option.
|
||||
It accepts a positive integer or `:infinity`. It defaults to `100` since
|
||||
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
|
||||
It accepts a positive integer or `:infinity`. It defaults to `200` since
|
||||
`Elixir v1.20.0`, as it has better defaults to deal with nested collections.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
|
||||
|
||||
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
|
||||
charlists: :infer,
|
||||
custom_options: [],
|
||||
inspect_fun: &Inspect.inspect/2,
|
||||
limit: 100,
|
||||
limit: 200,
|
||||
pretty: false,
|
||||
printable_limit: 4096,
|
||||
safe: true,
|
||||
|
||||
@@ -18,6 +18,45 @@ defmodule Integer do
|
||||
|
||||
import Bitwise
|
||||
|
||||
@doc """
|
||||
Counts the number of set bits (1) in the binary representation of a non-negative `integer`.
|
||||
|
||||
This operation is known as the Hamming weight or population count.
|
||||
|
||||
Raises an `ArithmeticError` if `integer` is negative.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.popcount(0)
|
||||
0
|
||||
|
||||
iex> Integer.popcount(1)
|
||||
1
|
||||
|
||||
iex> Integer.popcount(0b10110101)
|
||||
5
|
||||
|
||||
iex> Integer.popcount(255)
|
||||
8
|
||||
|
||||
iex> Integer.popcount(0b1111111111111111)
|
||||
16
|
||||
|
||||
iex> Integer.popcount(-1)
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec popcount(non_neg_integer) :: non_neg_integer
|
||||
def popcount(integer) when is_integer(integer) and integer < 0,
|
||||
do: :erlang.error(:badarith, [integer])
|
||||
|
||||
def popcount(integer) when is_integer(integer),
|
||||
do: popcount(integer, 0)
|
||||
|
||||
defp popcount(0, acc), do: acc
|
||||
defp popcount(n, acc), do: popcount(n &&& n - 1, acc + 1)
|
||||
|
||||
@doc """
|
||||
Determines if `integer` is odd.
|
||||
|
||||
@@ -258,8 +297,9 @@ defmodule Integer do
|
||||
|
||||
defp undigits([], _base, acc), do: acc
|
||||
|
||||
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
defp undigits([digit | _], base, _)
|
||||
when is_integer(digit) and (digit >= base or digit <= -base),
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
|
||||
defp undigits([digit | tail], base, acc) when is_integer(digit),
|
||||
do: undigits(tail, base, acc * base + digit)
|
||||
@@ -492,8 +532,12 @@ defmodule Integer do
|
||||
|
||||
iex> Integer.extended_gcd(10, 0)
|
||||
{10, 1, 0}
|
||||
iex> Integer.extended_gcd(-10, 0)
|
||||
{10, -1, 0}
|
||||
iex> Integer.extended_gcd(0, 10)
|
||||
{10, 0, 1}
|
||||
iex> Integer.extended_gcd(0, -10)
|
||||
{10, 0, -1}
|
||||
iex> Integer.extended_gcd(0, 0)
|
||||
{0, 0, 0}
|
||||
|
||||
@@ -501,8 +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), do: {b, 0, 1}
|
||||
def extended_gcd(a, 0), do: {a, 1, 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(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
|
||||
extended_gcd(integer2, integer1, 0, 1, 1, 0)
|
||||
|
||||
+10
-10
@@ -67,7 +67,7 @@ defprotocol JSON.Encoder do
|
||||
|
||||
{io, _prefix} =
|
||||
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
|
||||
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
|
||||
key = IO.iodata_to_binary([prefix, :json.encode_binary(Atom.to_string(field)), ?:])
|
||||
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
|
||||
end)
|
||||
|
||||
@@ -130,25 +130,25 @@ end
|
||||
|
||||
defimpl JSON.Encoder, for: BitString do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_binary(value)
|
||||
:json.encode_binary(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: List do
|
||||
def encode(value, encoder) do
|
||||
:elixir_json.encode_list(value, encoder)
|
||||
:json.encode_list(value, encoder)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Integer do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_integer(value)
|
||||
:json.encode_integer(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Float do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_float(value)
|
||||
:json.encode_float(value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -408,7 +408,7 @@ defmodule JSON do
|
||||
decoders = Keyword.put_new(decoders, :null, nil)
|
||||
|
||||
try do
|
||||
:elixir_json.decode(binary, acc, Map.new(decoders))
|
||||
:json.decode(binary, acc, Map.new(decoders))
|
||||
catch
|
||||
:error, :unexpected_end ->
|
||||
{:error, {:unexpected_end, byte_size(binary)}}
|
||||
@@ -528,16 +528,16 @@ defmodule JSON do
|
||||
end
|
||||
|
||||
def protocol_encode(value, _encoder) when is_binary(value),
|
||||
do: :elixir_json.encode_binary(value)
|
||||
do: :json.encode_binary(value)
|
||||
|
||||
def protocol_encode(value, _encoder) when is_integer(value),
|
||||
do: :elixir_json.encode_integer(value)
|
||||
do: :json.encode_integer(value)
|
||||
|
||||
def protocol_encode(value, _encoder) when is_float(value),
|
||||
do: :elixir_json.encode_float(value)
|
||||
do: :json.encode_float(value)
|
||||
|
||||
def protocol_encode(value, encoder) when is_list(value),
|
||||
do: :elixir_json.encode_list(value, encoder)
|
||||
do: :json.encode_list(value, encoder)
|
||||
|
||||
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
|
||||
do: JSON.Encoder.Map.encode(value, encoder)
|
||||
|
||||
+81
-71
@@ -2081,7 +2081,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
[optimize_boolean: true, type_check: {:case, operator}],
|
||||
{:case, [], [check, [do: bools ++ error]]}
|
||||
)
|
||||
end
|
||||
@@ -2109,7 +2109,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
[optimize_boolean: true, type_check: {:case, :"!!"}],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when unquote(x_is_false_or_nil()) -> false
|
||||
@@ -2123,7 +2123,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
[optimize_boolean: true, type_check: {:case, :!}],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when unquote(x_is_false_or_nil()) -> true
|
||||
@@ -2752,7 +2752,7 @@ defmodule Kernel do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(term) do
|
||||
%_{__exception__: true} -> true
|
||||
%_{__exception__: _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2764,8 +2764,7 @@ defmodule Kernel do
|
||||
quote do
|
||||
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
:erlang.is_map_key(:__exception__, unquote(term))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2792,7 +2791,7 @@ defmodule Kernel do
|
||||
case unquote(name) do
|
||||
name when is_atom(name) ->
|
||||
case unquote(term) do
|
||||
%{__struct__: ^name, __exception__: true} -> true
|
||||
%{__struct__: ^name, __exception__: _} -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
@@ -2810,8 +2809,7 @@ defmodule Kernel do
|
||||
(is_atom(unquote(name)) or :fail) and
|
||||
:erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
:erlang.is_map_key(:__exception__, unquote(term))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4054,7 +4052,7 @@ defmodule Kernel do
|
||||
|
||||
defp build_if(condition, do: do_clause, else: else_clause) do
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
[optimize_boolean: true, type_check: {:case, :if}],
|
||||
quote do
|
||||
case unquote(condition) do
|
||||
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
|
||||
@@ -4105,9 +4103,15 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp build_unless(condition, do: do_clause, else: else_clause) do
|
||||
quote do
|
||||
if(unquote(condition), do: unquote(else_clause), else: unquote(do_clause))
|
||||
end
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, :unless}],
|
||||
quote do
|
||||
case unquote(condition) do
|
||||
x when unquote(x_is_false_or_nil()) -> unquote(do_clause)
|
||||
_ -> unquote(else_clause)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
defp build_unless(_condition, _arguments) do
|
||||
@@ -4375,7 +4379,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
|
||||
|
||||
annotate_case(
|
||||
[type_check: :expr],
|
||||
[type_check: {:case, :&&}],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
@@ -4418,7 +4422,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "||")
|
||||
|
||||
annotate_case(
|
||||
[type_check: :expr],
|
||||
[type_check: {:case, :||}],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
@@ -4699,17 +4703,13 @@ defmodule Kernel do
|
||||
false
|
||||
|
||||
[] ->
|
||||
quote do
|
||||
_ = unquote(left)
|
||||
false
|
||||
end
|
||||
# inlined as false in erlang pass
|
||||
quote(do: :lists.member(unquote(left), []))
|
||||
|
||||
[head | tail] = list ->
|
||||
# We only expand lists in the body if they are relatively
|
||||
# short and it is made only of literal expressions.
|
||||
case not in_body? or small_literal_list?(right) do
|
||||
true -> in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
|
||||
false -> quote(do: :lists.member(unquote(left), unquote(right)))
|
||||
case in_body? do
|
||||
false -> in_list(left, head, tail, expand, list)
|
||||
true -> quote(do: :lists.member(unquote(left), unquote(right)))
|
||||
end
|
||||
|
||||
%{} = right ->
|
||||
@@ -4721,7 +4721,7 @@ defmodule Kernel do
|
||||
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step)))
|
||||
|
||||
_ when in_body? ->
|
||||
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
|
||||
quote(do: Elixir.Enum.__in__(unquote(left), unquote(right)))
|
||||
|
||||
_ ->
|
||||
raise_on_invalid_args_in_2(right)
|
||||
@@ -4756,12 +4756,6 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp small_literal_list?(list) when is_list(list) and length(list) <= 32 do
|
||||
:lists.all(fn x -> is_binary(x) or is_atom(x) or is_number(x) end, list)
|
||||
end
|
||||
|
||||
defp small_literal_list?(_list), do: false
|
||||
|
||||
defp in_range(left, first, last, step) when is_integer(step) do
|
||||
in_range_literal(left, first, last, step)
|
||||
end
|
||||
@@ -4821,12 +4815,12 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp in_list(left, head, tail, expand, right, in_body?) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
|
||||
defp in_list(left, head, tail, expand, right) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right), [head | tail])
|
||||
:lists.foldl("e(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
|
||||
end
|
||||
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right) do
|
||||
case expand.(tail) do
|
||||
[] ->
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(head)))
|
||||
@@ -4835,15 +4829,7 @@ defmodule Kernel do
|
||||
quote do
|
||||
Kernel.or(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
|
||||
)
|
||||
end
|
||||
|
||||
tail when in_body? ->
|
||||
quote do
|
||||
Kernel.or(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
:lists.member(unquote(left), unquote(tail))
|
||||
unquote(in_list(left, tail_head, tail, expand, right))
|
||||
)
|
||||
end
|
||||
|
||||
@@ -4852,7 +4838,7 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp comp(left, right, _expand, _right, _in_body?) do
|
||||
defp comp(left, right, _expand, _right) do
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(right)))
|
||||
end
|
||||
|
||||
@@ -5228,6 +5214,11 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defmacro defmodule(alias, [{:do, _block}, {atom, _} | _]) when is_atom(atom) do
|
||||
raise ArgumentError,
|
||||
"unexpected reserved word at the top-level of the \"defmodule #{Macro.to_string(alias)}\" do-block: #{atom}"
|
||||
end
|
||||
|
||||
defp module_meta({_, meta, _}), do: meta
|
||||
defp module_meta(_), do: []
|
||||
|
||||
@@ -5880,11 +5871,15 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a macro suitable for use in guard expressions.
|
||||
Defines a custom guard with the given name.
|
||||
|
||||
It raises at compile time if the `guard` uses expressions that aren't
|
||||
allowed in [guard clauses](patterns-and-guards.html#guards),
|
||||
and otherwise creates a macro that can be used both inside or outside guards.
|
||||
Once defined, custom guards can be invoked within regular code or in
|
||||
guards. The module that contains the custom guard must be required before usage.
|
||||
|
||||
Custom guards are defined by providing a valid guard expression to
|
||||
the right-hand side of `when`. `defguard` will then expand and validate
|
||||
the expressions as guards. `defguard` will raise at compile time if the
|
||||
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
|
||||
|
||||
When defining your own guards, consider the
|
||||
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
|
||||
@@ -5892,31 +5887,30 @@ defmodule Kernel do
|
||||
|
||||
## Example
|
||||
|
||||
For example, to define a guard similar to `Integer.is_even/1`, you can write:
|
||||
|
||||
defmodule Integer.Guards do
|
||||
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
|
||||
end
|
||||
|
||||
defmodule Collatz do
|
||||
@moduledoc "Tools for working with the Collatz sequence."
|
||||
import Integer.Guards
|
||||
which can then be used as:
|
||||
|
||||
@doc "Determines the number of steps `n` takes to reach `1`."
|
||||
# If this function never converges, please let me know what `n` you used.
|
||||
def converge(n) when n > 0, do: step(n, 0)
|
||||
require Integer.Guards
|
||||
Integer.Guards.is_even(3)
|
||||
#=> false
|
||||
|
||||
defp step(1, step_count) do
|
||||
step_count
|
||||
end
|
||||
## Implementation details
|
||||
|
||||
defp step(n, step_count) when is_even(n) do
|
||||
step(div(n, 2), step_count + 1)
|
||||
end
|
||||
Behind the scenes, `defguard` will generate a macro which can be used
|
||||
inside and outside of guards, preserving their respective semantics.
|
||||
|
||||
defp step(n, step_count) do
|
||||
step(3 * n + 1, step_count + 1)
|
||||
end
|
||||
end
|
||||
When invoked inside a guard, it behaves as if the right-hand side of
|
||||
`when` is injected as part of the guard, replacing the custom guard
|
||||
arguments by the expressions given as inputs.
|
||||
|
||||
When invoked outside of a guard, it preserves regular function calling
|
||||
semantics with one caveat: all arguments are evaluated before invocation,
|
||||
except arguments which are unused, which are then never evaluated.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec defguard(Macro.t()) :: Macro.t()
|
||||
@@ -6331,7 +6325,15 @@ defmodule Kernel do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
|
||||
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
|
||||
# The compiling process may override the callback by putting it in
|
||||
# the process dictionary.
|
||||
dbg_callback =
|
||||
case :erlang.get({:elixir, :dbg_callback}) do
|
||||
:undefined -> Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
|
||||
value -> value
|
||||
end
|
||||
|
||||
{mod, fun, args} = dbg_callback
|
||||
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
|
||||
end
|
||||
|
||||
@@ -6420,12 +6422,20 @@ defmodule Kernel do
|
||||
{microsecond, _precision} = duration.microsecond
|
||||
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
|
||||
|
||||
duration.week * unquote(week_in_ms) +
|
||||
duration.day * unquote(day_in_ms) +
|
||||
duration.hour * unquote(hour_in_ms) +
|
||||
duration.minute * 60_000 +
|
||||
duration.second * 1000 +
|
||||
millisecond
|
||||
total =
|
||||
duration.week * unquote(week_in_ms) +
|
||||
duration.day * unquote(day_in_ms) +
|
||||
duration.hour * unquote(hour_in_ms) +
|
||||
duration.minute * 60_000 +
|
||||
duration.second * 1000 +
|
||||
millisecond
|
||||
|
||||
if total < 0 do
|
||||
raise ArgumentError,
|
||||
"duration must be positive, got: #{inspect(duration)}"
|
||||
end
|
||||
|
||||
total
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ defmodule Kernel.CLI do
|
||||
parse_argv(t, %{config | verbose_compile: true})
|
||||
end
|
||||
|
||||
defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
|
||||
defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do
|
||||
parse_argv(t, %{config | profile: :time})
|
||||
end
|
||||
|
||||
|
||||
@@ -25,9 +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: ([module()], [Code.diagnostic(:warning)] ->
|
||||
{:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
long_compilation_threshold: pos_integer(),
|
||||
long_verification_threshold: pos_integer(),
|
||||
verification: boolean(),
|
||||
@@ -198,8 +197,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
deprecation warnings, and type checking should run. Defaults to `true`.
|
||||
We recommend disabling it only for debugging purposes.
|
||||
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
|
||||
each module type check, and group pass checker
|
||||
|
||||
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
|
||||
after compilation (see `Code.purge_compiler_modules/0`)
|
||||
@@ -340,7 +339,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
defp spawn_workers(schedulers, checker, files, output, options) do
|
||||
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
|
||||
timer_ref = Process.send_after(self(), :threshold_check, threshold)
|
||||
timer_ref = :erlang.send_after(threshold, self(), :threshold_check)
|
||||
|
||||
purge_compiler_modules =
|
||||
if Keyword.get(options, :purge_compiler_modules, false) do
|
||||
@@ -460,7 +459,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
modules = write_module_binaries(result, state.output, state)
|
||||
profile(state, "after compile callback", state.after_compile)
|
||||
|
||||
runtime_warnings =
|
||||
{runtime_warnings, errors} =
|
||||
if state.verification? do
|
||||
profile(
|
||||
state,
|
||||
@@ -471,11 +470,19 @@ defmodule Kernel.ParallelCompiler do
|
||||
fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end
|
||||
)
|
||||
else
|
||||
[]
|
||||
{[], []}
|
||||
end
|
||||
|
||||
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
|
||||
{{:ok, modules, info}, state}
|
||||
|
||||
case errors do
|
||||
[] ->
|
||||
{{:ok, modules, info}, state}
|
||||
|
||||
_ ->
|
||||
IO.puts(:stderr, "== Type checking failed with errors ==")
|
||||
{{:error, errors, info}, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
|
||||
@@ -833,7 +840,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
|
||||
timer_ref = :erlang.send_after(state.long_compilation_threshold, self(), :threshold_check)
|
||||
state = %{state | timer_ref: timer_ref}
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
|
||||
@@ -255,9 +255,20 @@ defmodule Kernel.Typespec do
|
||||
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = type_pair ->
|
||||
if built_in_type?(name, arity) do
|
||||
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
|
||||
compile_error(env, message)
|
||||
cond do
|
||||
# This is a built-in type since OTP 29 but it just generates a warning for now
|
||||
{name, arity} == {:record, 0} ->
|
||||
IO.warn("type #{name}/#{arity} is overriding a built-in type",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
|
||||
built_in_type?(name, arity) ->
|
||||
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
|
||||
compile_error(env, message)
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
|
||||
if Map.has_key?(type_pairs, type_pair) do
|
||||
@@ -599,8 +610,7 @@ defmodule Kernel.Typespec do
|
||||
types =
|
||||
:lists.map(
|
||||
fn %{field: field} ->
|
||||
default_type = if field == :__exception__, do: true, else: quote(do: term())
|
||||
{field, Keyword.get(fields, field, default_type)}
|
||||
{field, Keyword.get(fields, field, quote(do: term()))}
|
||||
end,
|
||||
struct_info
|
||||
)
|
||||
|
||||
@@ -266,7 +266,7 @@ defmodule Kernel.Utils do
|
||||
module.exception([])
|
||||
end
|
||||
|
||||
def raise(%_{__exception__: true} = exception) do
|
||||
def raise(%_{__exception__: _} = exception) do
|
||||
exception
|
||||
end
|
||||
|
||||
@@ -339,7 +339,7 @@ defmodule Kernel.Utils do
|
||||
unquote(literal_quote(unquote_every_ref(expr, vars), []))
|
||||
|
||||
false ->
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -369,7 +369,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
|
||||
# Prefaces `guard` with unquoted versions of `refs`.
|
||||
defp unquote_refs_once(guard, refs, module) do
|
||||
defp unquote_refs_once(guard, refs, %{module: module}) do
|
||||
{guard, used_refs} =
|
||||
Macro.postwalk(guard, %{}, fn
|
||||
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
|
||||
|
||||
+20
-11
@@ -119,14 +119,23 @@ defmodule Keyword do
|
||||
|
||||
iex> Keyword.from_keys([:foo, :bar, :baz], :atom)
|
||||
[foo: :atom, bar: :atom, baz: :atom]
|
||||
|
||||
iex> Keyword.from_keys([], :atom)
|
||||
[]
|
||||
|
||||
iex> Keyword.from_keys(["foo"], :bar)
|
||||
** (ArgumentError) expected a list of atoms as keys, got: "foo"
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec from_keys([key], value) :: t(value)
|
||||
def from_keys(keys, value) when is_list(keys) do
|
||||
:lists.map(&{&1, value}, keys)
|
||||
:lists.map(
|
||||
fn
|
||||
key when is_atom(key) -> {key, value}
|
||||
other -> raise ArgumentError, "expected a list of atoms as keys, got: #{inspect(other)}"
|
||||
end,
|
||||
keys
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -261,34 +270,34 @@ defmodule Keyword do
|
||||
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
|
||||
{:ok, keyword()} | {:error, [atom]}
|
||||
def validate(keyword, values) when is_list(keyword) and is_list(values) do
|
||||
validate(keyword, values, [], [], [])
|
||||
validate(keyword, values, [], keyword, [])
|
||||
end
|
||||
|
||||
defp validate([{key, _} = pair | keyword], values1, values2, acc, bad_keys) when is_atom(key) do
|
||||
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, [pair | acc], bad_keys)
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
|
||||
:error ->
|
||||
case find_key!(key, values2, values1) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate(keyword, values1, values2, acc, [key | bad_keys])
|
||||
validate(keyword, values1, values2, original, [key | bad_keys])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], values1, values2, acc, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
|
||||
defp validate([], values1, values2, original, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
|
||||
end
|
||||
|
||||
defp validate([], _values1, _values2, _acc, bad_keys) do
|
||||
defp validate([], _values1, _values2, _original, bad_keys) do
|
||||
{:error, bad_keys}
|
||||
end
|
||||
|
||||
defp validate([pair | _], _values1, _values2, _acc, []) do
|
||||
defp validate([pair | _], _values1, _values2, _original, []) do
|
||||
raise ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
end
|
||||
@@ -956,7 +965,7 @@ defmodule Keyword do
|
||||
iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
|
||||
true
|
||||
|
||||
Comparison between values is done with `===/3`,
|
||||
Comparison between values is done with `===/2`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Keyword.equal?([a: 1.0], [a: 1])
|
||||
|
||||
@@ -1193,7 +1193,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list)}
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
"""
|
||||
else
|
||||
result when is_binary(result) ->
|
||||
@@ -1245,7 +1245,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list)}
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
"""
|
||||
else
|
||||
result when is_list(result) ->
|
||||
|
||||
+10
-4
@@ -971,9 +971,15 @@ defmodule Macro do
|
||||
}
|
||||
]
|
||||
def struct_info!(module, env) when is_atom(module) do
|
||||
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
|
||||
{:ok, info} -> info
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
meta = [line: env.line]
|
||||
|
||||
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do
|
||||
{:ok, info} ->
|
||||
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
|
||||
info
|
||||
|
||||
{:error, desc} ->
|
||||
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2190,7 +2196,7 @@ defmodule Macro do
|
||||
Please check `expand_literals/2` for use cases and pitfalls.
|
||||
"""
|
||||
@doc since: "1.14.1"
|
||||
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
|
||||
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: {t(), acc} when acc: term()
|
||||
def expand_literals(ast, acc, fun)
|
||||
|
||||
def expand_literals({:__aliases__, meta, args}, acc, fun) do
|
||||
|
||||
@@ -367,7 +367,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
|
||||
{:ok, t} | {:error, String.t()}
|
||||
def define_require(env, meta, module, opts \\ [])
|
||||
when is_list(meta) and is_atom(module) and is_list(opts) do
|
||||
{trace, opts} = Keyword.pop(opts, :trace, true)
|
||||
|
||||
+13
-4
@@ -547,6 +547,8 @@ defmodule Map do
|
||||
:erlang.error({:badmap, non_map})
|
||||
end
|
||||
|
||||
defp take([], _map, []), do: %{}
|
||||
|
||||
defp take([], _map, acc) do
|
||||
:maps.from_list(acc)
|
||||
end
|
||||
@@ -1107,7 +1109,7 @@ defmodule Map do
|
||||
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
|
||||
false
|
||||
|
||||
Comparison between keys and values is done with `===/3`,
|
||||
Comparison between keys and values is done with `===/2`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Map.equal?(%{a: 1.0}, %{a: 1})
|
||||
@@ -1136,9 +1138,9 @@ defmodule Map do
|
||||
> #### Performance considerations {: .tip}
|
||||
>
|
||||
> If you find yourself doing multiple calls to `Map.filter/2`
|
||||
> and `Map.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 `Map.new/1`.
|
||||
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
|
||||
> a map at the end using `Map.new/1` or `Map.new/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1170,6 +1172,13 @@ defmodule Map do
|
||||
|
||||
See also `filter/2`.
|
||||
|
||||
> #### Performance considerations {: .tip}
|
||||
>
|
||||
> If you find yourself doing multiple calls to `Map.filter/2`
|
||||
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
|
||||
> a map at the end using `Map.new/1` or `Map.new/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
|
||||
|
||||
@@ -57,7 +57,8 @@ defmodule MapSet do
|
||||
|
||||
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
|
||||
# either via module attributes or by the compiler.
|
||||
@typep internal(value) :: %{optional(value) => term()}
|
||||
# Defaulting to a broad `term()` type to prevent opaqueness violations.
|
||||
@typep internal(_value) :: term()
|
||||
|
||||
@type t(value) :: %__MODULE__{map: internal(value)}
|
||||
@type t :: t(term)
|
||||
|
||||
+45
-35
@@ -656,7 +656,8 @@ defmodule Module do
|
||||
@spec module_info(:attributes) :: keyword()
|
||||
@spec module_info(:compile) :: keyword()
|
||||
@spec module_info(:md5) :: binary()
|
||||
@spec module_info(:nifs) :: module()
|
||||
@spec module_info(:nifs) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
@spec module_info(:exports) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
@spec module_info(:functions) :: [function_info]
|
||||
@@ -679,7 +680,7 @@ defmodule Module do
|
||||
information. For documentation, see `c:Module.__info__/1`.
|
||||
'''
|
||||
|
||||
@type definition :: {atom, arity}
|
||||
@type definition :: {function_name :: atom, arity}
|
||||
@type def_kind :: :def | :defp | :defmacro | :defmacrop
|
||||
|
||||
@type create_opts :: [
|
||||
@@ -958,7 +959,7 @@ defmodule Module do
|
||||
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -984,7 +985,7 @@ defmodule Module do
|
||||
It handles binaries and atoms. If one of the aliases
|
||||
is nil, it is discarded.
|
||||
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1015,7 +1016,7 @@ defmodule Module do
|
||||
If the alias was not referenced yet, fails with `ArgumentError`.
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1039,7 +1040,7 @@ defmodule Module do
|
||||
If the alias was not referenced yet, fails with `ArgumentError`.
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1249,17 +1250,18 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec defines?(module, definition) :: boolean
|
||||
def defines?(module, {name, arity} = tuple)
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 do
|
||||
def defines?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
|
||||
:ets.member(set, {:def, tuple})
|
||||
:ets.member(set, {:def, definition})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the module defines a function or macro of the
|
||||
given `kind`.
|
||||
given kind.
|
||||
|
||||
`kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
|
||||
`def_kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
|
||||
|
||||
This function can only be used on modules that have not yet been compiled.
|
||||
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
|
||||
@@ -1275,12 +1277,13 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec defines?(module, definition, def_kind) :: boolean
|
||||
def defines?(module, {name, arity} = tuple, def_kind)
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 and
|
||||
def defines?(module, {function_name, arity} = definition, def_kind)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 and
|
||||
def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
|
||||
|
||||
case :ets.lookup(set, {:def, tuple}) do
|
||||
case :ets.lookup(set, {:def, definition}) do
|
||||
[{_, ^def_kind, _, _, _, _}] -> true
|
||||
_ -> false
|
||||
end
|
||||
@@ -1293,7 +1296,9 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec defines_type?(module, definition) :: boolean
|
||||
def defines_type?(module, definition) when is_atom(module) do
|
||||
def defines_type?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
Kernel.Typespec.defines_type?(module, definition)
|
||||
end
|
||||
|
||||
@@ -1306,7 +1311,9 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec spec_to_callback(module, definition) :: boolean
|
||||
def spec_to_callback(module, definition) do
|
||||
def spec_to_callback(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
Kernel.Typespec.spec_to_callback(module, definition)
|
||||
end
|
||||
|
||||
@@ -1358,7 +1365,7 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec overridables_in(module) :: [atom]
|
||||
@spec overridables_in(module) :: [definition]
|
||||
def overridables_in(module) when is_atom(module) do
|
||||
assert_not_compiled!(__ENV__.function, module, :all)
|
||||
:elixir_overridable.overridables_for(module)
|
||||
@@ -1407,10 +1414,10 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec definitions_in(module, def_kind) :: [definition]
|
||||
def definitions_in(module, kind)
|
||||
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
def definitions_in(module, def_kind)
|
||||
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
{set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in)
|
||||
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
:ets.select(set, [{{{:def, :"$1"}, def_kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1441,16 +1448,17 @@ defmodule Module do
|
||||
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
|
||||
| nil
|
||||
@doc since: "1.12.0"
|
||||
def get_definition(module, {name, arity}, options \\ [])
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
|
||||
def get_definition(module, {function_name, arity} = _definition, options \\ [])
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 and is_list(options) do
|
||||
{set, bag} = data_tables_for!(module, __ENV__.function, "")
|
||||
|
||||
case :ets.lookup(set, {:def, {name, arity}}) do
|
||||
case :ets.lookup(set, {:def, {function_name, arity}}) do
|
||||
[{_key, kind, meta, _, _, _}] ->
|
||||
clauses =
|
||||
if options[:skip_clauses],
|
||||
do: [],
|
||||
else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
|
||||
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2)
|
||||
|
||||
{:v1, kind, meta, clauses}
|
||||
|
||||
@@ -1467,10 +1475,11 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec delete_definition(module, definition) :: boolean()
|
||||
def delete_definition(module, {name, arity})
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) do
|
||||
def delete_definition(module, {function_name, arity} = _definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
assert_not_compiled!(__ENV__.function, module, :writeable)
|
||||
:elixir_def.take_definition(module, {name, arity}) != false
|
||||
:elixir_def.take_definition(module, {function_name, arity}) != false
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1486,20 +1495,20 @@ defmodule Module do
|
||||
given.
|
||||
"""
|
||||
@spec make_overridable(module, [definition]) :: :ok
|
||||
def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
|
||||
def make_overridable(module, definitions) when is_atom(module) and is_list(definitions) do
|
||||
assert_not_compiled!(__ENV__.function, module, :writeable)
|
||||
|
||||
func = fn
|
||||
{function_name, arity} = tuple
|
||||
{function_name, arity} = definition
|
||||
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 ->
|
||||
case :elixir_def.take_definition(module, tuple) do
|
||||
case :elixir_def.take_definition(module, definition) do
|
||||
false ->
|
||||
raise ArgumentError,
|
||||
"cannot make function #{function_name}/#{arity} " <>
|
||||
"overridable because it was not defined"
|
||||
|
||||
clause ->
|
||||
:elixir_overridable.record_overridable(module, tuple, clause)
|
||||
:elixir_overridable.record_overridable(module, definition, clause)
|
||||
end
|
||||
|
||||
other ->
|
||||
@@ -1508,7 +1517,7 @@ defmodule Module do
|
||||
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
:lists.foreach(func, tuples)
|
||||
:lists.foreach(func, definitions)
|
||||
end
|
||||
|
||||
@spec make_overridable(module, module) :: :ok
|
||||
@@ -1566,9 +1575,10 @@ defmodule Module do
|
||||
exists or one is pending.
|
||||
"""
|
||||
@spec overridable?(module, definition) :: boolean
|
||||
def overridable?(module, {function_name, arity} = tuple)
|
||||
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 do
|
||||
:elixir_overridable.overridable_for(module, tuple) != :not_overridable
|
||||
def overridable?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
:elixir_overridable.overridable_for(module, definition) != :not_overridable
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -10,6 +10,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type error() :: term()
|
||||
@type mode() :: :erlang | :elixir | :protocol
|
||||
|
||||
@typedoc """
|
||||
@@ -19,6 +20,7 @@ defmodule Module.ParallelChecker do
|
||||
{:max_concurrency, pos_integer()}
|
||||
| {:long_verification_threshold, pos_integer()}
|
||||
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
|
||||
| {:profile, :time}
|
||||
| {atom(), term()}
|
||||
]
|
||||
|
||||
@@ -66,17 +68,37 @@ defmodule Module.ParallelChecker do
|
||||
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
|
||||
# Protocols may have been consolidated. So if we know their beam location,
|
||||
# we discard their module map on purpose and start from file.
|
||||
info =
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
List.to_string(beam_location)
|
||||
else
|
||||
cache_from_module_map(table, module_map, signatures)
|
||||
end
|
||||
|
||||
inner_spawn(pid, checker, table, module, info, log?)
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
spawn_and_register_cache(pid, checker, table, module, List.to_string(beam_location), log?)
|
||||
else
|
||||
{mode, module_tuple} = cache_from_module_map(table, module_map, signatures)
|
||||
ref = make_ref()
|
||||
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
|
||||
register_cache_and_checker(checker, mode, module, spawned, ref)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp inner_spawn(pid, checker, table, module, info, log?) do
|
||||
defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
|
||||
spawn(fn ->
|
||||
mon_ref = Process.monitor(pid)
|
||||
|
||||
receive do
|
||||
{^ref, :check, profile} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
Process.link(pid)
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
{warnings, errors} = check_module(module_tuple, {checker, table}, log?, profile)
|
||||
send(pid, {__MODULE__, module, warnings, errors})
|
||||
send(checker, {__MODULE__, :checked, ref})
|
||||
|
||||
{:DOWN, ^mon_ref, _, _, _} ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp spawn_and_register_cache(pid, checker, table, module, info, log?) do
|
||||
ref = make_ref()
|
||||
|
||||
spawned =
|
||||
@@ -87,48 +109,28 @@ defmodule Module.ParallelChecker do
|
||||
{^ref, :cache} ->
|
||||
Process.link(pid)
|
||||
|
||||
{mode, module_tuple} =
|
||||
cond do
|
||||
is_binary(info) ->
|
||||
location =
|
||||
case :code.which(module) do
|
||||
[_ | _] = path -> path
|
||||
_ -> info
|
||||
end
|
||||
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok,
|
||||
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
|
||||
else
|
||||
_ -> {:not_found, nil}
|
||||
end
|
||||
|
||||
is_tuple(info) ->
|
||||
info
|
||||
location =
|
||||
case :code.which(module) do
|
||||
[_ | _] = path -> path
|
||||
_ -> info
|
||||
end
|
||||
|
||||
# We only make the module available now, so they are not visible during inference
|
||||
:ets.insert(table, {module, mode})
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
{^ref, :check} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
warnings =
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, table}, log?)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
send(pid, {__MODULE__, module, warnings})
|
||||
send(checker, {__MODULE__, :done, module})
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", exck}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(exck) do
|
||||
mode = cache_chunk(table, module, contents)
|
||||
module_tuple = module_map_to_module_tuple(module_map)
|
||||
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
|
||||
:ets.insert(table, {module, mode})
|
||||
send(checker, {__MODULE__, :cached, module, spawned, ref})
|
||||
else
|
||||
_ ->
|
||||
# Nothing to check, so we notify everyone we are done
|
||||
:ets.insert(table, {module, :uncached})
|
||||
send(checker, {__MODULE__, :cached, module, nil, ref})
|
||||
send(pid, {__MODULE__, module, [], []})
|
||||
end
|
||||
|
||||
{:DOWN, ^mon_ref, _, _, _} ->
|
||||
@@ -136,7 +138,7 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end)
|
||||
|
||||
register(checker, module, spawned, ref)
|
||||
register_cache(checker, module, spawned, ref)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -184,39 +186,44 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
Returns the updated list of warnings from the verification.
|
||||
"""
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: {[warning()], [error()]}
|
||||
def verify({checker, table}, runtime_files) do
|
||||
value = :erlang.get(:elixir_code_diagnostics)
|
||||
log? = not match?({_, false}, value)
|
||||
|
||||
for {module, file} <- runtime_files do
|
||||
inner_spawn(self(), checker, table, module, file, log?)
|
||||
spawn_and_register_cache(self(), checker, table, module, file, log?)
|
||||
end
|
||||
|
||||
count = :gen_server.call(checker, :start, :infinity)
|
||||
diagnostics = collect_results(count, [])
|
||||
{warnings, errors} = collect_results(count, [], [])
|
||||
|
||||
case :erlang.get(:elixir_code_diagnostics) do
|
||||
:undefined -> :ok
|
||||
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
|
||||
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
|
||||
end
|
||||
|
||||
diagnostics
|
||||
{warnings, errors}
|
||||
end
|
||||
|
||||
defp collect_results(0, diagnostics) do
|
||||
diagnostics
|
||||
defp collect_results(0, warnings, errors) do
|
||||
{warnings, errors}
|
||||
end
|
||||
|
||||
defp collect_results(count, diagnostics) do
|
||||
defp collect_results(count, warnings, errors) do
|
||||
receive do
|
||||
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
diagnostic = %{diagnostic | file: file && Path.absname(file)}
|
||||
collect_results(count, [diagnostic | diagnostics])
|
||||
|
||||
{__MODULE__, _module, new_diagnostics} ->
|
||||
collect_results(count - 1, new_diagnostics ++ diagnostics)
|
||||
if Map.get(diagnostic, :severity, :warning) == :error do
|
||||
collect_results(count, warnings, [diagnostic | errors])
|
||||
else
|
||||
collect_results(count, [diagnostic | warnings], errors)
|
||||
end
|
||||
|
||||
{__MODULE__, _module, new_warnings, new_errors} ->
|
||||
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -256,7 +263,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module_tuple, cache, log?) do
|
||||
defp check_module(module_tuple, cache, log?, profile) do
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
|
||||
module_tuple
|
||||
|
||||
@@ -270,18 +277,23 @@ defmodule Module.ParallelChecker do
|
||||
definitions
|
||||
)
|
||||
|
||||
diagnostics =
|
||||
module
|
||||
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|
||||
type_warnings =
|
||||
profile(module, profile, fn ->
|
||||
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
|
||||
end)
|
||||
|
||||
{warnings, errors} =
|
||||
type_warnings
|
||||
|> Kernel.++(behaviour_warnings)
|
||||
|> group_warnings()
|
||||
|> emit_warnings(file, log?)
|
||||
|> group_diagnostics()
|
||||
|> emit_diagnostics(file, log?)
|
||||
|> Enum.split_with(&(&1.severity == :warning))
|
||||
|
||||
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
|
||||
apply(verify_mod, verify_fun, [module])
|
||||
end)
|
||||
|
||||
diagnostics
|
||||
{warnings, errors}
|
||||
end
|
||||
|
||||
defp module_map_to_module_tuple(module_map) do
|
||||
@@ -332,16 +344,16 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Warning helpers
|
||||
|
||||
defp group_warnings(warnings) do
|
||||
defp group_diagnostics(triplets) do
|
||||
{ungrouped, grouped} =
|
||||
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(warning)
|
||||
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(term)
|
||||
|
||||
if Map.get(diagnostic, :group, false) do
|
||||
locations = MapSet.new([location])
|
||||
|
||||
grouped =
|
||||
Map.update(grouped, warning, {locations, diagnostic}, fn
|
||||
Map.update(grouped, term, {locations, diagnostic}, fn
|
||||
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
|
||||
end)
|
||||
|
||||
@@ -359,7 +371,7 @@ defmodule Module.ParallelChecker do
|
||||
Enum.sort(ungrouped ++ grouped)
|
||||
end
|
||||
|
||||
defp emit_warnings(warnings, file, log?) do
|
||||
defp emit_diagnostics(warnings, file, log?) do
|
||||
Enum.flat_map(warnings, fn {locations, diagnostic} ->
|
||||
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
|
||||
log? and print_diagnostics(diagnostics)
|
||||
@@ -521,8 +533,12 @@ defmodule Module.ParallelChecker do
|
||||
:gen_server.call(server, {:unlock, module, mode}, :infinity)
|
||||
end
|
||||
|
||||
defp register(server, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register, module, pid, ref})
|
||||
defp register_cache_and_checker(server, mode, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, ref})
|
||||
end
|
||||
|
||||
defp register_cache(server, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register_cache, module, pid, ref})
|
||||
end
|
||||
|
||||
## Server callbacks
|
||||
@@ -565,11 +581,13 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
state = %{
|
||||
waiting: %{},
|
||||
modules: [],
|
||||
caches: [],
|
||||
checkers: [],
|
||||
spawned: %{},
|
||||
schedulers: schedulers,
|
||||
threshold: threshold,
|
||||
callback: callback,
|
||||
profile: Keyword.get(options, :profile),
|
||||
protocols: [],
|
||||
table: table
|
||||
}
|
||||
@@ -577,20 +595,9 @@ defmodule Module.ParallelChecker do
|
||||
:gen_server.enter_loop(__MODULE__, [], state)
|
||||
end
|
||||
|
||||
def handle_call(:start, _from, %{modules: modules, protocols: protocols, table: table} = state) do
|
||||
def handle_call(:start, _from, %{caches: caches, protocols: protocols, table: table} = state) do
|
||||
:ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
|
||||
|
||||
for {_module, pid, ref} <- modules do
|
||||
send(pid, {ref, :cache})
|
||||
end
|
||||
|
||||
for {_module, _pid, ref} <- modules do
|
||||
receive do
|
||||
{^ref, :cached} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
{:reply, length(modules), run_checkers(%{state | protocols: []})}
|
||||
{:reply, length(caches), run_caches(%{state | protocols: []})}
|
||||
end
|
||||
|
||||
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
|
||||
@@ -620,10 +627,21 @@ defmodule Module.ParallelChecker do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({__MODULE__, :done, module}, state) do
|
||||
# Unfortunately we cannot assume uniqueness because the same module
|
||||
# may be defined by mistake several times
|
||||
{timer, spawned} = Map.pop(state.spawned, module)
|
||||
def handle_info({__MODULE__, :cached, module, pid, ref}, state) do
|
||||
{_nil, spawned} = Map.pop(state.spawned, ref)
|
||||
|
||||
state =
|
||||
if pid do
|
||||
%{state | spawned: spawned, checkers: [{module, pid, ref} | state.checkers]}
|
||||
else
|
||||
%{state | spawned: spawned}
|
||||
end
|
||||
|
||||
{:noreply, run_caches(state)}
|
||||
end
|
||||
|
||||
def handle_info({__MODULE__, :checked, ref}, state) do
|
||||
{timer, spawned} = Map.pop(state.spawned, ref)
|
||||
timer && Process.cancel_timer(timer)
|
||||
{:noreply, run_checkers(%{state | spawned: spawned})}
|
||||
end
|
||||
@@ -632,11 +650,41 @@ defmodule Module.ParallelChecker do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
|
||||
{:noreply, %{state | modules: [{module, pid, ref} | modules]}}
|
||||
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do
|
||||
{:noreply, %{state | caches: [{module, pid, ref} | caches]}}
|
||||
end
|
||||
|
||||
defp run_checkers(%{modules: []} = state) do
|
||||
def handle_cast(
|
||||
{:register_cache_and_checker, mode, module, pid, ref},
|
||||
%{caches: caches, checkers: checkers} = state
|
||||
) do
|
||||
{:noreply,
|
||||
%{state | caches: [{module, mode} | caches], checkers: [{module, pid, ref} | checkers]}}
|
||||
end
|
||||
|
||||
defp run_caches(%{caches: [], spawned: spawned} = state) do
|
||||
if spawned == %{}, do: run_checkers(state), else: state
|
||||
end
|
||||
|
||||
defp run_caches(%{spawned: spawned, schedulers: schedulers} = state)
|
||||
when map_size(spawned) >= schedulers do
|
||||
state
|
||||
end
|
||||
|
||||
defp run_caches(%{caches: [cache | caches]} = state) do
|
||||
case cache do
|
||||
{_module, pid, ref} ->
|
||||
send(pid, {ref, :cache})
|
||||
spawned = Map.put(state.spawned, ref, nil)
|
||||
run_caches(%{state | caches: caches, spawned: spawned})
|
||||
|
||||
{module, mode} ->
|
||||
:ets.insert(state.table, {module, mode})
|
||||
run_caches(%{state | caches: caches})
|
||||
end
|
||||
end
|
||||
|
||||
defp run_checkers(%{checkers: []} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
@@ -645,10 +693,25 @@ defmodule Module.ParallelChecker do
|
||||
state
|
||||
end
|
||||
|
||||
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
|
||||
send(pid, {ref, :check})
|
||||
timer = Process.send_after(self(), {__MODULE__, :timeout, module, pid}, state.threshold)
|
||||
spawned = Map.put(state.spawned, module, timer)
|
||||
run_checkers(%{state | modules: modules, spawned: spawned})
|
||||
defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do
|
||||
send(pid, {ref, :check, state.profile})
|
||||
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
|
||||
spawned = Map.put(state.spawned, ref, timer)
|
||||
run_checkers(%{state | checkers: checkers, spawned: spawned})
|
||||
end
|
||||
|
||||
defp profile(module, :time, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
|
||||
)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp profile(_module, _profile, fun) do
|
||||
fun.()
|
||||
end
|
||||
end
|
||||
|
||||
+248
-44
@@ -4,7 +4,7 @@
|
||||
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
alias Module.Types.{Descr, Expr, Pattern, Helpers}
|
||||
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern}
|
||||
|
||||
# The mode controls what happens on function application when
|
||||
# there are gradual arguments. Non-gradual arguments always
|
||||
@@ -86,7 +86,7 @@ defmodule Module.Types do
|
||||
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
|
||||
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
|
||||
{[{fun_arity, inferred} | types], private, context}
|
||||
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
|
||||
else
|
||||
{types, private, context}
|
||||
end
|
||||
@@ -136,8 +136,8 @@ defmodule Module.Types do
|
||||
defp impl_for(attrs) do
|
||||
case List.keyfind(attrs, :__impl__, 0) do
|
||||
{:__impl__, [protocol: protocol, for: for]} ->
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
|
||||
{for, protocol.behaviour_info(:callbacks)}
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
|
||||
{for, protocol.__protocol__(:functions)}
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -251,17 +251,27 @@ defmodule Module.Types do
|
||||
context ->
|
||||
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
|
||||
|
||||
clauses_indexes =
|
||||
for type_index <- pending,
|
||||
not skip_unused_clause?(info, type_index),
|
||||
{clause_index, ^type_index} <- mapping,
|
||||
do: clause_index
|
||||
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)
|
||||
|
||||
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity}
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, 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)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -289,11 +299,11 @@ defmodule Module.Types do
|
||||
|
||||
local_sigs ->
|
||||
case finder.(fun_arity) do
|
||||
{mode, {fun_arity, kind, meta, clauses}, expected} ->
|
||||
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
|
||||
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
@@ -306,9 +316,10 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
|
||||
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
|
||||
context =
|
||||
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
|
||||
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
Module.Types.Traverse.of_expr(body, stack, context)
|
||||
end)
|
||||
|
||||
@@ -316,31 +327,113 @@ defmodule Module.Types do
|
||||
{inferred, [{0, 0}], context}
|
||||
end
|
||||
|
||||
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
|
||||
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
|
||||
{fun, _arity} = fun_arity
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
stack = fresh_stack(stack, mode, fun_arity)
|
||||
base_info = {:def, kind, fun, expected}
|
||||
|
||||
{_, _, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, [], [], context}, fn
|
||||
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
|
||||
context = fresh_context(context)
|
||||
case clauses do
|
||||
[{meta, args, [], {:super, _, [_ | _]} = body}] ->
|
||||
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context)
|
||||
|
||||
_ ->
|
||||
infer_local_handler(clauses, base_info, kind, fun, expected, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
|
||||
stack = with_file_meta(stack, meta)
|
||||
guards = []
|
||||
previous = Pattern.init_previous()
|
||||
fresh_context = fresh_context(context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
try do
|
||||
{trees, _, _, _, head_context} =
|
||||
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
|
||||
|
||||
# Compute the intersected arrows from the function call
|
||||
{:super, meta, call_args} = body
|
||||
{_kind, call_fun} = Keyword.fetch!(meta, :super)
|
||||
term = Descr.term()
|
||||
of_fun = &Expr.of_expr/5
|
||||
|
||||
{arrows, body_context} =
|
||||
Apply.local_arrows(call_fun, call_args, term, body, stack, head_context, of_fun)
|
||||
|
||||
# For each arrow, compute the default arrow
|
||||
{_, _, mapping, inferred} =
|
||||
Enum.reduce(arrows, {0, 0, [], []}, fn
|
||||
{clause_domain, return_type}, {index, total, mapping, inferred} ->
|
||||
of_fun = &Expr.of_expr(&1, &2, body, stack, &3)
|
||||
|
||||
{_clause_args, clause_context} =
|
||||
Helpers.zip_map_reduce(call_args, clause_domain, head_context, of_fun)
|
||||
|
||||
clause_types = Pattern.of_domain(trees, stack, clause_context)
|
||||
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, clause_types, return_type, total - 1, [])
|
||||
|
||||
total = if type_index == -1, do: total + 1, else: total
|
||||
{index + 1, total, [{0, index} | mapping], inferred}
|
||||
end)
|
||||
|
||||
domain =
|
||||
case inferred do
|
||||
[_] ->
|
||||
nil
|
||||
|
||||
_ ->
|
||||
inferred
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
|
||||
end
|
||||
|
||||
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
|
||||
end
|
||||
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} ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
fresh_context = fresh_context(acc_context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
try do
|
||||
{trees, _precise?, context} =
|
||||
Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, 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} =
|
||||
Expr.of_expr(body, Descr.term(), body, stack, context)
|
||||
Expr.of_expr(body, Descr.term(), body, stack, head_context)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
if type_index == -1 do
|
||||
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
|
||||
mapping = [{index, total} | mapping]
|
||||
{index + 1, total + 1, previous, domain, mapping, inferred, context}
|
||||
else
|
||||
{index + 1, total, [{index, type_index} | mapping], inferred, context}
|
||||
mapping = [{index, type_index} | mapping]
|
||||
{index + 1, total, previous, domain, mapping, inferred, context}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@@ -350,19 +443,69 @@ defmodule Module.Types do
|
||||
|
||||
domain =
|
||||
case clauses_types do
|
||||
[_] ->
|
||||
nil
|
||||
|
||||
_ ->
|
||||
clauses_types
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
|
||||
[_] -> nil
|
||||
_ -> domain
|
||||
end
|
||||
|
||||
inferred = {:infer, domain, Enum.reverse(clauses_types)}
|
||||
{inferred, mapping, restore_context(clauses_context, context)}
|
||||
end
|
||||
|
||||
defp compute_domain(
|
||||
[arg | args_types],
|
||||
[head_arg | head_args_types],
|
||||
[no_prev_arg | no_prev_args_types],
|
||||
[d | domain]
|
||||
) do
|
||||
[
|
||||
# This is an optimization that broadens the domain, but it is acceptable
|
||||
# because the domain is used for reverse arrows and not type checking.
|
||||
#
|
||||
# The overall idea is that, if we have a function with three clauses,
|
||||
# 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)
|
||||
#
|
||||
# Where `rN` represents the refinement in every function body.
|
||||
#
|
||||
# What this function does is, if the type of a given arg in a clause
|
||||
# before and after the body is the same (meaning r3 is term), then
|
||||
# we replace all of `(c3 - c2 - c1)` by just `c3`, which removes
|
||||
# many of the differences in the node. However, keep in mind that,
|
||||
# because `r2` may have refine `c2` in the previous clause, the domain
|
||||
# may end-up being broader. Take this example:
|
||||
#
|
||||
# % %{..., foo: integer()} -> binary()
|
||||
# def example(%{foo: var}), do: Integer.to_string(var)
|
||||
#
|
||||
# % %{...} and not %{..., foo: term()} -> :error
|
||||
# def example(%{}), do: :error
|
||||
#
|
||||
# The actual domain is:
|
||||
#
|
||||
# %{..., foo: not_set()} or %{..., foo: integer()}
|
||||
# #=> %{..., foo: if_set(integer())}
|
||||
#
|
||||
# But we will infer:
|
||||
#
|
||||
# %{...} or %{..., foo: integer()}
|
||||
# #=> %{...}
|
||||
#
|
||||
# We lose precision but this is exactly what we want: to have simpler types.
|
||||
# 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)
|
||||
else
|
||||
Descr.union(arg, d)
|
||||
end
|
||||
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
|
||||
]
|
||||
end
|
||||
|
||||
defp compute_domain([], [], [], []), 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),
|
||||
@@ -374,6 +517,58 @@ defmodule Module.Types do
|
||||
defp add_inferred([], args, return, -1, acc),
|
||||
do: {-1, [{args, return} | Enum.reverse(acc)]}
|
||||
|
||||
# Compact clauses that have the same return and differ in exactly one
|
||||
# argument by unioning that argument. For example:
|
||||
#
|
||||
# (integer(), atom() -> boolean()) and (float(), atom() -> boolean())
|
||||
#
|
||||
# becomes:
|
||||
#
|
||||
# (number(), atom() -> boolean())
|
||||
#
|
||||
# Arity-zero clauses have no argument position to widen.
|
||||
defp group_clauses_by_return({:infer, domain, [{[_ | _], _} | _] = clauses}) do
|
||||
clauses =
|
||||
Enum.reduce(clauses, [], fn {args, return}, acc ->
|
||||
group_clause_by_return(acc, args, return)
|
||||
end)
|
||||
|
||||
{:infer, domain, clauses}
|
||||
end
|
||||
|
||||
defp group_clauses_by_return(info), do: info
|
||||
|
||||
defp group_clause_by_return([{existing_args, return} | tail], args, return) do
|
||||
case union_args(existing_args, args, [], false) do
|
||||
nil ->
|
||||
[{existing_args, return} | group_clause_by_return(tail, args, return)]
|
||||
|
||||
new_args ->
|
||||
[{new_args, return} | tail]
|
||||
end
|
||||
end
|
||||
|
||||
defp group_clause_by_return([head | tail], args, return) do
|
||||
[head | group_clause_by_return(tail, args, return)]
|
||||
end
|
||||
|
||||
defp group_clause_by_return([], args, return), do: [{args, return}]
|
||||
|
||||
defp union_args([arg | existing], [arg | args], acc, changed?) do
|
||||
union_args(existing, args, [arg | acc], changed?)
|
||||
end
|
||||
|
||||
# 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)
|
||||
end
|
||||
|
||||
defp union_args([_ | _], [_ | _], _acc, true), do: nil
|
||||
|
||||
# In theory fully equal args are merged on add_inferred
|
||||
defp union_args([], [], acc, _changed?), do: Enum.reverse(acc)
|
||||
|
||||
defp with_file_meta(stack, meta) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
|
||||
@@ -435,7 +630,9 @@ defmodule Module.Types do
|
||||
# The mode to be used, see the @modes attribute
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler
|
||||
local_handler: handler,
|
||||
# Reverse arrow handling (nil | :cache | :use)
|
||||
reverse_arrow: nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -450,34 +647,41 @@ defmodule Module.Types do
|
||||
subpatterns: %{},
|
||||
# Variables that are specific to the current environment/conditional
|
||||
conditional_vars: nil,
|
||||
# Track metadata specific to matches and guards
|
||||
# Track metadata specific to patterns and guards
|
||||
pattern_info: nil,
|
||||
# If type checking has found an error/failure
|
||||
failed: false,
|
||||
# Local signatures used by local handler
|
||||
local_sigs: %{},
|
||||
# Track which clauses have been used across private local calls
|
||||
local_used: %{}
|
||||
local_used: %{},
|
||||
# Cached reverse arrows
|
||||
reverse_arrows: %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp fresh_stack(stack, mode, function) when mode in @modes do
|
||||
%{stack | mode: mode, function: function}
|
||||
%{stack | mode: mode, function: function, reverse_arrow: nil}
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
%{context | vars: %{}, failed: false}
|
||||
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
|
||||
end
|
||||
|
||||
defp restore_context(later_context, %{vars: vars, failed: failed}) do
|
||||
%{later_context | vars: vars, failed: failed}
|
||||
defp restore_context(later_context, %{
|
||||
vars: vars,
|
||||
failed: failed,
|
||||
reverse_arrows: reverse_arrows
|
||||
}) do
|
||||
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
|
||||
end
|
||||
|
||||
## Diagnostics
|
||||
|
||||
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
|
||||
%{
|
||||
message: "this clause of #{kind} #{fun}/#{arity} is never used"
|
||||
message:
|
||||
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
fas = list(tuple([atom(), integer()]))
|
||||
struct_info = list(closed_map(default: if_set(term()), field: atom()))
|
||||
|
||||
struct_info =
|
||||
list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
|
||||
|
||||
shared_info = [
|
||||
attributes: list(tuple([atom(), list(term())])),
|
||||
@@ -211,7 +213,11 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :spawn_link, [{mfargs, pid()}]},
|
||||
{:erlang, :spawn_monitor, [{[fun(0)], tuple([pid(), reference()])}]},
|
||||
{:erlang, :spawn_monitor, [{mfargs, tuple([pid(), reference()])}]},
|
||||
{:erlang, :split_binary, [{[binary(), integer()], tuple([binary(), binary()])}]},
|
||||
{:erlang, :split_binary,
|
||||
[
|
||||
{[binary(), integer()], tuple([binary(), binary()])},
|
||||
{[bitstring_no_binary(), integer()], tuple([binary(), bitstring_no_binary()])}
|
||||
]},
|
||||
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
|
||||
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
|
||||
|
||||
@@ -236,21 +242,43 @@ defmodule Module.Types.Apply do
|
||||
{: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, :is_map_key, [{[open_map(), term()], boolean()}]},
|
||||
{Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
|
||||
|
||||
## Lists
|
||||
{:lists, :member,
|
||||
[
|
||||
{[term(), empty_list()], atom([false])},
|
||||
{[term(), non_empty_list(term())], boolean()}
|
||||
]},
|
||||
|
||||
## Map
|
||||
{Map, :delete, [{[open_map(), term()], open_map()}]},
|
||||
{Map, :fetch,
|
||||
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
|
||||
{Map, :fetch!, [{[open_map(), term()], term()}]},
|
||||
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
|
||||
{Map, :get, [{[open_map(), term()], term()}]},
|
||||
{Map, :get, [{[open_map(), term(), term()], term()}]},
|
||||
{Map, :get_lazy, [{[open_map(), term(), fun(0)], term()}]},
|
||||
{Map, :has_key?, [{[open_map(), term()], boolean()}]},
|
||||
{Map, :pop, [{[open_map(), term()], tuple([term(), open_map()])}]},
|
||||
{Map, :pop, [{[open_map(), term(), term()], tuple([term(), open_map()])}]},
|
||||
{Map, :pop!, [{[open_map(), term()], tuple([term(), open_map()])}]},
|
||||
{Map, :pop_lazy, [{[open_map(), term(), fun(0)], tuple([term(), open_map()])}]},
|
||||
{Map, :put, [{[open_map(), term(), term()], open_map()}]},
|
||||
{Map, :put_new, [{[open_map(), term(), term()], open_map()}]},
|
||||
{Map, :put_new_lazy, [{[open_map(), term(), fun(0)], open_map()}]},
|
||||
{Map, :replace, [{[open_map(), term(), term()], open_map()}]},
|
||||
{Map, :replace!, [{[open_map(), term(), term()], open_map()}]},
|
||||
{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, :duplicate, [{[term(), integer()], tuple()}]},
|
||||
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
|
||||
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
|
||||
{:maps, :find,
|
||||
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
|
||||
@@ -267,8 +295,6 @@ defmodule Module.Types.Apply do
|
||||
] do
|
||||
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
|
||||
|
||||
true = Code.ensure_loaded?(mod)
|
||||
|
||||
domain_clauses =
|
||||
case clauses do
|
||||
[_] ->
|
||||
@@ -316,6 +342,18 @@ defmodule Module.Types.Apply do
|
||||
do: unquote(Macro.escape(domain_clauses))
|
||||
end
|
||||
|
||||
def signature(:erlang, :is_integer, 3),
|
||||
do:
|
||||
unquote(
|
||||
Macro.escape(
|
||||
{:strong, [term(), integer(), integer()],
|
||||
[
|
||||
{[integer(), integer(), integer()], boolean()},
|
||||
{[negation(integer()), integer(), integer()], atom([false])}
|
||||
]}
|
||||
)
|
||||
)
|
||||
|
||||
def signature(_mod, _fun, _arity), do: :none
|
||||
|
||||
@doc """
|
||||
@@ -374,63 +412,70 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
defp do_remote(:erlang, name, [left, right], expected, expr, stack, context, of_fun)
|
||||
when name in [:>=, :"=<", :>, :<, :min, :max] do
|
||||
when name in [:>=, :"=<", :>, :<] do
|
||||
case sized_order(name, left, right, expected) do
|
||||
{arg, expected, precise?, return} ->
|
||||
{actual, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
# expected is a static type, so we can use subtype? rather than compatible?
|
||||
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
|
||||
{result, context}
|
||||
|
||||
:none ->
|
||||
{left_type, context} = of_fun.(left, term(), expr, stack, context)
|
||||
{right_type, context} = of_fun.(right, term(), expr, stack, context)
|
||||
result = return(boolean(), [left_type, right_type], stack)
|
||||
|
||||
result =
|
||||
if name in [:min, :max] do
|
||||
union(left_type, right_type)
|
||||
else
|
||||
return(boolean(), [left_type, right_type], stack)
|
||||
end
|
||||
|
||||
if is_warning(stack) do
|
||||
common = intersection(left_type, right_type)
|
||||
|
||||
cond do
|
||||
# This check is incomplete. After all, we could have the number type nested
|
||||
# inside a tuple or a list and the comparison would still be valid.
|
||||
# However, nested comparison between distinct numbers is very uncommon,
|
||||
# so we only check the direct value here.
|
||||
empty?(common) and not (number_type?(left_type) and number_type?(right_type)) ->
|
||||
error = {:mismatched_comparison, left_type, right_type}
|
||||
remote_error(error, :erlang, name, 2, expr, stack, context)
|
||||
|
||||
match?({false, _}, map_fetch_key(dynamic(common), :__struct__)) ->
|
||||
error = {:struct_comparison, left_type, right_type}
|
||||
remote_error(error, :erlang, name, 2, expr, stack, context)
|
||||
|
||||
true ->
|
||||
{result, context}
|
||||
end
|
||||
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
|
||||
remote_error(error, :erlang, name, 2, expr, stack, context)
|
||||
else
|
||||
{result, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(:erlang, name, [left, right], expected, expr, stack, context, of_fun)
|
||||
when name in [:min, :max] do
|
||||
# While comparison between distinct types are allowed,
|
||||
# we check for disjointedness, so we effectively require
|
||||
# left and right to have at least one type in common.
|
||||
# Overall, it behaves as if we had this signature:
|
||||
#
|
||||
# integer(), integer() -> integer()
|
||||
# float(), float() -> float()
|
||||
# float(), integer() -> number()
|
||||
# integer(), float() -> number()
|
||||
# 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)
|
||||
|
||||
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
|
||||
remote_error(error, :erlang, name, 2, expr, stack, context)
|
||||
else
|
||||
{result, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(:erlang, :element, [index, tuple], expected, expr, stack, context, of_fun)
|
||||
when is_integer(index) do
|
||||
tuple_type = open_tuple(List.duplicate(term(), max(index - 1, 0)) ++ [expected])
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
|
||||
case tuple_fetch(tuple_type, index - 1) do
|
||||
{_optional?, value_type} ->
|
||||
{return(value_type, [tuple_type], stack), context}
|
||||
if index < 1 do
|
||||
remote_error({:negindex, index - 1}, :erlang, :element, 2, expr, stack, context)
|
||||
else
|
||||
case tuple_fetch(tuple_type, index - 1) do
|
||||
{_optional?, value_type} ->
|
||||
{return(value_type, [tuple_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
remote_error(:erlang, :element, [integer(), tuple_type], expr, stack, context)
|
||||
:badtuple ->
|
||||
remote_error(:erlang, :element, [integer(), tuple_type], expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
remote_error({:badindex, index, tuple_type}, :erlang, :element, 2, expr, stack, context)
|
||||
:badindex ->
|
||||
remote_error({:badindex, index, tuple_type}, :erlang, :element, 2, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -447,17 +492,46 @@ defmodule Module.Types.Apply do
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
|
||||
|
||||
case tuple_insert_at(tuple_type, index - 1, elem_type) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type, elem_type], stack), context}
|
||||
if index < 1 do
|
||||
remote_error({:negindex, index - 1}, :erlang, :insert_element, 3, expr, stack, context)
|
||||
else
|
||||
case tuple_insert_at(tuple_type, index - 1, elem_type) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type, elem_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
args_types = [integer(), tuple_type, elem_type]
|
||||
remote_error(:erlang, :insert_element, args_types, expr, stack, context)
|
||||
:badtuple ->
|
||||
args_types = [integer(), tuple_type, elem_type]
|
||||
remote_error(:erlang, :insert_element, args_types, expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
error = {:badindex, index - 1, tuple_type}
|
||||
remote_error(error, :erlang, :insert_element, 3, expr, stack, context)
|
||||
:badindex ->
|
||||
error = {:badindex, index - 1, tuple_type}
|
||||
remote_error(error, :erlang, :insert_element, 3, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(:erlang, :setelement, [index, tuple, elem], _, expr, stack, context, of_fun)
|
||||
when is_integer(index) do
|
||||
tuple_type = open_tuple(List.duplicate(term(), max(index, 1)))
|
||||
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
|
||||
|
||||
if index < 1 do
|
||||
remote_error({:negindex, index - 1}, :erlang, :setelement, 3, expr, stack, context)
|
||||
else
|
||||
case tuple_replace_at(tuple_type, index - 1, elem_type) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type, elem_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
args_types = [integer(), tuple_type, elem_type]
|
||||
remote_error(:erlang, :setelement, args_types, expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
error = {:badindex, index, tuple_type}
|
||||
remote_error(error, :erlang, :setelement, 3, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -466,16 +540,172 @@ defmodule Module.Types.Apply do
|
||||
tuple_type = open_tuple(List.duplicate(term(), max(index, 1)))
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
|
||||
case tuple_delete_at(tuple_type, index - 1) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type], stack), context}
|
||||
if index < 1 do
|
||||
remote_error({:negindex, index - 1}, :erlang, :delete_element, 2, expr, stack, context)
|
||||
else
|
||||
case tuple_delete_at(tuple_type, index - 1) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
remote_error(:erlang, :delete_element, [integer(), tuple_type], expr, stack, context)
|
||||
:badtuple ->
|
||||
remote_error(:erlang, :delete_element, [integer(), tuple_type], expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
error = {:badindex, index, tuple_type}
|
||||
remote_error(error, :erlang, :delete_element, 2, expr, stack, context)
|
||||
:badindex ->
|
||||
error = {:badindex, index, tuple_type}
|
||||
remote_error(error, :erlang, :delete_element, 2, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(Kernel, :elem, [tuple, index], expected, expr, stack, context, of_fun)
|
||||
when is_integer(index) do
|
||||
tuple_type = open_tuple(List.duplicate(term(), max(index, 0)) ++ [expected])
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
|
||||
if index < 0 do
|
||||
remote_error({:negindex, index}, Kernel, :elem, 2, expr, stack, context)
|
||||
else
|
||||
case tuple_fetch(tuple_type, index) do
|
||||
{_optional?, value_type} ->
|
||||
{return(value_type, [tuple_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
remote_error(Kernel, :elem, [tuple_type, integer()], expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
remote_error({:badindex, index + 1, tuple_type}, Kernel, :elem, 2, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(Kernel, :put_elem, [tuple, index, elem], _, expr, stack, context, of_fun)
|
||||
when is_integer(index) do
|
||||
tuple_type = open_tuple(List.duplicate(term(), max(index + 1, 1)))
|
||||
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
|
||||
|
||||
if index < 0 do
|
||||
remote_error({:negindex, index}, Kernel, :put_elem, 3, expr, stack, context)
|
||||
else
|
||||
case tuple_replace_at(tuple_type, index, elem_type) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type, elem_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
args_types = [tuple_type, integer(), elem_type]
|
||||
remote_error(Kernel, :put_elem, args_types, expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
error = {:badindex, index + 1, tuple_type}
|
||||
remote_error(error, Kernel, :put_elem, 3, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(Tuple, :duplicate, [elem, size], _, expr, stack, context, of_fun)
|
||||
when is_integer(size) and size >= 0 do
|
||||
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
|
||||
{return(tuple(List.duplicate(elem_type, size)), [elem_type], stack), context}
|
||||
end
|
||||
|
||||
defp do_remote(Tuple, :insert_at, [tuple, index, elem], _, expr, stack, context, of_fun)
|
||||
when is_integer(index) do
|
||||
tuple_type = open_tuple(List.duplicate(term(), max(index, 0)))
|
||||
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
|
||||
|
||||
if index < 0 do
|
||||
remote_error({:negindex, index}, Tuple, :insert_at, 3, expr, stack, context)
|
||||
else
|
||||
case tuple_insert_at(tuple_type, index, elem_type) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type, elem_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
args_types = [tuple_type, integer(), elem_type]
|
||||
remote_error(Tuple, :insert_at, args_types, expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
error = {:badindex, index, tuple_type}
|
||||
remote_error(error, Tuple, :insert_at, 3, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(Tuple, :delete_at, [tuple, index], _, expr, stack, context, of_fun)
|
||||
when is_integer(index) do
|
||||
tuple_type = open_tuple(List.duplicate(term(), max(index + 1, 1)))
|
||||
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
|
||||
|
||||
if index < 0 do
|
||||
remote_error({:negindex, index}, Tuple, :delete_at, 2, expr, stack, context)
|
||||
else
|
||||
case tuple_delete_at(tuple_type, index) do
|
||||
value_type when is_descr(value_type) ->
|
||||
{return(value_type, [tuple_type], stack), context}
|
||||
|
||||
:badtuple ->
|
||||
remote_error(Tuple, :delete_at, [tuple_type, integer()], expr, stack, context)
|
||||
|
||||
:badindex ->
|
||||
error = {:badindex, index + 1, tuple_type}
|
||||
remote_error(error, Tuple, :delete_at, 2, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remote(:lists, :member, [arg, list] = args, expected, expr, stack, context, of_fun)
|
||||
when is_list(list) do
|
||||
if list == [] or Enum.any?(list, &match?({:|, _, [_, _]}, &1)) do
|
||||
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
|
||||
else
|
||||
case booleaness(expected) do
|
||||
{polarity, _maybe_or_always} ->
|
||||
{return, acc} =
|
||||
case polarity do
|
||||
true -> {@atom_true, none()}
|
||||
false -> {@atom_false, term()}
|
||||
end
|
||||
|
||||
{expected, singleton?, context} =
|
||||
Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} ->
|
||||
{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, all_singleton?, context}
|
||||
else
|
||||
acc = if polarity, do: union(acc, type), else: acc
|
||||
{acc, false, context}
|
||||
end
|
||||
end)
|
||||
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
|
||||
cond do
|
||||
# expected can have dynamic terms but, since we have verified
|
||||
# them to be singletons, we can compute the upper bound.
|
||||
singleton? and subtype?(arg_type, upper_bound(expected)) ->
|
||||
{return, context}
|
||||
|
||||
# Singleton types with reverse polarity are negated, so we don't check for disjoint
|
||||
(singleton? and not polarity) or not is_warning(stack) ->
|
||||
{return(boolean(), [arg_type, expected], stack), context}
|
||||
|
||||
# Nothing in common between left and right, emit a warning
|
||||
disjoint?(arg_type, expected) ->
|
||||
error = {:mismatched_comparison, arg_type, list(expected)}
|
||||
remote_error(error, :lists, :member, 2, expr, stack, context)
|
||||
|
||||
true ->
|
||||
{return(boolean(), [arg_type, expected], stack), context}
|
||||
end
|
||||
|
||||
_ ->
|
||||
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -519,17 +749,29 @@ defmodule Module.Types.Apply do
|
||||
|
||||
{expected, precise?} =
|
||||
case fun do
|
||||
:length when :erlang.xor(polarity, literal > 0) ->
|
||||
{@empty_list, literal == 0}
|
||||
:length when polarity and literal == 0 ->
|
||||
{@empty_list, true}
|
||||
|
||||
:length when not polarity and literal == 0 ->
|
||||
{@non_empty_list, true}
|
||||
|
||||
:length when polarity ->
|
||||
{@non_empty_list, false}
|
||||
|
||||
:length ->
|
||||
{@non_empty_list, literal == 0}
|
||||
{@list, false}
|
||||
|
||||
:map_size when :erlang.xor(polarity, literal > 0) ->
|
||||
{@empty_map, literal == 0}
|
||||
:map_size when polarity and literal == 0 ->
|
||||
{@empty_map, true}
|
||||
|
||||
:map_size when not polarity and literal == 0 ->
|
||||
{@non_empty_map, true}
|
||||
|
||||
:map_size when polarity ->
|
||||
{@non_empty_map, false}
|
||||
|
||||
:map_size ->
|
||||
{@non_empty_map, literal == 0}
|
||||
{open_map(), false}
|
||||
|
||||
:tuple_size when polarity ->
|
||||
{tuple(List.duplicate(term(), literal)), true}
|
||||
@@ -539,6 +781,8 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
{actual, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
|
||||
# expected here is a static type, so subtyping relation is fine
|
||||
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
|
||||
|
||||
# We can skip return compare because literal is always an integer,
|
||||
@@ -553,7 +797,7 @@ defmodule Module.Types.Apply do
|
||||
compare(name, arg, literal, false, expr, stack, context, of_fun)
|
||||
|
||||
{boolean, _maybe_or_always} ->
|
||||
{literal_type, context} = of_fun.(literal, term(), expr, stack, context)
|
||||
{type, context} = of_fun.(literal, term(), expr, stack, context)
|
||||
|
||||
{polarity, return} =
|
||||
case boolean do
|
||||
@@ -563,28 +807,28 @@ 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?(literal_type) do
|
||||
expected = if polarity, do: literal_type, else: negation(literal_type)
|
||||
if singleton?(type) do
|
||||
expected = if polarity, do: type, else: negation(type)
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
result = if subtype?(arg_type, expected), do: return, else: boolean()
|
||||
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
|
||||
|
||||
# Because reverse polarity means we will infer negated types
|
||||
# (which are naturally disjoint), we skip checks in such cases
|
||||
skip_check? = not polarity
|
||||
return_compare(name, arg_type, literal_type, result, skip_check?, expr, stack, context)
|
||||
return_compare(name, arg_type, type, result, skip_check?, expr, stack, context)
|
||||
else
|
||||
expected =
|
||||
cond do
|
||||
# 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
|
||||
name in [:==, :"/="] -> numberize(literal_type)
|
||||
name in [:==, :"/="] -> numberize(type)
|
||||
# Otherwise we have the literal type as is
|
||||
true -> literal_type
|
||||
true -> type
|
||||
end
|
||||
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
return_compare(name, arg_type, literal_type, boolean(), false, expr, stack, context)
|
||||
return_compare(name, arg_type, type, boolean(), false, expr, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -621,28 +865,44 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp sized_order(name, left, right, expected) do
|
||||
if name in [:>=, :"=<", :>, :<] do
|
||||
case {left, right} do
|
||||
{{{:., _, [:erlang, fun]}, _, [arg]}, size} when is_data_size(fun, size) ->
|
||||
case booleaness(expected) do
|
||||
{true, _} -> sized_order(name, fun, size, arg, @atom_true)
|
||||
{false, _} -> sized_order(invert_order(name), fun, size, arg, @atom_false)
|
||||
_ -> :none
|
||||
end
|
||||
defp mismatched_ordered_comparison(left_type, right_type, stack) do
|
||||
if is_warning(stack) do
|
||||
cond do
|
||||
# These checks are incomplete. After all, we could have numbers and
|
||||
# structs nested inside tuples or lists, but we only check the direct
|
||||
# value here.
|
||||
not (number_type?(left_type) and number_type?(right_type)) and
|
||||
disjoint?(left_type, right_type) ->
|
||||
{:mismatched_comparison, left_type, right_type}
|
||||
|
||||
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
|
||||
case booleaness(expected) do
|
||||
{true, _} -> sized_order(invert_order(name), fun, size, arg, @atom_true)
|
||||
{false, _} -> sized_order(name, fun, size, arg, @atom_false)
|
||||
_ -> :none
|
||||
end
|
||||
match?({false, _}, map_fetch_key(dynamic(left_type), :__struct__)) and
|
||||
match?({false, _}, map_fetch_key(dynamic(right_type), :__struct__)) ->
|
||||
{:struct_comparison, left_type, right_type}
|
||||
|
||||
_ ->
|
||||
:none
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
else
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
defp sized_order(name, left, right, expected) do
|
||||
case {left, right} do
|
||||
{{{:., _, [:erlang, fun]}, _, [arg]}, size} when is_data_size(fun, size) ->
|
||||
case booleaness(expected) do
|
||||
{true, _} -> sized_order(name, fun, size, arg, @atom_true)
|
||||
{false, _} -> sized_order(invert_order(name), fun, size, arg, @atom_false)
|
||||
_ -> :none
|
||||
end
|
||||
|
||||
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
|
||||
case booleaness(expected) do
|
||||
{true, _} -> sized_order(mirror_order(name), fun, size, arg, @atom_true)
|
||||
{false, _} -> sized_order(invert_order(mirror_order(name)), fun, size, arg, @atom_false)
|
||||
_ -> :none
|
||||
end
|
||||
|
||||
_ ->
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
@@ -689,6 +949,11 @@ defmodule Module.Types.Apply do
|
||||
defp invert_order(:>), do: :"=<"
|
||||
defp invert_order(:<), do: :>=
|
||||
|
||||
defp mirror_order(:>=), do: :"=<"
|
||||
defp mirror_order(:"=<"), do: :>=
|
||||
defp mirror_order(:>), do: :<
|
||||
defp mirror_order(:<), do: :>
|
||||
|
||||
@doc """
|
||||
Returns the domain of an unknown module.
|
||||
|
||||
@@ -731,17 +996,40 @@ defmodule Module.Types.Apply do
|
||||
{info, filter_domain(info, expected, 2), 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])}
|
||||
]}
|
||||
|
||||
{info, filter_domain(info, expected, 2), context}
|
||||
end
|
||||
|
||||
def remote_domain(:erlang, :map_get, [key, _], expected, _meta, _stack, context)
|
||||
when is_atom(key) do
|
||||
domain = [term(), open_map([{key, expected}])]
|
||||
{{: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()]
|
||||
{{: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}])]
|
||||
{{: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()]
|
||||
{{: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()}])]
|
||||
@@ -803,6 +1091,13 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(:erlang, :send, info, [_dest, message] = args_types, stack) do
|
||||
case remote_apply(info, args_types, stack) do
|
||||
{:ok, _type} -> {:ok, return(message, args_types, stack)}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(:erlang, :tl, _info, [list], stack) do
|
||||
case list_tl(list) do
|
||||
{:ok, value_type} -> {:ok, return(value_type, [list], stack)}
|
||||
@@ -810,6 +1105,33 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(:erlang, :++, _info, [left, right], stack) do
|
||||
# TODO: remove once we add parametric types, this will just be:
|
||||
# empty_list(), a -> a
|
||||
# non_empty_list(elem), a -> non_empty_list(elem, a)
|
||||
case list_of(left) do
|
||||
{empty_list?, list_of} ->
|
||||
left_result =
|
||||
if empty_list? do
|
||||
right
|
||||
else
|
||||
none()
|
||||
end
|
||||
|
||||
right_result =
|
||||
if list_of do
|
||||
non_empty_list(list_of, right)
|
||||
else
|
||||
none()
|
||||
end
|
||||
|
||||
{:ok, return(union(left_result, right_result), [left, right], stack)}
|
||||
|
||||
:badproperlist ->
|
||||
{:error, badremote(:erlang, :++, [left, right])}
|
||||
end
|
||||
end
|
||||
|
||||
@struct_key atom([:__struct__])
|
||||
@nil_atom atom([nil])
|
||||
|
||||
@@ -821,6 +1143,36 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :delete, _info, [map, key] = args_types, stack) do
|
||||
case map_update(map, key, not_set(), false, true) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :delete, args_types)}
|
||||
{:error, _errors} -> {:ok, map}
|
||||
end
|
||||
end
|
||||
|
||||
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]))
|
||||
{:ok, return(result, args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
{:error, badremote(Map, :fetch, args_types)}
|
||||
|
||||
:error ->
|
||||
{:error, {:badkeydomain, map, key, atom([:error])}}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :fetch!, _info, [map, key] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(value, args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :fetch!, args_types)}
|
||||
:error -> {:error, {:badkeydomain, map, key, "raise"}}
|
||||
end
|
||||
end
|
||||
|
||||
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)}
|
||||
@@ -902,6 +1254,14 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :put, _info, [map, key, value] = args_types, stack) do
|
||||
case map_update(map, key, value, false, true) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :put, args_types)}
|
||||
{:error, _errors} -> {:ok, map}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :pop!, _info, [map, key] = args_types, stack) do
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
{value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)}
|
||||
@@ -920,6 +1280,16 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
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
|
||||
|
||||
case map_update_fun(map, key, fun, false, false) do
|
||||
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :replace!, args_types)}
|
||||
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(Map, :replace_lazy, _info, args_types, stack) do
|
||||
map_update_or_replace_lazy(:replace_lazy, args_types, stack, "do nothing")
|
||||
end
|
||||
@@ -1077,7 +1447,7 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(:maps, :values, _info, [map], stack) do
|
||||
case map_to_list(map, fn _key, value -> value end) do
|
||||
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
|
||||
:badmap -> {:error, badremote(:maps, :keys, [map])}
|
||||
:badmap -> {:error, badremote(:maps, :values, [map])}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1268,33 +1638,62 @@ defmodule Module.Types.Apply do
|
||||
## Local
|
||||
|
||||
def local(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
|
||||
{local_info, domain, context} = local_domain(fun, args, expected, meta, stack, context)
|
||||
{updated_used?, info, domain, context} =
|
||||
local_domain(fun, args, expected, meta, stack, context)
|
||||
|
||||
{args_types, context} =
|
||||
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
|
||||
|
||||
local_apply(local_info, fun, args_types, expr, stack, context)
|
||||
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
|
||||
{_indexes, type, context} -> {type, context}
|
||||
{type, context} -> {type, context}
|
||||
end
|
||||
end
|
||||
|
||||
def local_arrows(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
|
||||
{updated_used?, info, domain, context} =
|
||||
local_domain(fun, args, expected, meta, stack, context)
|
||||
|
||||
{args_types, context} =
|
||||
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
|
||||
|
||||
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
|
||||
{indexes, _type, context} -> {indexes_to_arrows(indexes, info), context}
|
||||
{type, context} -> {[{Enum.map(args, fn _ -> type end), type}], context}
|
||||
end
|
||||
end
|
||||
|
||||
defp indexes_to_arrows(indexes, {_, _domain, clauses}),
|
||||
do: indexes_to_arrows(Enum.reverse(indexes), clauses, 0)
|
||||
|
||||
defp indexes_to_arrows([index | indexes], [clause | clauses], index),
|
||||
do: [clause | indexes_to_arrows(indexes, clauses, index + 1)]
|
||||
|
||||
defp indexes_to_arrows([_ | _] = indexes, [_clause | clauses], index),
|
||||
do: indexes_to_arrows(indexes, clauses, index + 1)
|
||||
|
||||
defp indexes_to_arrows([], _clauses, _index),
|
||||
do: []
|
||||
|
||||
defp local_domain(fun, args, expected, meta, stack, context) do
|
||||
arity = length(args)
|
||||
|
||||
case stack.local_handler.(meta, {fun, arity}, stack, context) do
|
||||
false ->
|
||||
{{false, :none}, List.duplicate(term(), arity), context}
|
||||
{false, :none, List.duplicate(term(), arity), context}
|
||||
|
||||
{kind, info, context} ->
|
||||
update_used? = is_warning(stack) and kind == :defp
|
||||
|
||||
if info == :none do
|
||||
{{update_used?, :none}, List.duplicate(term(), arity), context}
|
||||
{update_used?, :none, List.duplicate(term(), arity), context}
|
||||
else
|
||||
{{update_used?, info}, filter_domain(info, expected, arity), context}
|
||||
{update_used?, info, filter_domain(info, expected, arity), context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp local_apply({update_used?, :none}, fun, args_types, _expr, _stack, context) do
|
||||
defp local_apply(update_used?, :none, fun, args_types, _expr, _stack, context) do
|
||||
if update_used? do
|
||||
{dynamic(), put_in(context.local_used[{fun, length(args_types)}], [])}
|
||||
else
|
||||
@@ -1302,7 +1701,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_apply({update_used?, info}, fun, args_types, expr, stack, context) do
|
||||
defp local_apply(update_used?, info, fun, args_types, expr, stack, context) do
|
||||
case local_apply(info, args_types, stack) do
|
||||
{indexes, type} ->
|
||||
context =
|
||||
@@ -1314,7 +1713,7 @@ defmodule Module.Types.Apply do
|
||||
context
|
||||
end
|
||||
|
||||
{type, context}
|
||||
{indexes, type, context}
|
||||
|
||||
:error ->
|
||||
error = {:badlocal, info, args_types, expr, context}
|
||||
@@ -1435,9 +1834,9 @@ defmodule Module.Types.Apply do
|
||||
domain(domain, clauses)
|
||||
end
|
||||
|
||||
defp filter_domain({_type, domain, clauses}, expected, arity) do
|
||||
defp filter_domain({_type, domain, clauses}, expected, _arity) do
|
||||
case filter_domain(clauses, expected, [], true) do
|
||||
:none -> List.duplicate(term(), arity)
|
||||
:none -> domain(domain, clauses)
|
||||
:all -> domain(domain, clauses)
|
||||
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end)
|
||||
end
|
||||
@@ -1537,7 +1936,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
{message, to_trace, hints} =
|
||||
case reason do
|
||||
{:badarg, domain} ->
|
||||
{:badarg, domain, _empty?} ->
|
||||
message = """
|
||||
incompatible types given on #{mfa_or_call}:
|
||||
|
||||
@@ -1672,6 +2071,29 @@ defmodule Module.Types.Apply do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({{:negindex, index}, mfac, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
{mod, fun, arity, _converter} = mfac
|
||||
mfa = Exception.format_mfa(mod, fun, arity)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected a non-negative integer as index in #{mfa}:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
got the index:
|
||||
|
||||
#{index}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({{:badkeydomain, map, key, error}, mfac, expr, context}) do
|
||||
{mod, fun, arity, _converter} = mfac
|
||||
mfa = Exception.format_mfa(mod, fun, arity)
|
||||
@@ -1845,7 +2267,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
def format_diagnostic({{:mismatched_comparison, left, right}, mfac, expr, context}) do
|
||||
{_, name, _, _} = mfac
|
||||
{mod, name, _, _} = mfac
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
@@ -1859,7 +2281,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
given types:
|
||||
|
||||
#{type_comparison_to_string(name, left, right) |> indent(4)}
|
||||
#{type_comparison_to_string(mod, name, left, right) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
"""
|
||||
@@ -1873,7 +2295,7 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
def format_diagnostic({{:struct_comparison, left, right}, mfac, expr, context}) do
|
||||
{_, name, _, _} = mfac
|
||||
{mod, name, _, _} = mfac
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
@@ -1887,7 +2309,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
given types:
|
||||
|
||||
#{type_comparison_to_string(name, left, right) |> indent(4)}
|
||||
#{type_comparison_to_string(mod, name, left, right) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
"""
|
||||
@@ -1902,17 +2324,10 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, :badmodule, module, fun, arity}) do
|
||||
top =
|
||||
if fun == :__struct__ and arity == 0 do
|
||||
"struct #{inspect(module)}"
|
||||
else
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
end
|
||||
|
||||
%{
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
top,
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)",
|
||||
@@ -1922,14 +2337,6 @@ defmodule Module.Types.Apply do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, {:badfunction, _}, module, :__struct__, 0}) do
|
||||
%{
|
||||
message:
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined, {:badfunction, _}, module, fun, arity}) do
|
||||
_ = Code.ensure_loaded(module)
|
||||
|
||||
@@ -1985,8 +2392,12 @@ defmodule Module.Types.Apply do
|
||||
|
||||
alias Inspect.Algebra, as: IA
|
||||
|
||||
defp type_comparison_to_string(fun, left, right) do
|
||||
{_, fun, _, _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right])
|
||||
defp type_comparison_to_string(:lists, :member, left, right) do
|
||||
type_comparison_to_string(Kernel, :in, left, right)
|
||||
end
|
||||
|
||||
defp type_comparison_to_string(mod, fun, left, right) do
|
||||
{_, fun, _, _} = :elixir_rewrite.erl_to_ex(mod, fun, [left, right])
|
||||
|
||||
{fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]}
|
||||
|> Code.Formatter.to_algebra()
|
||||
@@ -2019,7 +2430,8 @@ defmodule Module.Types.Apply do
|
||||
defp args_to_quoted_string(args_types, domain, converter) do
|
||||
docs =
|
||||
Enum.zip_with(args_types, domain, fn actual, expected ->
|
||||
if compatible?(actual, expected) or not has_simple_difference?(actual, expected) do
|
||||
if compatible?(actual, expected) or not has_simple_difference?(actual, expected) or
|
||||
term_type?(actual) do
|
||||
actual |> to_quoted() |> Code.Formatter.to_algebra()
|
||||
else
|
||||
common = intersection(actual, expected)
|
||||
|
||||
+2371
-1329
File diff suppressed because it is too large
Load Diff
+542
-310
File diff suppressed because it is too large
Load Diff
@@ -329,6 +329,13 @@ defmodule Module.Types.Helpers do
|
||||
end
|
||||
end
|
||||
|
||||
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
|
||||
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
|
||||
call
|
||||
else
|
||||
{:in, meta, [expr, args]}
|
||||
end
|
||||
|
||||
{{:., _, [Elixir.String.Chars, :to_string]}, meta, [arg]} ->
|
||||
{:to_string, meta, [arg]}
|
||||
|
||||
@@ -355,99 +362,122 @@ defmodule Module.Types.Helpers do
|
||||
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
|
||||
|
||||
{:case, meta, [expr, [do: clauses]]} ->
|
||||
if meta[:type_check] == :expr do
|
||||
case clauses do
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
case meta[:type_check] do
|
||||
{:case, op} ->
|
||||
case clauses do
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
true
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], false]}
|
||||
] ->
|
||||
{:!, meta, [expr]}
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
true
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], false]}
|
||||
]
|
||||
when op == :! ->
|
||||
{:!, meta, [expr]}
|
||||
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
right_side
|
||||
]},
|
||||
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
|
||||
] ->
|
||||
{:||, meta, [expr, right_side]}
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
right_side
|
||||
]},
|
||||
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
|
||||
]
|
||||
when op == :|| ->
|
||||
{:||, meta, [expr, right_side]}
|
||||
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
{var, _, Kernel}
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], right_side]}
|
||||
] ->
|
||||
{:&&, meta, [expr, right_side]}
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
{var, _, Kernel}
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], right_side]}
|
||||
]
|
||||
when op == :&& ->
|
||||
{:&&, meta, [expr, right_side]}
|
||||
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
else_block
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], do_block]}
|
||||
] ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
[
|
||||
{:when, _,
|
||||
[
|
||||
{var, _, Kernel},
|
||||
{{:., _, [:erlang, :orelse]}, _,
|
||||
[
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
|
||||
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
else_block
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], do_block]}
|
||||
]
|
||||
when op == :if ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
|
||||
[
|
||||
{:->, _, [[false], else_block]},
|
||||
{:->, _, [[true], do_block]}
|
||||
] ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
[
|
||||
{:->, _, [[false], else_block]},
|
||||
{:->, _, [[true], do_block]}
|
||||
]
|
||||
when op == :if ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
|
||||
_ ->
|
||||
{:case, meta, [expr, [do: {:..., [], []}]]}
|
||||
end
|
||||
else
|
||||
{:case, meta, [expr, [do: {:..., [], []}]]}
|
||||
[
|
||||
{:->, _, [[false], false]},
|
||||
{:->, _, [[true], right]}
|
||||
| _
|
||||
]
|
||||
when op == :and ->
|
||||
{:and, meta, [expr, right]}
|
||||
|
||||
[
|
||||
{:->, _, [[false], right]},
|
||||
{:->, _, [[true], true]}
|
||||
| _
|
||||
]
|
||||
when op == :or ->
|
||||
{:or, meta, [expr, right]}
|
||||
|
||||
_ ->
|
||||
{:case, meta, [expr, [do: {:..., [], []}]]}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:case, meta, [expr, [do: {:..., [], []}]]}
|
||||
end
|
||||
|
||||
{:try, meta, [[do: _] ++ _]} ->
|
||||
|
||||
@@ -48,7 +48,7 @@ defmodule Module.Types.Of do
|
||||
@doc """
|
||||
Declares a variable.
|
||||
"""
|
||||
def declare_var(var, context) do
|
||||
def declare_var(var, type \\ term(), context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
|
||||
@@ -58,7 +58,7 @@ defmodule Module.Types.Of do
|
||||
|
||||
vars ->
|
||||
data = %{
|
||||
type: term(),
|
||||
type: type,
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: [],
|
||||
@@ -86,13 +86,14 @@ defmodule Module.Types.Of do
|
||||
or if we are doing a guard analysis or occurrence typing.
|
||||
Returns `true` if there was a refinement, `false` otherwise.
|
||||
"""
|
||||
def refine_body_var(var_or_version, type, expr, stack, context, allow_empty? \\ false)
|
||||
@skip_refinement_for [term(), dynamic()]
|
||||
def refine_body_var(var_or_version, type, expr, stack, context)
|
||||
|
||||
def refine_body_var({_, meta, _}, type, expr, stack, context, allow_empty?) do
|
||||
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context, allow_empty?)
|
||||
def refine_body_var({_, meta, _}, type, expr, stack, context) do
|
||||
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
def refine_body_var(version, type, expr, stack, context, allow_empty?)
|
||||
def refine_body_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
|
||||
|
||||
@@ -105,31 +106,50 @@ defmodule Module.Types.Of do
|
||||
context
|
||||
end
|
||||
|
||||
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
|
||||
case compatible_intersection(old_type, type) do
|
||||
{:error, _} when allow_empty? ->
|
||||
data = %{
|
||||
data
|
||||
| type: none(),
|
||||
off_traces: new_trace(expr, none(), stack, off_traces)
|
||||
}
|
||||
case context do
|
||||
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
|
||||
{old_type, context}
|
||||
|
||||
{none(), %{context | vars: %{vars | version => data}}}
|
||||
%{pattern_info: %{guard_context: guard_context}} ->
|
||||
new_type = intersection(old_type, type)
|
||||
|
||||
{:ok, new_type} when new_type != old_type ->
|
||||
data = %{
|
||||
data
|
||||
| type: new_type,
|
||||
off_traces: new_trace(expr, new_type, stack, off_traces)
|
||||
}
|
||||
case empty?(new_type) do
|
||||
true when guard_context == :orelse ->
|
||||
data = %{
|
||||
data
|
||||
| type: none(),
|
||||
off_traces: new_trace(expr, none(), stack, off_traces)
|
||||
}
|
||||
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
{none(), %{context | vars: %{vars | version => data}}}
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
end
|
||||
else
|
||||
{old_type, context}
|
||||
false when new_type != old_type ->
|
||||
data = %{
|
||||
data
|
||||
| type: new_type,
|
||||
off_traces: new_trace(expr, new_type, stack, off_traces)
|
||||
}
|
||||
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
end
|
||||
|
||||
_ ->
|
||||
case gradual?(old_type) and compatible_intersection(old_type, type) do
|
||||
{:ok, new_type} when new_type != old_type ->
|
||||
data = %{
|
||||
data
|
||||
| type: new_type,
|
||||
off_traces: new_trace(expr, new_type, stack, off_traces)
|
||||
}
|
||||
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,6 +207,13 @@ defmodule Module.Types.Of do
|
||||
}),
|
||||
do: %{context | subpatterns: subpatterns, vars: vars, conditional_vars: conditional_vars}
|
||||
|
||||
@doc """
|
||||
Returns true if all entries have the same conditional vars.
|
||||
"""
|
||||
def all_same_conditional_vars?([{_, cond} | tail]) do
|
||||
Enum.all?(tail, fn {_, tail_cond} -> cond == tail_cond end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes the args with acc using conditional variables.
|
||||
"""
|
||||
@@ -434,48 +461,59 @@ defmodule Module.Types.Of do
|
||||
|
||||
@doc """
|
||||
Handles instantiation of a new struct.
|
||||
|
||||
This is expanded and validated by the compiler, so don't need to check the fields.
|
||||
"""
|
||||
# TODO: Type check the fields match the struct
|
||||
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
|
||||
when is_atom(struct) do
|
||||
{_info, context} = struct_info(struct, meta, stack, context)
|
||||
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
|
||||
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
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
|
||||
_ -> term()
|
||||
end
|
||||
if is_nil(info) do
|
||||
{dynamic(), context}
|
||||
else
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
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
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `__info__(:struct)` information about a struct.
|
||||
"""
|
||||
def struct_info(struct, meta, stack, context) do
|
||||
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
|
||||
case stack.no_warn_undefined do
|
||||
%Macro.Env{} = env ->
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
|
||||
{:ok, info} -> {info, context}
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
{:error, _desc} -> {nil, context}
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Fetch the signature to validate for warnings.
|
||||
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
info =
|
||||
struct.__info__(:struct) ||
|
||||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
|
||||
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and
|
||||
struct.__info__(:struct)
|
||||
|
||||
{info, context}
|
||||
if info do
|
||||
{_, context} =
|
||||
Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
{info, context}
|
||||
else
|
||||
error = {:unknown_struct, kind, struct, must_exist?}
|
||||
{nil, error(error, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -492,6 +530,14 @@ defmodule Module.Types.Of do
|
||||
closed_map(pairs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns shared error for unknown struct field.
|
||||
"""
|
||||
def unknown_struct_field(struct, field, kind, meta, stack, context) do
|
||||
error = {:unknown_struct_field, kind, struct, field}
|
||||
error(error, meta, stack, context)
|
||||
end
|
||||
|
||||
## Bitstrings
|
||||
|
||||
@doc """
|
||||
@@ -802,6 +848,37 @@ defmodule Module.Types.Of do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
|
||||
detail =
|
||||
case {Code.ensure_loaded?(module), must_exist?} do
|
||||
{true, false} ->
|
||||
"there is such module but it does not define a struct"
|
||||
|
||||
{false, false} ->
|
||||
"module #{inspect(module)} is not available or is yet to be defined"
|
||||
|
||||
{true, true} ->
|
||||
"the module may have been redefined as it no longer defines a struct"
|
||||
|
||||
{false, true} ->
|
||||
"the module was also only available but may have been removed during compilation"
|
||||
end
|
||||
|
||||
%{
|
||||
message: "struct #{inspect(module)} is undefined (#{detail})",
|
||||
group: true,
|
||||
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct_field, kind, module, field}) do
|
||||
%{
|
||||
message: "unknown key #{inspect(field)} for struct #{inspect(module)}",
|
||||
group: true,
|
||||
severity: if(kind == :pattern, do: :error, else: :warning)
|
||||
}
|
||||
end
|
||||
|
||||
defp dot_var?(expr) do
|
||||
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,19 @@ defmodule Module.Types.Traverse do
|
||||
end)
|
||||
end
|
||||
|
||||
# Structs, map update, tail operator
|
||||
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:%, :|] do
|
||||
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]}, stack, context) do
|
||||
context = of_expr(map, stack, context)
|
||||
context = of_expr(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
def of_expr({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) do
|
||||
context = of_expr(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
# Map update, tail operator
|
||||
def of_expr({:|, _meta, [left, right]}, stack, context) do
|
||||
context = of_expr(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
@@ -46,9 +57,9 @@ defmodule Module.Types.Traverse do
|
||||
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# left = right, left <_ right
|
||||
def of_expr({op, _meta, [_left, right]}, stack, context) when op in [:=, :<-] do
|
||||
# Skip the left side (pattern), only traverse right
|
||||
# left = right, left <- right
|
||||
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:=, :<-] do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
@@ -65,8 +76,9 @@ defmodule Module.Types.Traverse do
|
||||
end)
|
||||
end
|
||||
|
||||
# Treat -> as patterns for simplicity
|
||||
def of_expr({:->, _, [_head, body]}, stack, context) do
|
||||
# All non-handled -> are patterns
|
||||
def of_expr({:->, _, [head, body]}, stack, context) do
|
||||
context = of_pattern(head, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end
|
||||
|
||||
@@ -150,6 +162,45 @@ defmodule Module.Types.Traverse do
|
||||
context
|
||||
end
|
||||
|
||||
defp of_pattern({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) when is_atom(module) do
|
||||
context = of_pattern(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern({left, _meta, right}, stack, context) do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_pattern(right, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern({left, right}, stack, context) do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_pattern(right, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern([_ | _] = list, stack, context) do
|
||||
Enum.reduce(list, context, &of_pattern(&1, stack, &2))
|
||||
end
|
||||
|
||||
defp of_pattern(_, _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
defp of_struct(module, pairs, kind, meta, stack, context) do
|
||||
{info, context} = Module.Types.Of.struct_info(module, kind, meta, stack, context)
|
||||
|
||||
if info do
|
||||
Enum.reduce(pairs, context, fn {key, _value}, context ->
|
||||
if Enum.any?(info, &(&1.field == key)) do
|
||||
context
|
||||
else
|
||||
Module.Types.Of.unknown_struct_field(module, key, kind, meta, stack, context)
|
||||
end
|
||||
end)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp local_fun(meta, fun, arity, stack, context) do
|
||||
case stack.local_handler.(meta, {fun, arity}, stack, context) do
|
||||
false -> context
|
||||
|
||||
@@ -952,8 +952,14 @@ defmodule OptionParser do
|
||||
|
||||
case types |> List.delete(:keep) |> List.first(:string) do
|
||||
:boolean ->
|
||||
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
case to_switch(name) do
|
||||
"--no-" <> _ = switch ->
|
||||
add_aliases(switch, name, reverse_aliases)
|
||||
|
||||
switch ->
|
||||
base = "#{switch}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
end
|
||||
|
||||
type ->
|
||||
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
|
||||
|
||||
@@ -170,6 +170,8 @@ defmodule PartitionSupervisor do
|
||||
| {:max_seconds, non_neg_integer()}
|
||||
| {:with_arguments, (args :: [term()], partition() -> updated_args :: [term()])}
|
||||
|
||||
defguardp is_name(name) when is_atom(name) or elem(name, 0) == :via
|
||||
|
||||
@doc false
|
||||
def child_spec(opts) when is_list(opts) do
|
||||
id =
|
||||
@@ -366,7 +368,7 @@ defmodule PartitionSupervisor do
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
|
||||
def resize!(name, partitions) when is_integer(partitions) do
|
||||
def resize!(name, partitions) when is_name(name) and is_integer(partitions) do
|
||||
supervisor =
|
||||
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
|
||||
|
||||
@@ -421,8 +423,8 @@ defmodule PartitionSupervisor do
|
||||
Returns the number of partitions for the partition supervisor.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec partitions(name()) :: pos_integer()
|
||||
def partitions(name) do
|
||||
@spec partitions(name()) :: non_neg_integer()
|
||||
def partitions(name) when is_name(name) do
|
||||
name |> table() |> partitions(name)
|
||||
end
|
||||
|
||||
@@ -470,7 +472,7 @@ defmodule PartitionSupervisor do
|
||||
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
|
||||
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
|
||||
]
|
||||
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
|
||||
def which_children(name) when is_name(name) do
|
||||
Supervisor.which_children(name)
|
||||
end
|
||||
|
||||
@@ -498,7 +500,7 @@ defmodule PartitionSupervisor do
|
||||
supervisors: non_neg_integer,
|
||||
workers: non_neg_integer
|
||||
}
|
||||
def count_children(supervisor) when is_atom(supervisor) do
|
||||
def count_children(supervisor) when is_name(supervisor) do
|
||||
Supervisor.count_children(supervisor)
|
||||
end
|
||||
|
||||
@@ -514,7 +516,7 @@ defmodule PartitionSupervisor do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec stop(name(), reason :: term, timeout) :: :ok
|
||||
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_atom(supervisor) do
|
||||
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_name(supervisor) do
|
||||
Supervisor.stop(supervisor, reason, timeout)
|
||||
end
|
||||
|
||||
|
||||
@@ -485,7 +485,7 @@ defmodule Path do
|
||||
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
|
||||
case :file.get_cwd() do
|
||||
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
|
||||
_ -> path
|
||||
_ -> IO.chardata_to_string(path)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+27
-10
@@ -387,8 +387,6 @@ defmodule Process do
|
||||
automatically canceled when `dest` is an atom (as the atom resolution is done
|
||||
on delivery).
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Options
|
||||
|
||||
* `:abs` - (boolean) when `false`, `time` is treated as relative to the
|
||||
@@ -979,11 +977,35 @@ defmodule Process do
|
||||
@spec unalias(alias) :: boolean
|
||||
defdelegate unalias(alias), to: :erlang
|
||||
|
||||
@doc """
|
||||
Returns the label set for the process `pid` as set with `set_label/1`
|
||||
or `:proc_lib.set_label/1`.
|
||||
|
||||
Defaults to the current process when `pid` is not passed.
|
||||
|
||||
## Examples
|
||||
|
||||
Process.set_label({:any, "term"})
|
||||
Process.get_label()
|
||||
#=> {:any, "term"}
|
||||
|
||||
Returns `nil` when not set:
|
||||
|
||||
Process.get_label(pid)
|
||||
#=> nil
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec get_label(pid()) :: term()
|
||||
def get_label(pid \\ self()) do
|
||||
nilify(:proc_lib.get_label(pid))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add a descriptive term to the current process.
|
||||
|
||||
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
|
||||
Observer and in crash logs.
|
||||
The term does not need to be unique, and will be shown in Observer and in
|
||||
crash logs.
|
||||
This label may be useful for identifying a process as one of multiple in a
|
||||
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
|
||||
|
||||
@@ -997,12 +1019,7 @@ defmodule Process do
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec set_label(term()) :: :ok
|
||||
def set_label(label) do
|
||||
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
|
||||
Process.put(:"$process_label", label)
|
||||
# mimic return value of `:proc_lib.set_label/2`
|
||||
:ok
|
||||
end
|
||||
defdelegate set_label(label), to: :proc_lib
|
||||
|
||||
@compile {:inline, nilify: 1}
|
||||
defp nilify(:undefined), do: nil
|
||||
|
||||
+24
-20
@@ -786,7 +786,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp fallback_clause_for(value, _protocol, meta) do
|
||||
{meta, [quote(do: _)], [], value}
|
||||
{meta, [{:_, [version: -1], __MODULE__}], [], value}
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
@@ -818,11 +818,9 @@ defmodule Protocol do
|
||||
@fallback_to_any false
|
||||
@undefined_impl_description ""
|
||||
|
||||
# Invoke the user given block
|
||||
_ = unquote(block)
|
||||
|
||||
# Finalize expansion
|
||||
res = unquote(block)
|
||||
unquote(after_defprotocol())
|
||||
res
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -932,7 +930,7 @@ defmodule Protocol do
|
||||
quote bind_quoted: [built_in: built_in()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
__MODULE__.Any
|
||||
Protocol.__concat__(__MODULE__, "Any")
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -968,16 +966,6 @@ defmodule Protocol do
|
||||
built_in
|
||||
)
|
||||
|
||||
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
|
||||
# destructuring opaque types is illegal, Dialyzer will think none of the
|
||||
# previous clauses matches opaque types, and without this clause, will
|
||||
# conclude that impl_for can't handle an opaque argument). This is a hack
|
||||
# since it relies on Dialyzer not being smart enough to conclude that all
|
||||
# opaque types will get the any_impl_for/0 implementation.
|
||||
Kernel.def impl_for(_) do
|
||||
unquote(any_impl_for)
|
||||
end
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
|
||||
@@ -1021,8 +1009,22 @@ defmodule Protocol do
|
||||
)
|
||||
end
|
||||
|
||||
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
|
||||
# destructuring opaque types is illegal, Dialyzer will think none of the
|
||||
# previous clauses matches opaque types, and without this clause, will
|
||||
# conclude that impl_for can't handle an opaque argument). This is a hack
|
||||
# since it relies on Dialyzer not being smart enough to conclude that all
|
||||
# opaque types will get the any_impl_for/0 implementation.
|
||||
impl_for_fallback =
|
||||
quote generated: true, bind_quoted: [] do
|
||||
Kernel.def impl_for(_) do
|
||||
unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
quote generated: true do
|
||||
unquote(prefix)
|
||||
unquote(impl_for_fallback)
|
||||
|
||||
@doc false
|
||||
@spec impl_for!(term) :: atom
|
||||
@@ -1221,10 +1223,12 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __concat__(left, right) do
|
||||
String.to_atom(
|
||||
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
|
||||
)
|
||||
def __concat__(left, right) when is_atom(right) do
|
||||
__concat__(left, remove_prefix(Atom.to_string(right)))
|
||||
end
|
||||
|
||||
def __concat__(left, right) when is_binary(right) do
|
||||
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
|
||||
end
|
||||
|
||||
defp ensure_prefix("Elixir." <> _ = left), do: left
|
||||
|
||||
@@ -78,8 +78,7 @@ defmodule Regex do
|
||||
|
||||
* `:caseless` (i) - adds case insensitivity
|
||||
|
||||
* `:dotall` (s) - causes dot to match newlines and also sets newline to
|
||||
`(*ANYCRLF)`.\
|
||||
* `:dotall` (s) - causes dot to match newlines and also sets newline to `(*ANYCRLF)`.
|
||||
The new line setting, as described in the [`:re` documentation](`:re`),
|
||||
can be overridden by starting the regular expression pattern with:
|
||||
* `(*CR)` - carriage return
|
||||
@@ -87,7 +86,7 @@ defmodule Regex do
|
||||
* `(*CRLF)` - carriage return, followed by line feed
|
||||
* `(*ANYCRLF)` - any of the three above
|
||||
* `(*ANY)` - all Unicode newline sequences
|
||||
* _Starting from Erlang/OTP 28, `(*NUL)` - the NUL character (binary zero)_
|
||||
* `(*NUL)` - the NUL character (binary zero) (from Erlang/OTP 28)
|
||||
|
||||
* `:multiline` (m) - causes `^` and `$` to mark the beginning and end of
|
||||
each line; use `\A` and `\z` to match the end or beginning of the string
|
||||
|
||||
+191
-76
@@ -258,7 +258,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
{pid, _} ->
|
||||
if Process.alive?(pid), do: pid, else: :undefined
|
||||
|
||||
@@ -369,7 +369,10 @@ defmodule Registry do
|
||||
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
|
||||
many different keys (e.g., many topics with few subscribers each). This makes
|
||||
key-based lookups more efficient as they only need to check a single partition
|
||||
instead of all partitions.
|
||||
instead of all partitions. This option uses a different internal ETS table type
|
||||
(`ordered_set` instead of `duplicate_bag`), which means match specs passed to
|
||||
`select/2` and `count_select/2` that reference `:"$_"` may behave differently.
|
||||
Use named match variables (`:"$1"`, `:"$2"`, etc.) instead.
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@@ -532,13 +535,17 @@ defmodule Registry do
|
||||
given as an option, the dispatching happens in parallel. In both cases,
|
||||
the callback is only invoked if there are entries for that partition.
|
||||
|
||||
For `{:duplicate, :key}` registries, all entries for a given key are in a
|
||||
single partition, so the `:parallel` option has no effect.
|
||||
|
||||
See the module documentation for examples of using the `dispatch/3`
|
||||
function for building custom dispatching or a pubsub system.
|
||||
|
||||
## Options
|
||||
|
||||
* `:parallel` - if `true`, the dispatching is done in parallel
|
||||
across all partitions. Defaults to `false`.
|
||||
across all partitions. Only meaningful for `{:duplicate, :pid}`
|
||||
registries. Defaults to `false`.
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@@ -549,17 +556,24 @@ defmodule Registry do
|
||||
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
(key_ets || key_ets!(registry, key, partitions))
|
||||
|> safe_lookup_second(key)
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
:unique
|
||||
|> lookup_second(key_ets, key)
|
||||
|> List.wrap()
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
key_ets
|
||||
|> safe_lookup_second(key)
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
kind
|
||||
|> lookup_second(key_ets, key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{:duplicate, _}, partitions, _} ->
|
||||
{{:duplicate, :key}, partitions, _} ->
|
||||
{:duplicate, :key}
|
||||
|> lookup_second(key_ets!(registry, key, partitions), key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{{:duplicate, :pid}, partitions, _} ->
|
||||
if Keyword.get(opts, :parallel, false) do
|
||||
registry
|
||||
|> dispatch_parallel(key, mfa_or_fun, partitions)
|
||||
@@ -579,9 +593,8 @@ defmodule Registry do
|
||||
defp dispatch_serial(registry, key, mfa_or_fun, partition) do
|
||||
partition = partition - 1
|
||||
|
||||
registry
|
||||
|> key_ets!(partition)
|
||||
|> safe_lookup_second(key)
|
||||
{:duplicate, :pid}
|
||||
|> lookup_second(key_ets!(registry, partition), key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
dispatch_serial(registry, key, mfa_or_fun, partition)
|
||||
@@ -597,9 +610,8 @@ defmodule Registry do
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
registry
|
||||
|> key_ets!(partition)
|
||||
|> safe_lookup_second(key)
|
||||
{:duplicate, :pid}
|
||||
|> lookup_second(key_ets!(registry, partition), key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
Process.unlink(parent)
|
||||
@@ -663,7 +675,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
{_, _} = pair ->
|
||||
[pair]
|
||||
|
||||
@@ -671,16 +683,15 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
safe_lookup_second(key_ets, key)
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
lookup_second(kind, key_ets, key)
|
||||
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
partition = hash(key, partitions)
|
||||
safe_lookup_second(key_ets!(registry, partition), key)
|
||||
lookup_second({:duplicate, :key}, key_ets!(registry, key, partitions), key)
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
for partition <- 0..(partitions - 1),
|
||||
pair <- safe_lookup_second(key_ets!(registry, partition), key),
|
||||
pair <- lookup_second({:duplicate, :pid}, key_ets!(registry, partition), key),
|
||||
do: pair
|
||||
end
|
||||
end
|
||||
@@ -791,21 +802,18 @@ defmodule Registry do
|
||||
@doc since: "1.4.0"
|
||||
@spec match(registry, key, match_pattern, guards) :: [{pid, term}]
|
||||
def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select(key_ets, spec)
|
||||
{{:duplicate, :pid}, partitions, nil} ->
|
||||
spec = match_spec({:duplicate, :pid}, key, pattern, guards)
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
:ets.select(key_ets, spec)
|
||||
|
||||
{{:duplicate, _}, partitions, _key_ets} ->
|
||||
for partition <- 0..(partitions - 1),
|
||||
pair <- :ets.select(key_ets!(registry, partition), spec),
|
||||
do: pair
|
||||
|
||||
{kind, partitions, key_ets} ->
|
||||
spec = match_spec(kind, key, pattern, guards)
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select(key_ets, spec)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -943,7 +951,7 @@ defmodule Registry do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
{^pid, value} ->
|
||||
[value]
|
||||
|
||||
@@ -951,18 +959,17 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
{{:duplicate, _} = kind, 1, key_ets} ->
|
||||
for {^pid, value} <- lookup_second(kind, key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
partition = hash(key, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
key_ets = key_ets!(registry, key, partitions)
|
||||
for {^pid, value} <- lookup_second({:duplicate, :key}, key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
partition = hash(pid, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
for {^pid, value} <- lookup_second({:duplicate, :pid}, key_ets, key), do: value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1016,7 +1023,8 @@ defmodule Registry do
|
||||
# Remove first from the key_ets because in case of crashes
|
||||
# the pid_ets will still be able to clean up. The last step is
|
||||
# to clean if we have no more entries.
|
||||
true = __unregister__(key_ets, {key, {self, :_}}, 1)
|
||||
true = __unregister__(key_ets, {key, {self, :_}}, 1, ordered?(kind))
|
||||
|
||||
true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2)
|
||||
|
||||
unlink_if_unregistered(pid_server, pid_ets, self)
|
||||
@@ -1081,13 +1089,10 @@ defmodule Registry do
|
||||
# to clean if we have no more entries.
|
||||
|
||||
# Here we want to count all entries for this pid under this key, regardless of pattern.
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
|
||||
{total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards)
|
||||
total = :ets.select_count(key_ets, total_spec)
|
||||
|
||||
# We only want to delete things that match the pattern
|
||||
delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]
|
||||
|
||||
case :ets.select_delete(key_ets, delete_spec) do
|
||||
# We deleted everything, we can just delete the object
|
||||
^total ->
|
||||
@@ -1121,6 +1126,25 @@ defmodule Registry do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
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}}
|
||||
|
||||
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
|
||||
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers the current process under the given `key` in `registry`.
|
||||
|
||||
@@ -1180,7 +1204,14 @@ defmodule Registry do
|
||||
counter = System.unique_integer()
|
||||
true = :ets.insert(pid_ets, {self, key, key_ets, counter})
|
||||
|
||||
case register_key(kind, key_ets, key, {key, {self, value}}) do
|
||||
key_entry =
|
||||
if ordered?(kind) do
|
||||
{{key, self, counter}, value}
|
||||
else
|
||||
{key, {self, value}}
|
||||
end
|
||||
|
||||
case register_key(kind, key_ets, key, key_entry) do
|
||||
:ok ->
|
||||
for listener <- listeners do
|
||||
Kernel.send(listener, {:register, registry, key, self, value})
|
||||
@@ -1409,21 +1440,18 @@ defmodule Registry do
|
||||
@spec count_match(registry, key, match_pattern, guards) :: non_neg_integer()
|
||||
def count_match(registry, key, pattern, guards \\ [])
|
||||
when is_atom(registry) and is_list(guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
spec = [{{:_, {:_, pattern}}, guards, [true]}]
|
||||
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select_count(key_ets, spec)
|
||||
{{:duplicate, :pid}, partitions, nil} ->
|
||||
spec = count_match_spec({:duplicate, :pid}, key, pattern, guards)
|
||||
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
:ets.select_count(key_ets, spec)
|
||||
|
||||
{{:duplicate, _}, partitions, _key_ets} ->
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{kind, partitions, key_ets} ->
|
||||
spec = count_match_spec(kind, key, pattern, guards)
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select_count(key_ets, spec)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1452,6 +1480,9 @@ defmodule Registry do
|
||||
some operations like `:element` to modify the output format.
|
||||
|
||||
Do not use special match variables `:"$_"` and `:"$$"`, because they might not work as expected.
|
||||
In particular, `{:duplicate, :key}` registries use a different internal ETS layout, so match specs
|
||||
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.
|
||||
@@ -1479,15 +1510,16 @@ defmodule Registry do
|
||||
@spec select(registry, spec) :: [term]
|
||||
def select(registry, spec)
|
||||
when is_atom(registry) and is_list(spec) do
|
||||
spec = group_match_headers(spec, __ENV__.function)
|
||||
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
{kind, partitions, nil} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
|
||||
Enum.flat_map(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{_kind, 1, key_ets} ->
|
||||
{kind, 1, key_ets} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
:ets.select(key_ets, spec)
|
||||
end
|
||||
end
|
||||
@@ -1510,24 +1542,30 @@ defmodule Registry do
|
||||
@spec count_select(registry, spec) :: non_neg_integer()
|
||||
def count_select(registry, spec)
|
||||
when is_atom(registry) and is_list(spec) do
|
||||
spec = group_match_headers(spec, __ENV__.function)
|
||||
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
{kind, partitions, nil} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{_kind, 1, key_ets} ->
|
||||
{kind, 1, key_ets} ->
|
||||
spec = group_match_headers(spec, kind, __ENV__.function)
|
||||
:ets.select_count(key_ets, spec)
|
||||
end
|
||||
end
|
||||
|
||||
defp group_match_headers(spec, {fun, arity}) do
|
||||
defp group_match_headers(spec, kind, {fun, arity}) do
|
||||
ordered = ordered?(kind)
|
||||
|
||||
for part <- spec do
|
||||
case part do
|
||||
{{key, pid, value}, guards, select} ->
|
||||
{{key, {pid, value}}, guards, select}
|
||||
{{key, pid, value}, guards, body} when ordered ->
|
||||
{{{key, pid, :_}, value}, guards, body}
|
||||
|
||||
{{key, pid, value}, guards, body} ->
|
||||
{{key, {pid, value}}, guards, body}
|
||||
|
||||
_ ->
|
||||
raise ArgumentError,
|
||||
@@ -1578,7 +1616,23 @@ defmodule Registry do
|
||||
:ets.lookup_element(registry, partition, 3)
|
||||
end
|
||||
|
||||
defp safe_lookup_second(ets, key) do
|
||||
defp lookup_second({:duplicate, :key}, ets, key) do
|
||||
spec =
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
|
||||
else
|
||||
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
|
||||
end
|
||||
|
||||
try do
|
||||
:ets.select(ets, spec)
|
||||
catch
|
||||
:error, :badarg -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp lookup_second(_kind, ets, key) do
|
||||
try do
|
||||
:ets.lookup_element(ets, key, 2)
|
||||
catch
|
||||
@@ -1607,7 +1661,19 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __unregister__(table, match, pos) 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
|
||||
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.
|
||||
@@ -1620,9 +1686,52 @@ defmodule Registry do
|
||||
end
|
||||
end
|
||||
|
||||
# For {:duplicate, :key} registries, the ETS entry layout is
|
||||
# {{key, pid, counter}, value} (ordered_set with composite key),
|
||||
# so :"$_" refers to that whole tuple. The body extracts {pid, value}
|
||||
# to match the public API's return shape of [{pid, value}].
|
||||
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
|
||||
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, :"$_"}]}]
|
||||
end
|
||||
|
||||
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
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]}]
|
||||
end
|
||||
|
||||
defp reserved_atom?("_"), do: true
|
||||
defp reserved_atom?("$" <> _), do: true
|
||||
defp reserved_atom?(_), do: false
|
||||
|
||||
defp ordered?({:duplicate, :key}), do: true
|
||||
defp ordered?(_), do: false
|
||||
end
|
||||
|
||||
defmodule Registry.Supervisor do
|
||||
@@ -1734,7 +1843,8 @@ defmodule Registry.Partition do
|
||||
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
|
||||
end
|
||||
|
||||
{:ok, {pid_ets, %{}}}
|
||||
ordered = kind == {:duplicate, :key}
|
||||
{:ok, {pid_ets, %{}, ordered}}
|
||||
end
|
||||
|
||||
# The key partition is a set for unique keys,
|
||||
@@ -1744,7 +1854,12 @@ defmodule Registry.Partition do
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
|
||||
defp init_key_ets({:duplicate, :key}, key_partition, compressed) do
|
||||
opts = [:ordered_set, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
defp init_key_ets({:duplicate, :pid}, key_partition, compressed) do
|
||||
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
@@ -1768,7 +1883,7 @@ defmodule Registry.Partition do
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:lock, key}, from, {ets, lock}) do
|
||||
def handle_call({:lock, key}, from, {ets, lock, ordered}) do
|
||||
lock =
|
||||
case lock do
|
||||
%{^key => queue} ->
|
||||
@@ -1779,10 +1894,10 @@ defmodule Registry.Partition do
|
||||
Map.put(lock, key, :queue.new())
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
|
||||
def handle_info({:EXIT, pid, _reason}, {ets, lock, ordered}) do
|
||||
entries = :ets.take(ets, pid)
|
||||
|
||||
for {_pid, key, key_ets, _counter} <- entries do
|
||||
@@ -1797,13 +1912,13 @@ defmodule Registry.Partition do
|
||||
end
|
||||
|
||||
try do
|
||||
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1)
|
||||
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1, ordered)
|
||||
catch
|
||||
:error, :badarg -> :badarg
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
end
|
||||
|
||||
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
|
||||
@@ -1815,7 +1930,7 @@ defmodule Registry.Partition do
|
||||
unlock(key, state)
|
||||
end
|
||||
|
||||
defp unlock(key, {ets, lock}) do
|
||||
defp unlock(key, {ets, lock, ordered}) do
|
||||
%{^key => queue} = lock
|
||||
|
||||
lock =
|
||||
@@ -1824,7 +1939,7 @@ defmodule Registry.Partition do
|
||||
{:not_empty, queue} -> Map.put(lock, key, queue)
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
{:noreply, {ets, lock, ordered}}
|
||||
end
|
||||
|
||||
defp dequeue(queue, key) do
|
||||
|
||||
@@ -1437,7 +1437,7 @@ defmodule Stream do
|
||||
do_cycle(cycle, [], cycle, fun.(element, acc), fun)
|
||||
|
||||
{_, []} ->
|
||||
do_cycle(cycle, [], cycle, {:cont, acc}, fun)
|
||||
do_cycle(check_cycle_subsequent_element(cycle), [], cycle, {:cont, acc}, fun)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1446,10 +1446,26 @@ defmodule Stream do
|
||||
end
|
||||
|
||||
defp check_cycle_first_element(reduce) do
|
||||
check_cycle_non_empty(
|
||||
reduce,
|
||||
ArgumentError,
|
||||
"cannot cycle over an empty enumerable"
|
||||
)
|
||||
end
|
||||
|
||||
defp check_cycle_subsequent_element(reduce) do
|
||||
check_cycle_non_empty(
|
||||
reduce,
|
||||
RuntimeError,
|
||||
"cycled enumerable became empty after a previous iteration produced elements"
|
||||
)
|
||||
end
|
||||
|
||||
defp check_cycle_non_empty(reduce, exception, message) do
|
||||
fn acc ->
|
||||
case reduce.(acc) do
|
||||
{state, []} when state in [:done, :halted] and elem(acc, 0) != :halt ->
|
||||
raise ArgumentError, "cannot cycle over an empty enumerable"
|
||||
raise exception, message
|
||||
|
||||
other ->
|
||||
other
|
||||
|
||||
+62
-10
@@ -745,7 +745,15 @@ defmodule String do
|
||||
iex> String.count("hello world", "")
|
||||
12
|
||||
|
||||
The `pattern` can also be a compiled pattern:
|
||||
The `pattern` can also be a list:
|
||||
|
||||
iex> String.count("hello world", ["e", "o"])
|
||||
3
|
||||
|
||||
iex> String.count("hello world", [])
|
||||
0
|
||||
|
||||
Or a compiled pattern:
|
||||
|
||||
iex> pattern = :binary.compile_pattern([" ", "!"])
|
||||
iex> String.count("foo bar baz!!", pattern)
|
||||
@@ -756,6 +764,8 @@ defmodule String do
|
||||
@doc since: "1.19.0"
|
||||
def count(string, <<>>), do: length(string) + 1
|
||||
|
||||
def count(_string, []), do: 0
|
||||
|
||||
def count(string, pattern) when is_struct(pattern, Regex) do
|
||||
Kernel.length(Regex.scan(pattern, string, return: :index))
|
||||
end
|
||||
@@ -998,6 +1008,10 @@ defmodule String do
|
||||
IO.iodata_to_binary(downcase_ascii(string))
|
||||
end
|
||||
|
||||
def downcase(string, :greek) when is_binary(string) do
|
||||
String.Unicode.downcase_greek(string, [])
|
||||
end
|
||||
|
||||
def downcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
|
||||
String.Unicode.downcase(string, [], mode)
|
||||
end
|
||||
@@ -1048,19 +1062,39 @@ defmodule String do
|
||||
@letter_I_dot_above <<0x0130::utf8>>
|
||||
|
||||
def capitalize(<<@letter_i, right::binary>>, mode) do
|
||||
if(mode == :turkic, do: @letter_I_dot_above, else: @letter_I) <> downcase(right, mode)
|
||||
capitalized = if mode == :turkic, do: @letter_I_dot_above, else: @letter_I
|
||||
capitalized <> downcase_after_capitalize(right, mode, ?i)
|
||||
end
|
||||
|
||||
def capitalize(string, mode) when is_binary(string) do
|
||||
case :unicode_util.gc(string) do
|
||||
[gc] -> grapheme_to_binary(:string.titlecase([gc]))
|
||||
[gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[] -> ""
|
||||
{:error, <<byte, rest::bits>>} -> <<byte>> <> downcase(rest, mode)
|
||||
[gc] ->
|
||||
grapheme_to_binary(:string.titlecase([gc]))
|
||||
|
||||
[gc, rest] ->
|
||||
grapheme_to_binary(:string.titlecase([gc])) <>
|
||||
downcase_after_capitalize(rest, mode, gc)
|
||||
|
||||
[gc | rest] ->
|
||||
grapheme_to_binary(:string.titlecase([gc])) <>
|
||||
downcase_after_capitalize(rest, mode, gc)
|
||||
|
||||
[] ->
|
||||
""
|
||||
|
||||
{:error, <<byte, rest::bits>>} ->
|
||||
<<byte>> <> downcase(rest, mode)
|
||||
end
|
||||
end
|
||||
|
||||
defp downcase_after_capitalize(rest, :greek, gc) do
|
||||
String.Unicode.downcase_greek(rest, [], grapheme_to_binary(gc))
|
||||
end
|
||||
|
||||
defp downcase_after_capitalize(rest, mode, _gc) do
|
||||
downcase(rest, mode)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.trim_trailing/1 instead"
|
||||
defdelegate rstrip(binary), to: String.Break, as: :trim_trailing
|
||||
@@ -2290,6 +2324,19 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
# 56-bit SWAR guard: all 7 bytes are ASCII (< 128) and none is \r (0x0D).
|
||||
# Uses Mycroft's zero-byte detection: XOR with 0x0D and check for zero bytes.
|
||||
defguardp ascii_no_cr_swar?(w)
|
||||
when Bitwise.band(w, 0x80808080808080) == 0 and
|
||||
Bitwise.band(
|
||||
Bitwise.bxor(w, 0x0D0D0D0D0D0D0D) - 0x01010101010101,
|
||||
0x80808080808080
|
||||
) == 0
|
||||
|
||||
defp skip_length(<<w::56, b, rest::binary>>, acc)
|
||||
when b <= 127 and b != ?\r and ascii_no_cr_swar?(w),
|
||||
do: skip_length(rest, acc + 8)
|
||||
|
||||
defp skip_length(<<byte, rest::binary>>, acc)
|
||||
when byte <= 127 and byte != ?\r,
|
||||
do: skip_length(rest, acc + 1)
|
||||
@@ -3168,8 +3215,7 @@ defmodule String do
|
||||
def jaro_distance("", _string), do: 0.0
|
||||
|
||||
def jaro_distance(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
# TODO: Replace by :string.jaro_similarity/2 when we require Erlang/OTP 27+
|
||||
:elixir_utils.jaro_similarity(string1, string2)
|
||||
:string.jaro_similarity(string1, string2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -3214,7 +3260,13 @@ defmodule String do
|
||||
byte_size_unicode(unicode)
|
||||
end
|
||||
|
||||
defp byte_size_remaining_at(unicode, n) do
|
||||
defp byte_size_remaining_at(<<byte1, byte2, rest::binary>> = binary, n)
|
||||
when n > 0 and byte1 <= 127 and byte1 != ?\r and byte2 <= 127 and byte2 != ?\r do
|
||||
skip = min(skip_length(rest, 1), n)
|
||||
byte_size_remaining_at(binary_part(binary, skip, byte_size(binary) - skip), n - skip)
|
||||
end
|
||||
|
||||
defp byte_size_remaining_at(unicode, n) when n > 0 do
|
||||
case :unicode_util.gc(unicode) do
|
||||
[_] -> 0
|
||||
[_ | rest] -> byte_size_remaining_at(rest, n - 1)
|
||||
|
||||
@@ -556,19 +556,29 @@ defmodule Supervisor do
|
||||
|
||||
Developers typically invoke `Supervisor.init/2` at the end of their
|
||||
init callback to return the proper supervision flags.
|
||||
|
||||
See `start_link/2` for more information.
|
||||
"""
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok,
|
||||
{sup_flags(), [child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
|
||||
| :ignore
|
||||
|
||||
@typedoc "Return values of `start_link/2` and `start_link/3`."
|
||||
@typedoc """
|
||||
Return values of `start_link/2` and `start_link/3`.
|
||||
|
||||
See the documentation for those functions for more information.
|
||||
"""
|
||||
@type on_start ::
|
||||
{:ok, pid}
|
||||
| :ignore
|
||||
| {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
|
||||
@typedoc "Return values of `start_child/2`."
|
||||
@typedoc """
|
||||
Return values of `start_child/2`.
|
||||
|
||||
See the documentation for those functions for more information.
|
||||
"""
|
||||
@type on_start_child ::
|
||||
{:ok, child}
|
||||
| {:ok, child, info :: term}
|
||||
@@ -698,15 +708,18 @@ defmodule Supervisor do
|
||||
If the supervisor and all child processes are successfully spawned
|
||||
(if the start function of each child process returns `{:ok, child}`,
|
||||
`{:ok, child, info}`, or `:ignore`), this function returns
|
||||
`{:ok, pid}`, where `pid` is the PID of the supervisor. If the supervisor
|
||||
is given a name and a process with the specified name already exists,
|
||||
the function returns `{:error, {:already_started, pid}}`, where `pid`
|
||||
is the PID of that process.
|
||||
`{:ok, pid}`, where `pid` is the PID of the supervisor.
|
||||
|
||||
If the start function of any of the child processes fails or returns an error
|
||||
tuple or an erroneous value, the supervisor first terminates with reason
|
||||
`:shutdown` all the child processes that have already been started, and then
|
||||
terminates itself and returns `{:error, {:shutdown, reason}}`.
|
||||
If the supervisor is given a name and a process with the specified name
|
||||
already exists, the function returns `{:error, {:already_started, pid}}`,
|
||||
where `pid` is the PID of that process.
|
||||
|
||||
If the start function of any of the child processes fails or returns
|
||||
an error tuple or an erroneous value, the supervisor first terminates
|
||||
with reason `:shutdown` all the child processes that have already been
|
||||
started, and then terminates itself. If the caller process is trapping
|
||||
exits, this function then returns
|
||||
`{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
|
||||
|
||||
Note that a supervisor started with this function is linked to the parent
|
||||
process and exits not only on crashes but also if the parent process exits
|
||||
@@ -939,9 +952,17 @@ defmodule Supervisor do
|
||||
|
||||
If the `c:init/1` callback returns `:ignore`, this function returns
|
||||
`:ignore` as well and the supervisor terminates with reason `:normal`.
|
||||
If it fails or returns an incorrect value, this function returns
|
||||
`{:error, term}` where `term` is a term with information about the
|
||||
error, and the supervisor terminates with reason `term`.
|
||||
|
||||
If the `c:init/1` callback fails or returns an incorrect value, the supervisor
|
||||
will shutdown with reason `term`, where `term` contains information
|
||||
about the error. If the caller process is trapping exits, this function then
|
||||
returns `{:error, term()}`.
|
||||
|
||||
If the start function of any of the child processes returned by `c:init/1`
|
||||
fail or return an error tuple or an erroneous value, the supervisor first
|
||||
terminates with reason `:shutdown` all the child processes that have already
|
||||
been started, and then terminates itself. If the caller process is trapping
|
||||
exits, this function then returns `{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
|
||||
|
||||
The `:name` option can also be given in order to register a supervisor
|
||||
name, the supported values are described in the "Name registration"
|
||||
|
||||
@@ -105,7 +105,7 @@ defmodule Task do
|
||||
], strategy: :one_for_one)
|
||||
|
||||
And now you can use async/await by passing the name of
|
||||
the supervisor instead of the pid:
|
||||
the supervisor instead of the PID:
|
||||
|
||||
Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
|
||||
# Do something
|
||||
@@ -1248,9 +1248,10 @@ defmodule Task do
|
||||
{ref, nil}
|
||||
end)
|
||||
|
||||
ref_count = map_size(refs)
|
||||
on_timeout = Keyword.get(opts, :on_timeout, :nothing)
|
||||
timeout = Keyword.get(opts, :timeout, 5_000)
|
||||
limit = Keyword.get(opts, :limit, map_size(refs))
|
||||
limit = min(Keyword.get(opts, :limit, ref_count), ref_count)
|
||||
timeout_ref = make_ref()
|
||||
|
||||
timer_ref =
|
||||
|
||||
@@ -122,8 +122,8 @@ defmodule Task.Supervised do
|
||||
function: fun,
|
||||
args: args,
|
||||
reason: {log_value(kind, value), __STACKTRACE__},
|
||||
# TODO use Process.get_label/0 when we require Erlang/OTP 27+
|
||||
process_label: Process.get(:"$process_label", :undefined)
|
||||
# using :proc_lib over Process because we want the :undefined default, not nil
|
||||
process_label: :proc_lib.get_label(self())
|
||||
}
|
||||
},
|
||||
%{
|
||||
|
||||
+41
-9
@@ -24,6 +24,12 @@ defmodule URI do
|
||||
|
||||
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
|
||||
|
||||
The fields contain the encoded URI components as they appear in the URI
|
||||
itself. For example, a slash inside the userinfo must be stored as `%2F`,
|
||||
not as `/`. Functions such as `parse/1` and `new/1` preserve existing
|
||||
percent-encoded sequences in those fields, and functions such as `to_string/1`
|
||||
expects those fields to already be encoded as needed. Whenever setting or
|
||||
modifying the fields directly, you must encode them accordingly.
|
||||
|
||||
Note the `authority` field is deprecated. `parse/1` will still
|
||||
populate it for backwards compatibility but you should generally
|
||||
@@ -520,6 +526,16 @@ defmodule URI do
|
||||
for the URI's scheme is used for the `:port` field. The scheme is also
|
||||
normalized to lowercase.
|
||||
|
||||
> #### Browser compatibility {: .warning}
|
||||
>
|
||||
> This function does not follow the same parsing rules as browsers.
|
||||
> Browsers adhere to the WHATWG URL standard, while this function
|
||||
> implements RFC 3986. You should expect different behaviours between
|
||||
> them and this function will often reject URLs that are accepted by
|
||||
> browsers (as they tend to be permissive). Common examples include
|
||||
> unencoded square brackets in query strings (such as `foo[bar]=baz`)
|
||||
> and backslashes used as path separators.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> URI.new("https://elixir-lang.org/")
|
||||
@@ -693,18 +709,26 @@ defmodule URI do
|
||||
|
||||
This function can parse both absolute and relative URLs. You can check
|
||||
if a URI is absolute or relative by checking if the `scheme` field is
|
||||
nil or not. Furthermore, this function expects both absolute and
|
||||
relative URIs to be well-formed and does not perform any validation.
|
||||
See the "Examples" section below. Use `new/1` if you want to validate
|
||||
the URI fields after parsing.
|
||||
nil or not.
|
||||
|
||||
This function expects both absolute and relative URIs to be well-formed
|
||||
and does not perform any validation, it simply breaks the URL into parts.
|
||||
Use `new/1` if you want to validate the URI fields after parsing.
|
||||
|
||||
When a URI is given without a port, the value returned by `URI.default_port/1`
|
||||
for the URI's scheme is used for the `:port` field. The scheme is also
|
||||
normalized to lowercase.
|
||||
|
||||
If a `%URI{}` struct is given to this function, this function returns it
|
||||
If a `URI` struct is given to this function, this function returns it
|
||||
unmodified.
|
||||
|
||||
> #### Browser compatibility {: .warning}
|
||||
>
|
||||
> This function does not follow the same parsing rules as browsers.
|
||||
> Browsers adhere to the WHATWG URL standard, while this function
|
||||
> implements RFC 3986. You should expect different behaviours between
|
||||
> them, especially around corner cases.
|
||||
|
||||
> #### `:authority` field {: .info}
|
||||
>
|
||||
> This function sets the deprecated field `:authority` for backwards-compatibility reasons.
|
||||
@@ -757,8 +781,9 @@ defmodule URI do
|
||||
userinfo: nil
|
||||
}
|
||||
|
||||
In contrast to `URI.new/1`, this function will parse poorly-formed
|
||||
URIs, for example:
|
||||
In contrast to `URI.new/1`, this function will accept poorly-formed
|
||||
URIs and break them into components, as it does not perform validation.
|
||||
For example:
|
||||
|
||||
iex> URI.parse("/invalid_greater_than_in_path/>")
|
||||
%URI{
|
||||
@@ -772,8 +797,8 @@ defmodule URI do
|
||||
}
|
||||
|
||||
Another example is a URI with brackets in query strings. It is accepted
|
||||
by `parse/1`, it is commonly accepted by browsers, but it will be refused
|
||||
by `new/1`:
|
||||
by `parse/1` for the same reasons as above (no validation), but it will
|
||||
be refused by `new/1`:
|
||||
|
||||
iex> URI.parse("/?foo[bar]=baz")
|
||||
%URI{
|
||||
@@ -786,6 +811,9 @@ defmodule URI do
|
||||
userinfo: nil
|
||||
}
|
||||
|
||||
Generally speaking, square brackets are strictly invalid even in the WHATWG
|
||||
URL standard used by browsers, but browsers tend to be permissive and often
|
||||
allow invalid characters to pass through.
|
||||
"""
|
||||
@spec parse(t | binary) :: t
|
||||
def parse(%URI{} = uri), do: uri
|
||||
@@ -875,6 +903,9 @@ defmodule URI do
|
||||
@doc """
|
||||
Returns the string representation of the given [URI struct](`t:t/0`).
|
||||
|
||||
This function assembles the URI components into a string, assuming each
|
||||
field is valid and escaped as done by `parse/1` and `new/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> uri = URI.parse("http://google.com")
|
||||
@@ -942,6 +973,7 @@ defmodule URI do
|
||||
end
|
||||
|
||||
defp merge_paths(nil, rel_path), do: merge_paths("/", rel_path)
|
||||
defp merge_paths("", rel_path), do: merge_paths("/", rel_path)
|
||||
defp merge_paths(_, "/" <> _ = rel_path), do: remove_dot_segments_from_path(rel_path)
|
||||
|
||||
defp merge_paths(base_path, rel_path) do
|
||||
|
||||
@@ -18,12 +18,16 @@ defmodule Version do
|
||||
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
Each numeric component is limited to at most 14 digits.
|
||||
|
||||
Pre-releases are supported by optionally appending a hyphen and a series of
|
||||
period-separated identifiers immediately following the patch version.
|
||||
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
|
||||
|
||||
"1.0.0-alpha.3"
|
||||
|
||||
Numeric pre-release identifiers are also limited to at most 14 digits.
|
||||
|
||||
Build information can be added by appending a plus sign and a series of
|
||||
dot-separated identifiers immediately following the patch or pre-release version.
|
||||
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
|
||||
@@ -520,6 +524,8 @@ defmodule Version do
|
||||
defmodule Parser do
|
||||
@moduledoc false
|
||||
|
||||
@max_numeric_component_digits 14
|
||||
|
||||
operators = [
|
||||
{">=", :>=},
|
||||
{"<=", :<=},
|
||||
@@ -621,7 +627,9 @@ defmodule Version do
|
||||
defp require_digits(nil), do: :error
|
||||
|
||||
defp require_digits(string) do
|
||||
if leading_zero?(string), do: :error, else: parse_digits(string, "")
|
||||
if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits,
|
||||
do: :error,
|
||||
else: parse_digits(string, "")
|
||||
end
|
||||
|
||||
defp leading_zero?(<<?0, _, _::binary>>), do: true
|
||||
@@ -649,6 +657,11 @@ defmodule Version do
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_parts_to_integer([part | rest], acc)
|
||||
when byte_size(part) > @max_numeric_component_digits do
|
||||
if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc])
|
||||
end
|
||||
|
||||
defp convert_parts_to_integer([part | rest], acc) do
|
||||
case parse_digits(part, "") do
|
||||
{:ok, integer} ->
|
||||
@@ -667,6 +680,10 @@ defmodule Version do
|
||||
{:ok, Enum.reverse(acc)}
|
||||
end
|
||||
|
||||
defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest)
|
||||
defp all_digits?(<<>>), do: true
|
||||
defp all_digits?(_other), do: false
|
||||
|
||||
defp valid_identifier?(<<char, rest::binary>>)
|
||||
when char in ?0..?9
|
||||
when char in ?a..?z
|
||||
|
||||
@@ -75,7 +75,7 @@ iex> trunc(3.58)
|
||||
3
|
||||
```
|
||||
|
||||
Finally, we work with different data types, we will learn Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
|
||||
Finally, as we work with different data types, we will learn that Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
|
||||
|
||||
```elixir
|
||||
iex> is_integer(1)
|
||||
@@ -310,4 +310,4 @@ false
|
||||
|
||||
The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](`Kernel#module-structural-comparison`).
|
||||
|
||||
Elixir also provides data-types for expressing collections, such as lists and tuples, which we learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come on later chapters. Let's move forward.
|
||||
Elixir also provides data-types for expressing collections, such as lists and tuples, which we'll learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come in later chapters. Let's move forward.
|
||||
|
||||
@@ -160,8 +160,6 @@ iex> :observer.start()
|
||||
> When running `iex` inside a project with `iex -S mix`, `observer` won't be available as a dependency. To do so, you will need to call the following functions before:
|
||||
>
|
||||
> ```elixir
|
||||
> iex> Mix.ensure_application!(:wx) # Not necessary on Erlang/OTP 27+
|
||||
> iex> Mix.ensure_application!(:runtime_tools) # Not necessary on Erlang/OTP 27+
|
||||
> iex> Mix.ensure_application!(:observer)
|
||||
> iex> :observer.start()
|
||||
> ```
|
||||
|
||||
@@ -110,4 +110,4 @@ The example above will fetch the first 10 lines of the file you have selected. T
|
||||
|
||||
The `Enum` and `Stream` modules provide a wide range of functions, but you don't have to know all of them by heart. Familiarize yourself with `Enum.map/2`, `Enum.reduce/3` and other functions with either `map` or `reduce` in their names, and you will naturally build an intuition around the most important use cases. You may also focus on the `Enum` module first and only move to `Stream` for the particular scenarios where laziness is required, to either deal with slow resources or large, possibly infinite, collections.
|
||||
|
||||
Next, we'll look at a feature central to Elixir, Processes, which allows us to write concurrent, parallel and distributed programs in an easy and understandable way.
|
||||
Next, we will look at an alternate approach of operating on collections: Comprehensions.
|
||||
|
||||
@@ -15,8 +15,8 @@ Let's get started.
|
||||
|
||||
If you haven't yet installed Elixir, visit our [installation page](https://elixir-lang.org/install.html). Once you are done, you can run `elixir --version` to get the current Elixir version. The requirements for this guide are:
|
||||
|
||||
* Elixir 1.15.0 onwards
|
||||
* Erlang/OTP 26 onwards
|
||||
* Elixir 1.18.0 onwards
|
||||
* Erlang/OTP 27 onwards
|
||||
|
||||
If you are looking for other resources for learning Elixir, you can also consult the [learning page](https://elixir-lang.org/learning.html) of the official website.
|
||||
|
||||
|
||||
@@ -206,3 +206,5 @@ We packed a lot into this small section, so let's break it down:
|
||||
* the choice between iodata and chardata depends on the encoding of the IO device. If the file is opened without encoding, the file expects iodata, and the functions in the `IO` module starting with `bin*` must be used. The default IO device (`:stdio`) and files opened with `:utf8` encoding expect chardata and work with the remaining functions in the `IO` module;
|
||||
|
||||
This finishes our tour of IO devices and IO related functionality. We have learned about three Elixir modules - `IO`, `File`, and `Path` - as well as how the VM uses processes for the underlying IO mechanisms and how to use `chardata` and `iodata` for IO operations.
|
||||
|
||||
Next we will talk about a very important subject to Elixir developers: writing documentation.
|
||||
|
||||
@@ -151,6 +151,7 @@ defmodule Concat do
|
||||
# A function head declaring defaults
|
||||
def join(a, b, sep \\ " ")
|
||||
|
||||
# The separator is unused in this case, so we prefix it with underscore
|
||||
def join(a, b, _sep) when b == "" do
|
||||
a
|
||||
end
|
||||
@@ -165,7 +166,7 @@ IO.puts(Concat.join("Hello", "world")) #=> Hello world
|
||||
IO.puts(Concat.join("Hello", "world", "_")) #=> Hello_world
|
||||
```
|
||||
|
||||
When a variable is not used by a function or a clause, we add a leading underscore (`_`) to its name to signal this intent. This rule is also covered in our [Naming Conventions](../references/naming-conventions.md#underscore-_foo) document.
|
||||
Function heads cannot have patterns nor guards. They may only define the argument names and their default values.
|
||||
|
||||
## Understanding Aliases
|
||||
|
||||
@@ -182,8 +183,7 @@ true
|
||||
|
||||
By using the `alias/2` directive, we are changing the atom the alias expands to.
|
||||
|
||||
Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms. By namespacing
|
||||
those atoms elixir modules avoid conflicting with existing erlang modules.
|
||||
Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms. By namespacing those atoms, Elixir modules avoid conflicting with existing Erlang modules.
|
||||
|
||||
```elixir
|
||||
iex> List.flatten([1, [2], 3])
|
||||
@@ -212,9 +212,11 @@ end
|
||||
|
||||
The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
|
||||
|
||||
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above.
|
||||
> #### Module names are isolated {: .info}
|
||||
>
|
||||
> You don't have to define the `Foo` module before defining the `Foo.Bar` module, as they are effectively independent.
|
||||
|
||||
**Note**: in Elixir, you don't have to define the `Foo` module before being able to define the `Foo.Bar` module, as they are effectively independent. The above could also be written as:
|
||||
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive, which we will learn in the next chapter:
|
||||
|
||||
```elixir
|
||||
defmodule Foo.Bar do
|
||||
@@ -225,20 +227,3 @@ defmodule Foo do
|
||||
# Can still access it as `Bar`
|
||||
end
|
||||
```
|
||||
|
||||
Aliasing a nested module does not bring parent modules into scope. Consider the following example:
|
||||
|
||||
```elixir
|
||||
defmodule Foo do
|
||||
defmodule Bar do
|
||||
defmodule Baz do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
alias Foo.Bar.Baz
|
||||
# The module `Foo.Bar.Baz` is now available as `Baz`
|
||||
# However, the module `Foo.Bar` is *not* available as `Bar`
|
||||
```
|
||||
|
||||
As we will see in later chapters, aliases also play a crucial role in macros, to guarantee they are hygienic.
|
||||
|
||||
@@ -91,7 +91,7 @@ iex> ~w(foo bar bat)a
|
||||
|
||||
## Interpolation and escaping in textual sigils
|
||||
|
||||
Elixir supports some sigil variants to deal with escaping characters and interpolation. In particular, uppercase-letter textual sigils do not perform interpolation nor escaping. For example, although both `~s` and `~S` will return strings, the former allows escape codes and interpolation while the latter does not:
|
||||
Sigils also help deal with escaping characters and interpolation. In particular, uppercase-letter textual sigils do not perform interpolation nor escaping, and most lowercase sigils have an uppercase variant. For example, although both `~s` and `~S` will return strings, the former allows escape codes and interpolation while the latter does not:
|
||||
|
||||
```elixir
|
||||
iex> ~s(String with escape codes \x26 #{"inter" <> "polation"})
|
||||
@@ -117,8 +117,6 @@ The following escape codes can be used in textual sigils:
|
||||
* `\xDD` - represents a single byte in hexadecimal (such as `\x13`)
|
||||
* `\uDDDD` and `\u{D...}` - represents a Unicode codepoint in hexadecimal (such as `\u{1F600}`)
|
||||
|
||||
In addition to those, a double quote inside a double-quoted string needs to be escaped as `\"`, and, analogously, a single quote inside a single-quoted char list needs to be escaped as `\'`. Nevertheless, it is better style to change delimiters as seen above than to escape them.
|
||||
|
||||
Sigils also support heredocs, that is, three double-quotes or single-quotes as separators:
|
||||
|
||||
```elixir
|
||||
@@ -160,7 +158,7 @@ def convert(...)
|
||||
|
||||
## Calendar sigils
|
||||
|
||||
Elixir offers several sigils to deal with various flavors of times and dates.
|
||||
Elixir offers sigils to deal with various flavors of times and dates.
|
||||
|
||||
### Date
|
||||
|
||||
@@ -240,6 +238,4 @@ iex> ~i(42)n
|
||||
-42
|
||||
```
|
||||
|
||||
Custom sigils may be either a single lowercase character, or an uppercase character followed by more uppercase characters and digits.
|
||||
|
||||
Sigils can also be used to do compile-time work with the help of macros. For example, regular expressions in Elixir are compiled into an efficient representation during compilation of the source code, therefore skipping this step at runtime. If you're interested in the subject, you can learn more about macros and check out how sigils are implemented in the `Kernel` module (where the `sigil_*` functions are defined).
|
||||
Custom sigils may be either a single lowercase character, or an uppercase character followed by more uppercase characters and digits. In practice, they are often used to embed templating languages or even represent regular languages within Elixir itself. If you're interested in learning more, check out how sigils are implemented in the `Kernel` module (where the `sigil_*` functions/macros are defined) for a starting point.
|
||||
|
||||
@@ -109,19 +109,18 @@ iex> john.__struct__
|
||||
User
|
||||
```
|
||||
|
||||
However, structs do not inherit any of the protocols that maps do. For example, you can neither enumerate nor access a struct:
|
||||
However, structs do not inherit any of the built-in features that maps do. For example, you can neither enumerate nor access a struct:
|
||||
|
||||
```elixir
|
||||
iex> john = %User{}
|
||||
%User{age: 27, name: "John"}
|
||||
iex> john[:name]
|
||||
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
|
||||
User.fetch(%User{age: 27, name: "John"}, :name)
|
||||
iex> Enum.each(john, fn {field, value} -> IO.puts(value) end)
|
||||
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"} of type User (a struct)
|
||||
```
|
||||
|
||||
Structs alongside protocols provide one of the most important features for Elixir developers: data polymorphism. That's what we will explore in the next chapter.
|
||||
As we will learn in future chapters, Elixir does allow developers to attach custom behaviours to structs based on their names, enabling custom data enumeration, pretty printing, and more. This dispatch mechanism is called [protocols](protocols.md) and enables extensible data polymorphism in Elixir.
|
||||
|
||||
## Default values and required keys
|
||||
|
||||
|
||||
@@ -303,4 +303,6 @@ iex> try do
|
||||
** (CompileError) undefined variable "another_what_happened"
|
||||
```
|
||||
|
||||
This finishes our introduction on `try`, `catch`, and `rescue`. You will find they are used less frequently in Elixir than in other languages. Next we will talk about a very important subject to Elixir developers: writing documentation.
|
||||
This finishes our introduction on `try`, `catch`, and `rescue`. You will find they are used less frequently in Elixir than in other languages.
|
||||
|
||||
Next, we'll look at a feature central to Elixir, Processes, which allows us to write concurrent, parallel and distributed programs in an easy and understandable way.
|
||||
|
||||
@@ -241,7 +241,7 @@ $ mix help
|
||||
mix deps # Lists dependencies and their status
|
||||
mix deps.clean # Deletes the given dependencies' files
|
||||
mix deps.compile # Compiles dependencies
|
||||
mix deps.get # Gets all out of date dependencies
|
||||
mix deps.get # Fetches unavailable and out of date dependencies
|
||||
mix deps.tree # Prints the dependency tree
|
||||
mix deps.unlock # Unlocks the given dependencies
|
||||
mix deps.update # Updates the given dependencies
|
||||
|
||||
@@ -143,7 +143,7 @@ With this change, we are saying that we want to run `KV.Server.accept(4040)` as
|
||||
Now that the server is part of the supervision tree, it should start automatically when we run the application. Run `iex -S mix` to boot the app and use the `telnet` client to make sure that everything still works:
|
||||
|
||||
```console
|
||||
$ telnet 127.0.0.1 4321
|
||||
$ telnet 127.0.0.1 4040
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
@@ -158,7 +158,7 @@ Yes, it works! However, can it handle more than one client?
|
||||
Try to connect two telnet clients at the same time. When you do so, you will notice that the second client doesn't echo:
|
||||
|
||||
```console
|
||||
$ telnet 127.0.0.1 4321
|
||||
$ telnet 127.0.0.1 4040
|
||||
Trying 127.0.0.1...
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
|
||||
@@ -14,12 +14,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.20 | Development
|
||||
1.19 | Bug fixes and security patches
|
||||
1.20 | Bug fixes and security patches
|
||||
1.19 | Security patches only
|
||||
1.18 | Security patches only
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | Security patches only
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
@@ -49,7 +48,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
|
||||
|
||||
Elixir version | Supported Erlang/OTP versions
|
||||
:------------- | :-------------------------------
|
||||
1.20 | 26 - 28
|
||||
1.20 | 27 - 29
|
||||
1.19 | 26 - 28
|
||||
1.18 | 25 - 27
|
||||
1.17 | 25 - 27
|
||||
@@ -243,4 +242,4 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.20]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.20]: https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md#4-hard-deprecations
|
||||
|
||||
@@ -231,20 +231,62 @@ Inferring type signatures comes with a series of trade-offs:
|
||||
|
||||
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
|
||||
|
||||
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
|
||||
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide type inference across dependencies: our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
|
||||
|
||||
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
|
||||
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing, without requiring any effort from them and keeping the expressiveness of the language.
|
||||
|
||||
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
|
||||
In the long term, Elixir developers who want static typing guarantees may explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
|
||||
|
||||
### False positives
|
||||
|
||||
Elixir's type inference generally avoids emitting false positive type violations: which are warnings emitted by the type checker when there are no runtime errors. However, in some situations, those may happen and are documented below.
|
||||
|
||||
#### `for`-comprehensions assume they are executed at least once
|
||||
|
||||
For comprehensions in Elixir assume they are executed at least once. Take this code:
|
||||
|
||||
```elixir
|
||||
def example(x, list) do
|
||||
for _i <- list do
|
||||
Atom.to_string(x)
|
||||
end
|
||||
|
||||
x + 1
|
||||
end
|
||||
```
|
||||
|
||||
`x + 1` will fail because it assumes `x` is an atom from the `Atom.to_string(x)` call, even though the function may raise no runtime error if `list` is an empty list. This is intentional, as it helps find discrepancies inside and outside comprehensions. You can address this by explicitly wrapping the comprehension in a `if list != [] do` block (or similar condition).
|
||||
|
||||
#### Struct update syntax must be statically proven
|
||||
|
||||
Elixir will warn if you use the struct update syntax and it is not statically proven that the given value does not have said struct type. For example:
|
||||
|
||||
```elixir
|
||||
user = find_user_by_id(42)
|
||||
%User{user | name: "John Doe"}
|
||||
```
|
||||
|
||||
Even though it is guaranteed at runtime that user is always a `User` struct. If the type system cannot prove it, it will emit a typing violation. This is how stuct updates work by design. In such cases, you can address it by matching on the struct when the user variable is defined:
|
||||
|
||||
```elixir
|
||||
%User{} = user = find_user_by_id(42)
|
||||
%User{user | name: "John Doe"}
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
The current milestone is to implement type inference of existing codebases, as well as type checking of all language constructs, without changes to the Elixir language. At this stage, we want to collect feedback on the quality of error messages and performance, and therefore the type system has no user facing API. Full type inference of patterns was released in Elixir v1.18, and complete inference is expected as part of Elixir v1.20.
|
||||
At this moment, Elixir implements type inference of all language constructs. The goal is to assess performance and collect feedback on the quality of error messages, before introducing user facing types.
|
||||
|
||||
If the results are satisfactory, the next milestone will include a mechanism for defining typed structs. Elixir programs frequently pattern match on structs, which reveals information about the struct fields, but it knows nothing about their respective types. By propagating types from structs and their fields throughout the program, we will increase the type system’s ability to find errors while further straining our type system implementation. Proposals including the required changes to the language surface will be sent to the community once we reach this stage.
|
||||
|
||||
The third milestone is to introduce set-theoretic type signatures for functions. Unfortunately, the existing Erlang Typespecs are not precise enough for set-theoretic types and they will be phased out of the language and have their postprocessing moved into a separate library once this stage concludes.
|
||||
|
||||
## Resources
|
||||
|
||||
* (Paper) ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391)
|
||||
* (Video) ["The foundations of the Elixir type system" by José Valim](https://youtu.be/giYbq4HmfGA)
|
||||
* (Video) ["Precision in type system design" by José Valim](https://youtu.be/Ay-gnCqDw9o?t=102)
|
||||
|
||||
## 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/).
|
||||
|
||||
@@ -72,6 +72,15 @@ iex> {x, x} = {1, 2}
|
||||
** (MatchError) no match of right hand side value: {1, 2}
|
||||
```
|
||||
|
||||
However, a variable cannot be defined through itself in the same pattern. This includes cases where the variable appears on both sides of a nested match operator:
|
||||
|
||||
```elixir
|
||||
{:ok, x} = {x, :ok}
|
||||
%{a: value} = %{b: value}
|
||||
```
|
||||
|
||||
Such patterns are rejected as cyclic variable definitions. You can rewrite them by matching once and then using guards or separate comparisons for the additional constraints.
|
||||
|
||||
The underscore variable (`_`) has a special meaning as it can never be bound to any value. It is especially useful when you don't care about certain value in a pattern:
|
||||
|
||||
```iex
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
-->
|
||||
|
||||
# Software Bill of Materials
|
||||
|
||||
A Software Bill of Materials (SBoM) is a structured inventory of the components that make up a software system. This guide explains what SBoMs are, why they matter, and how to generate them for Elixir projects.
|
||||
|
||||
## What is an SBoM?
|
||||
|
||||
An SBoM is a formal, machine-readable record of all components in a piece of software. Think of it as a detailed ingredient list for your application. A typical SBoM includes:
|
||||
|
||||
* **Dependencies**: all libraries and packages your project uses
|
||||
* **Versions**: the exact version of each component
|
||||
* **Source locations**: where each component was obtained (Hex, GitHub, etc.)
|
||||
* **Checksums**: cryptographic hashes to verify integrity
|
||||
* **Licensing information**: the license under which each component is distributed
|
||||
|
||||
Two widely adopted standards exist for SBoM formats:
|
||||
|
||||
* [CycloneDX](https://cyclonedx.org/): a lightweight standard designed for security contexts
|
||||
* [SPDX](https://spdx.dev/): a more comprehensive standard originally focused on licensing
|
||||
|
||||
Both formats are machine-readable (JSON, XML) and designed to be consumed by automated tooling.
|
||||
|
||||
> #### SBoM is an inventory, not a certification {: .info}
|
||||
>
|
||||
> An SBoM does not claim that software is secure, compliant, or free of vulnerabilities.
|
||||
> It simply provides a detailed inventory that enables further analysis. Security and
|
||||
> compliance assessments are performed by separate tools that consume the SBoM.
|
||||
|
||||
## Why generate SBoMs?
|
||||
|
||||
There are three main reasons to generate SBoMs for your projects.
|
||||
|
||||
### Vulnerability analysis
|
||||
|
||||
When a security vulnerability (CVE) is discovered in a library, you need to quickly determine if your projects are affected. SBoMs make this possible by providing a complete inventory of your dependencies.
|
||||
|
||||
Tools like [OWASP Dependency-Track](https://dependencytrack.org/) continuously monitor your SBoMs against vulnerability databases. When a new CVE is published, you get notified if any of your projects use the affected component.
|
||||
|
||||
Without an SBoM, answering "are we affected by CVE-2024-XXXXX?" means manually checking each project, which is time-consuming and error-prone.
|
||||
|
||||
### Regulatory requirements
|
||||
|
||||
SBoMs are required by some regulations and procurement policies:
|
||||
|
||||
* **US Executive Order 14028** (2021) requires SBoMs for certain software supplied to the U.S. federal government, particularly for designated critical software
|
||||
* **EU Cyber Resilience Act** introduces requirements for component inventories (commonly fulfilled using SBoMs) for products with digital elements placed on the EU market
|
||||
* **Safety-critical industries** (medical devices, automotive, aerospace) often require detailed component inventories as part of certification
|
||||
|
||||
Customers and partners may also request SBoMs as part of their own compliance efforts.
|
||||
|
||||
### License compliance
|
||||
|
||||
Every dependency in your project comes with a license. An SBoM provides a starting point for license review by listing the declared license for each package. This helps you:
|
||||
|
||||
* Get an overview of licenses in your dependency tree
|
||||
* Flag packages that may need closer review
|
||||
* Support due diligence for acquisitions, audits, or legal review
|
||||
|
||||
Note that package-level license information (as provided by mix_sbom) reflects what the package declares, not necessarily all licenses present in its source files. For thorough license compliance, file-level scanning tools like ORT provide deeper analysis.
|
||||
|
||||
## Generating SBoMs with mix_sbom
|
||||
|
||||
[mix_sbom](https://github.com/erlef/mix_sbom) is an EEF project that generates CycloneDX SBoMs for Elixir projects.
|
||||
|
||||
### Installation
|
||||
|
||||
There are several ways to install mix_sbom:
|
||||
|
||||
* **As a project dependency**: add `sbom` to your `mix.exs`
|
||||
* **As a global escript**: run `mix escript.install hex sbom` (requires Elixir 1.19.4+)
|
||||
* **As a standalone binary**: download from the [releases page](https://github.com/erlef/mix_sbom/releases)
|
||||
|
||||
The standalone binary is useful for CI environments or when you don't want to modify your project's dependencies. It requires no local Elixir or Erlang installation.
|
||||
|
||||
### Basic usage
|
||||
|
||||
To generate an SBoM for your project using the standalone binary:
|
||||
|
||||
```console
|
||||
$ mix_sbom cyclonedx /path/to/your/project
|
||||
```
|
||||
|
||||
This creates a `bom.cdx.json` file in CycloneDX format containing your project's complete dependency tree.
|
||||
|
||||
### Common options
|
||||
|
||||
The most useful options are:
|
||||
|
||||
* `-o, --output PATH`: specify the output file (default: `bom.cdx.json`)
|
||||
* `-t, --format FORMAT`: output format (`json`, `xml`, or `protobuf`)
|
||||
* `-s, --schema VERSION`: CycloneDX schema version
|
||||
|
||||
For example, to generate an XML format SBoM:
|
||||
|
||||
```console
|
||||
$ mix_sbom cyclonedx --format xml --output sbom.xml /path/to/project
|
||||
```
|
||||
|
||||
## CI integration
|
||||
|
||||
For automated SBoM generation, mix_sbom provides a GitHub Action:
|
||||
|
||||
```yaml
|
||||
name: Generate SBoM
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
sbom:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: erlef/mix_sbom@v0
|
||||
with:
|
||||
path: "."
|
||||
format: "json"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sbom
|
||||
path: bom.cdx.json
|
||||
```
|
||||
|
||||
This workflow generates an SBoM whenever you publish a release and uploads it as a build artifact. See the [action's documentation](https://github.com/erlef/mix_sbom) for additional options.
|
||||
|
||||
## Deeper analysis with ORT
|
||||
|
||||
mix_sbom provides package-level license information based on what each dependency declares. However, some compliance workflows require file-level scanning. A package might declare an MIT license but contain individual files under different licenses, or include vendored code with its own licensing terms.
|
||||
|
||||
The [OSS Review Toolkit (ORT)](https://oss-review-toolkit.org/) addresses this by scanning actual source files for license headers and copyright notices. ORT supports Mix projects and provides:
|
||||
|
||||
* **File-level license detection**: scans source code for license texts and SPDX identifiers
|
||||
* **Copyright holder identification**: extracts copyright notices from files
|
||||
* **Policy enforcement**: define rules for allowed and denied licenses
|
||||
* **Multi-ecosystem support**: analyze projects that span multiple package managers
|
||||
|
||||
For organizations with strict compliance requirements, ORT complements mix_sbom by providing the deeper analysis needed for thorough license audits.
|
||||
|
||||
See the [ORT Mix plugin documentation](https://oss-review-toolkit.org/ort/docs/plugins/package-managers/Mix) for details.
|
||||
|
||||
## Next steps
|
||||
|
||||
* [CycloneDX Specification](https://cyclonedx.org/specification/overview/): learn more about the SBoM format
|
||||
* [OWASP Dependency-Track](https://dependencytrack.org/): continuous SBoM analysis platform
|
||||
* [mix_sbom documentation](https://hexdocs.pm/sbom): full documentation and advanced options
|
||||
@@ -3,7 +3,11 @@
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
# Returns config for Elixir docs (exclusively)
|
||||
canonical = System.fetch_env!("CANONICAL")
|
||||
canonical =
|
||||
case System.fetch_env!("CANONICAL") do
|
||||
"" -> System.version() <> "/"
|
||||
canonical -> canonical
|
||||
end
|
||||
|
||||
[
|
||||
search: [
|
||||
@@ -61,6 +65,7 @@ canonical = System.fetch_env!("CANONICAL")
|
||||
"lib/elixir/pages/references/operators.md",
|
||||
"lib/elixir/pages/references/patterns-and-guards.md",
|
||||
"lib/elixir/pages/references/syntax-reference.md",
|
||||
"lib/elixir/pages/references/sbom.md",
|
||||
"lib/elixir/pages/references/typespecs.md",
|
||||
"lib/elixir/pages/references/unicode-syntax.md",
|
||||
"lib/elixir/pages/mix-and-otp/introduction-to-mix.md",
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
# Returns config for other apps except Elixir
|
||||
canonical = System.fetch_env!("CANONICAL")
|
||||
canonical =
|
||||
case System.fetch_env!("CANONICAL") do
|
||||
"" -> System.version() <> "/"
|
||||
canonical -> canonical
|
||||
end
|
||||
|
||||
[
|
||||
search: [
|
||||
|
||||
+102
-91
@@ -9,10 +9,9 @@
|
||||
-export([start_cli/0, start/0]).
|
||||
-export([start/2, stop/1, config_change/3]).
|
||||
-export([
|
||||
string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5,
|
||||
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3,
|
||||
eval_quoted/4, eval_local_handler/2, eval_external_handler/3,
|
||||
format_token_error/1
|
||||
string_to_tokens/5, tokens_to_quoted/3, string_to_quoted/5, 'string_to_quoted!'/5,
|
||||
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_forms/4, eval_quoted/3, eval_quoted/4,
|
||||
erl_eval/3, eval_local_handler/2, eval_external_handler/3, emit_warnings/3
|
||||
]).
|
||||
-include("elixir.hrl").
|
||||
-define(system, 'Elixir.System').
|
||||
@@ -64,11 +63,6 @@ start(_Type, _Args) ->
|
||||
application:set_env(elixir, ansi_enabled, prim_tty:isatty(stdout) == true)
|
||||
end,
|
||||
|
||||
%% Store the initial dbg_callback value before any runtime modifications.
|
||||
%% This allows Mix compiler to detect config changes vs runtime changes
|
||||
%% (e.g., Kino wrapping dbg_callback at runtime should not trigger recompilation).
|
||||
{ok, InitialDbgCallback} = application:get_env(elixir, dbg_callback),
|
||||
|
||||
Tokenizer = case code:ensure_loaded('Elixir.String.Tokenizer') of
|
||||
{module, Mod} -> Mod;
|
||||
_ -> elixir_tokenizer
|
||||
@@ -92,16 +86,16 @@ start(_Type, _Args) ->
|
||||
{no_halt, false},
|
||||
|
||||
%% Compiler options
|
||||
{debug_info, true},
|
||||
{docs, true},
|
||||
{ignore_already_consolidated, false},
|
||||
{ignore_module_conflict, false},
|
||||
{initial_dbg_callback, InitialDbgCallback},
|
||||
{infer_signatures, [elixir]},
|
||||
{module_definition, compiled},
|
||||
{no_warn_undefined, []},
|
||||
{on_undefined_variable, raise},
|
||||
{parser_options, [{columns, true}]},
|
||||
{debug_info, true},
|
||||
{relative_paths, true},
|
||||
{no_warn_undefined, []},
|
||||
{tracers, []}
|
||||
| URIConfig
|
||||
],
|
||||
@@ -133,20 +127,20 @@ preload_common_modules() ->
|
||||
parse_otp_release() ->
|
||||
%% Whenever we change this check, we should also change Makefile.
|
||||
case string:to_integer(erlang:system_info(otp_release)) of
|
||||
{Num, _} when Num >= 26 ->
|
||||
{Num, _} when Num >= 27 ->
|
||||
case Num == 28 andalso (code:ensure_loaded(re) == {module, re}) andalso not erlang:function_exported(re, import, 1) of
|
||||
true ->
|
||||
io:format(standard_error,
|
||||
"warning! Erlang/OTP 28.0 detected.~n"
|
||||
"Regexes will be re-compiled from source at runtime, which will cause degraded performance.~n"
|
||||
"This can be fixed by using Erlang OTP 28.1+ or 27-.~n"
|
||||
"This can be fixed by using Erlang OTP 28.1+ or 27.~n"
|
||||
, []);
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
Num;
|
||||
_ ->
|
||||
io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 26+~n", []),
|
||||
io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 27+~n", []),
|
||||
erlang:halt(1)
|
||||
end.
|
||||
|
||||
@@ -310,53 +304,70 @@ eval_forms(Tree, Binding, OrigE) ->
|
||||
eval_forms(Tree, Binding, OrigE, []).
|
||||
eval_forms(Tree, Binding, OrigE, Opts) ->
|
||||
Prune = proplists:get_value(prune_binding, Opts, false),
|
||||
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
|
||||
E = elixir_env:with_vars(OrigE, ExVars),
|
||||
ExS = elixir_env:env_to_ex(E),
|
||||
ErlS = elixir_erl_var:from_env(E, ErlVars),
|
||||
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
|
||||
%% We save and restore the previous dbg_callback so nested eval calls in
|
||||
%% the same process do not clobber the outer callback.
|
||||
PreviousDbg = erlang:get({elixir, dbg_callback}),
|
||||
case proplists:get_value(dbg_callback, Opts) of
|
||||
undefined -> ok;
|
||||
DbgCallback -> erlang:put({elixir, dbg_callback}, DbgCallback)
|
||||
end,
|
||||
try
|
||||
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
|
||||
E = elixir_env:with_vars(OrigE, ExVars),
|
||||
ExS = elixir_env:env_to_ex(E),
|
||||
ErlS = elixir_erl_var:from_env(E, ErlVars),
|
||||
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
|
||||
|
||||
case Erl of
|
||||
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
|
||||
if
|
||||
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
|
||||
true -> {Value, Binding, NewE}
|
||||
end;
|
||||
case Erl of
|
||||
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
|
||||
if
|
||||
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
|
||||
true -> {Value, Binding, NewE}
|
||||
end;
|
||||
|
||||
_ ->
|
||||
Exprs =
|
||||
case Erl of
|
||||
{block, _, BlockExprs} -> BlockExprs;
|
||||
_ -> [Erl]
|
||||
end,
|
||||
_ ->
|
||||
{value, Value, NewBinding} = erl_eval(Erl, ErlBinding, NewE),
|
||||
PruneBefore = if Prune -> length(Binding); true -> -1 end,
|
||||
|
||||
%% We use remote names so eval works across Elixir versions.
|
||||
LocalHandler = {value, fun ?MODULE:eval_local_handler/2},
|
||||
ExternalHandler = {value, fun ?MODULE:eval_external_handler/3},
|
||||
{DumpedBinding, DumpedVars} =
|
||||
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
|
||||
|
||||
{value, Value, NewBinding} =
|
||||
try
|
||||
%% ?elixir_eval_env is used by the external handler.
|
||||
%%
|
||||
%% The reason why we use the process dictionary to pass the environment
|
||||
%% is because we want to avoid passing closures to erl_eval, as that
|
||||
%% would effectively tie the eval code to the Elixir version and it is
|
||||
%% best if it depends solely on Erlang/OTP.
|
||||
%%
|
||||
%% The downside is that functions that escape the eval context will no
|
||||
%% longer have the original environment they came from.
|
||||
erlang:put(?elixir_eval_env, NewE),
|
||||
erl_eval:exprs(Exprs, ErlBinding, LocalHandler, ExternalHandler)
|
||||
after
|
||||
erlang:erase(?elixir_eval_env)
|
||||
end,
|
||||
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
|
||||
end
|
||||
after
|
||||
case PreviousDbg of
|
||||
undefined -> erlang:erase({elixir, dbg_callback});
|
||||
_ -> erlang:put({elixir, dbg_callback}, PreviousDbg)
|
||||
end
|
||||
end.
|
||||
|
||||
PruneBefore = if Prune -> length(Binding); true -> -1 end,
|
||||
%% Evaluate Erlang code with careful handling of local and external functions
|
||||
erl_eval(Expr, Binding, Env) ->
|
||||
%% We use remote names so eval works across Elixir versions
|
||||
LocalHandler = {value, fun ?MODULE:eval_local_handler/2},
|
||||
ExternalHandler = {value, fun ?MODULE:eval_external_handler/3},
|
||||
|
||||
{DumpedBinding, DumpedVars} =
|
||||
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
|
||||
|
||||
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
|
||||
%% ?elixir_eval_env is used by the external handler.
|
||||
%%
|
||||
%% The reason why we use the process dictionary to pass the environment
|
||||
%% is because we want to avoid passing closures to erl_eval, as that
|
||||
%% would effectively tie the eval code to the Elixir version and it is
|
||||
%% best if it depends solely on Erlang/OTP.
|
||||
%%
|
||||
%% The downside is that functions that escape the eval context will no
|
||||
%% longer have the original environment they came from.
|
||||
%%
|
||||
%% We save and restore the previous env so nested eval calls in the same
|
||||
%% process do not clobber the outer env.
|
||||
PreviousEvalEnv = erlang:get(?elixir_eval_env),
|
||||
erlang:put(?elixir_eval_env, Env),
|
||||
try
|
||||
erl_eval:expr(Expr, Binding, LocalHandler, ExternalHandler)
|
||||
after
|
||||
case PreviousEvalEnv of
|
||||
undefined -> erlang:erase(?elixir_eval_env);
|
||||
_ -> erlang:put(?elixir_eval_env, PreviousEvalEnv)
|
||||
end
|
||||
end.
|
||||
|
||||
eval_local_handler(FunName, Args) ->
|
||||
@@ -405,10 +416,8 @@ eval_external_handler(Ann, FunOrModFun, Args) ->
|
||||
%% Add file+line information at the bottom
|
||||
Bottom =
|
||||
case erlang:get(?elixir_eval_env) of
|
||||
#{file := File} ->
|
||||
[{elixir_eval, '__FILE__', 1,
|
||||
[{file, elixir_utils:characters_to_list(File)}, {line, erl_anno:line(Ann)}]}];
|
||||
|
||||
#{'__struct__' := 'Elixir.Macro.Env'} = E ->
|
||||
'Elixir.Macro.Env':stacktrace(E#{line := erl_anno:line(Ann)});
|
||||
_ ->
|
||||
[]
|
||||
end,
|
||||
@@ -443,40 +452,36 @@ quoted_to_erl(Quoted, ErlS, ExS, Env) ->
|
||||
|
||||
%% Converts a given string (charlist) into quote expression
|
||||
|
||||
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_binary(File) ->
|
||||
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_integer(StartColumn), is_binary(File) ->
|
||||
case elixir_tokenizer:tokenize(String, StartLine, StartColumn, Opts) of
|
||||
{ok, _Line, _Column, [], Tokens, []} ->
|
||||
{ok, lists:reverse(Tokens)};
|
||||
{ok, _Line, _Column, Warnings, Tokens, Terminators} ->
|
||||
(lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso
|
||||
[elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)],
|
||||
{ok, lists:reverse(Tokens, Terminators)};
|
||||
{ok, lists:reverse(Tokens, Terminators), Warnings};
|
||||
{error, Info, _Rest, _Warnings, _SoFar} ->
|
||||
{error, format_token_error(Info)}
|
||||
{error, elixir_tokenizer:format_error(Info)}
|
||||
end.
|
||||
|
||||
format_token_error({Location, {ErrorPrefix, ErrorSuffix}, Token}) ->
|
||||
{Location, {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)};
|
||||
format_token_error({Location, Error, Token}) ->
|
||||
{Location, to_binary(Error), to_binary(Token)}.
|
||||
|
||||
tokens_to_quoted(Tokens, WarningFile, Opts) ->
|
||||
handle_parsing_opts(WarningFile, Opts),
|
||||
tokens_to_quoted(Tokens, _WarningFile, Opts) ->
|
||||
put_parsing_state(Opts),
|
||||
|
||||
try elixir_parser:parse(Tokens) of
|
||||
{ok, Forms} ->
|
||||
{ok, Forms};
|
||||
{ok, Forms, get(elixir_parser_warnings)};
|
||||
{error, {Line, _, [{ErrorPrefix, ErrorSuffix}, Token]}} ->
|
||||
{error, {parser_location(Line), {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)}};
|
||||
{error, {Line, _, [Error, Token]}} ->
|
||||
{error, {parser_location(Line), to_binary(Error), to_binary(Token)}}
|
||||
after
|
||||
erase(elixir_parser_warning_file),
|
||||
erase(elixir_parser_warnings),
|
||||
erase(elixir_parser_columns),
|
||||
erase(elixir_token_metadata),
|
||||
erase(elixir_literal_encoder)
|
||||
end.
|
||||
|
||||
emit_warnings(Warnings, File, Opts) ->
|
||||
(Warnings /= []) andalso
|
||||
(lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso
|
||||
[elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)].
|
||||
|
||||
parser_location({Line, Column, _}) ->
|
||||
[{line, Line}, {column, Column}];
|
||||
parser_location(Meta) ->
|
||||
@@ -491,17 +496,28 @@ parser_location(Meta) ->
|
||||
false -> [{line, Line}]
|
||||
end.
|
||||
|
||||
'string_to_quoted!'(String, StartLine, StartColumn, File, Opts) ->
|
||||
string_to_quoted(String, StartLine, StartColumn, File, Opts) ->
|
||||
case string_to_tokens(String, StartLine, StartColumn, File, Opts) of
|
||||
{ok, Tokens} ->
|
||||
{ok, Tokens, Warnings1} ->
|
||||
emit_warnings(Warnings1, File, Opts),
|
||||
|
||||
case tokens_to_quoted(Tokens, File, Opts) of
|
||||
{ok, Forms} ->
|
||||
Forms;
|
||||
{error, {Meta, Error, Token}} ->
|
||||
Indentation = proplists:get_value(indentation, Opts, 0),
|
||||
Input = {String, StartLine, StartColumn, Indentation},
|
||||
elixir_errors:parse_error(Meta, File, Error, Token, Input)
|
||||
{ok, Forms, Warnings2} ->
|
||||
emit_warnings(Warnings2, File, Opts),
|
||||
{ok, Forms};
|
||||
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
'string_to_quoted!'(String, StartLine, StartColumn, File, Opts) ->
|
||||
case string_to_quoted(String, StartLine, StartColumn, File, Opts) of
|
||||
{ok, Forms} ->
|
||||
Forms;
|
||||
{error, {Meta, Error, Token}} ->
|
||||
Indentation = proplists:get_value(indentation, Opts, 0),
|
||||
Input = {String, StartLine, StartColumn, Indentation},
|
||||
@@ -511,12 +527,7 @@ parser_location(Meta) ->
|
||||
to_binary(List) when is_list(List) -> elixir_utils:characters_to_binary(List);
|
||||
to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom).
|
||||
|
||||
handle_parsing_opts(File, Opts) ->
|
||||
WarningFile =
|
||||
case lists:keyfind(emit_warnings, 1, Opts) of
|
||||
{emit_warnings, false} -> nil;
|
||||
_ -> File
|
||||
end,
|
||||
put_parsing_state(Opts) ->
|
||||
LiteralEncoder =
|
||||
case lists:keyfind(literal_encoder, 1, Opts) of
|
||||
{literal_encoder, Fun} -> Fun;
|
||||
@@ -524,7 +535,7 @@ handle_parsing_opts(File, Opts) ->
|
||||
end,
|
||||
TokenMetadata = lists:keyfind(token_metadata, 1, Opts) == {token_metadata, true},
|
||||
Columns = lists:keyfind(columns, 1, Opts) == {columns, true},
|
||||
put(elixir_parser_warning_file, WarningFile),
|
||||
put(elixir_parser_warnings, []),
|
||||
put(elixir_parser_columns, Columns),
|
||||
put(elixir_token_metadata, TokenMetadata),
|
||||
put(elixir_literal_encoder, LiteralEncoder).
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
-define(tracker, 'Elixir.Kernel.LexicalTracker').
|
||||
|
||||
-record(elixir_ex, {
|
||||
%% Stores that the function will fail, which can be used
|
||||
%% to run more expensive checks upfront
|
||||
tainted_function=false,
|
||||
%% Stores if __CALLER__ is allowed
|
||||
caller=false,
|
||||
%% Stores the variables available before a match.
|
||||
@@ -28,8 +31,8 @@
|
||||
prematch=none,
|
||||
%% Stores if __STACKTRACE__ is allowed
|
||||
stacktrace=false,
|
||||
%% A map of unused vars and a version counter for vars
|
||||
unused={#{}, 0},
|
||||
%% A map of unused vars
|
||||
unused=#{},
|
||||
%% A list of modules defined in functions (runtime)
|
||||
runtime_modules=[],
|
||||
%% A tuple with maps of read and optional write current vars.
|
||||
@@ -43,7 +46,9 @@
|
||||
%% if you write foo(a = 123), the value of `a` cannot be
|
||||
%% read in the following argument, only after the call
|
||||
%%
|
||||
vars={#{}, false}
|
||||
vars={#{}, false},
|
||||
%% Stores expression version counter
|
||||
version=0
|
||||
}).
|
||||
|
||||
-record(elixir_erl, {
|
||||
|
||||
@@ -61,7 +61,7 @@ expand(BitstrMeta, Fun, [H | T], Acc, S, E, Alignment, RequireSize) ->
|
||||
|
||||
InferredMeta = [{inferred_bitstring_spec, true} | Meta],
|
||||
EAcc = concat_or_prepend_bitstring(InferredMeta, ELeft, ERight, Acc, ES, MatchOrRequireSize),
|
||||
expand(Meta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
|
||||
expand(BitstrMeta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
|
||||
|
||||
extract_meta({_, Meta, _}, _) -> Meta;
|
||||
extract_meta(_, Meta) -> Meta.
|
||||
@@ -115,7 +115,9 @@ concat_or_prepend_bitstring(Meta, {'<<>>', PartsMeta, Parts} = ELeft, ERight, Ac
|
||||
[{'::', Meta, [ELeft, ERight]} | Acc]
|
||||
end;
|
||||
{bitstring, _, nil} ->
|
||||
lists:reverse(Parts, Acc)
|
||||
lists:reverse(Parts, Acc);
|
||||
_ ->
|
||||
[{'::', Meta, [ELeft, ERight]} | Acc]
|
||||
end;
|
||||
concat_or_prepend_bitstring(Meta, ELeft, ERight, Acc, _E, _RequireSize) ->
|
||||
[{'::', Meta, [ELeft, ERight]} | Acc].
|
||||
|
||||
@@ -155,13 +155,14 @@ recur_cycles(Cycles, Current, Source, Seen, SkipList, Meta, Expr, E) ->
|
||||
%% Match
|
||||
|
||||
match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
|
||||
#elixir_ex{vars=Current, unused={_, Counter} = Unused} = AfterS,
|
||||
#elixir_ex{vars=Current, unused=Unused, version=Counter} = AfterS,
|
||||
#elixir_ex{vars={Read, _}, prematch=Prematch} = BeforeS,
|
||||
|
||||
CallS = BeforeS#elixir_ex{
|
||||
prematch={Read, {#{}, []}, Counter},
|
||||
unused=Unused,
|
||||
vars=Current
|
||||
vars=Current,
|
||||
version=Counter
|
||||
},
|
||||
|
||||
CallE = E#{context := match},
|
||||
@@ -170,7 +171,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
|
||||
#elixir_ex{
|
||||
vars=NewCurrent,
|
||||
unused=NewUnused,
|
||||
prematch={_, Cycles, _}
|
||||
prematch={_, Cycles, _},
|
||||
version=NewCounter
|
||||
} = SE,
|
||||
|
||||
validate_cycles(Cycles, Meta, {match, Expr}, E),
|
||||
@@ -178,7 +180,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
|
||||
EndS = AfterS#elixir_ex{
|
||||
prematch=Prematch,
|
||||
unused=NewUnused,
|
||||
vars=NewCurrent
|
||||
vars=NewCurrent,
|
||||
version=NewCounter
|
||||
},
|
||||
|
||||
EndE = EE#{context := ?key(E, context)},
|
||||
@@ -319,7 +322,9 @@ with(Meta, Args, S, E) ->
|
||||
ok
|
||||
end,
|
||||
|
||||
{{with, Meta, EExprs ++ [[{do, EDo} | EOpts]]}, S3, E}.
|
||||
#elixir_ex{version=Counter} = S3,
|
||||
{{with, [{version, Counter} | Meta], EExprs ++ [[{do, EDo} | EOpts]]},
|
||||
S3#elixir_ex{version=Counter+1}, E}.
|
||||
|
||||
expand_with({'<-', Meta, [Left, Right]}, {S, E, HasMatch}) ->
|
||||
{ERight, SR, ER} = elixir_expand:expand(Right, S, E),
|
||||
@@ -535,8 +540,8 @@ warn_catch_before_rescue([_ | Rest], Meta, E, Found) ->
|
||||
warn_catch_before_rescue(Rest, Meta, E, Found).
|
||||
|
||||
origin(Meta, Default) ->
|
||||
case lists:keyfind(origin, 1, Meta) of
|
||||
{origin, Origin} -> Origin;
|
||||
case lists:keyfind(definition, 1, Meta) of
|
||||
{definition, Definition} -> Definition;
|
||||
false -> Default
|
||||
end.
|
||||
|
||||
@@ -557,16 +562,16 @@ format_error({recursive, Vars, TypeExpr}) ->
|
||||
Message =
|
||||
case lists:map(fun({Name, Context}) -> elixir_utils:var_info(Name, Context) end, lists:sort(Vars)) of
|
||||
[Var] ->
|
||||
io_lib:format("the variable ~ts is defined in function of itself", [Var]);
|
||||
io_lib:format("the variable ~ts depends on itself through the pattern", [Var]);
|
||||
[Var1, Var2] ->
|
||||
io_lib:format("the variable ~ts is defined recursively in function of ~ts", [Var1, Var2]);
|
||||
io_lib:format("the variable ~ts depends on ~ts through the pattern", [Var1, Var2]);
|
||||
[Head | Tail] ->
|
||||
List = lists:foldl(fun(X, Acc) -> [Acc, $,, $\s, X] end, Head, Tail),
|
||||
io_lib:format("the following variables form a cycle: ~ts", [List])
|
||||
end,
|
||||
|
||||
io_lib:format(
|
||||
"recursive variable definition in patterns:~n~n~ts~n~n~ts",
|
||||
"cyclic variable definition in patterns:~n~n~ts~n~n~ts",
|
||||
[Code, Message]
|
||||
);
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ handle_call({acquire, Path}, From, Config) ->
|
||||
handle_call(required, _From, Config) ->
|
||||
{reply, [F || {F, true} <- maps:to_list(Config#elixir_code_server.required)], Config};
|
||||
|
||||
handle_call({release, Path}, _From, Config) ->
|
||||
{reply, ok, release(Path, Config)};
|
||||
|
||||
handle_call(retrieve_compiler_module, _From, Config) ->
|
||||
case Config#elixir_code_server.mod_pool of
|
||||
{Used, [Mod | Unused], Counter} ->
|
||||
@@ -70,7 +73,7 @@ handle_call(retrieve_compiler_module, _From, Config) ->
|
||||
handle_call(purge_compiler_modules, _From, Config) ->
|
||||
{Used, Unused, Counter} = Config#elixir_code_server.mod_pool,
|
||||
purge_used(Used),
|
||||
Mods = lists:map(fun({Mod, _}) -> Mod end, Used),
|
||||
Mods = [Mod || {Mod, Purgeable} <- Used, Purgeable],
|
||||
ModPool = {[], Mods ++ Unused, Counter},
|
||||
{reply, {ok, length(Used)}, Config#elixir_code_server{mod_pool=ModPool}};
|
||||
|
||||
@@ -112,7 +115,7 @@ handle_cast(purge_compiler_modules, Config) ->
|
||||
%% purge them asynchronously, especially because they can
|
||||
%% block the code server. Ideally we would purge them in
|
||||
%% batches, but that's not supported at the moment.
|
||||
Mods = lists:map(fun({Mod, _}) -> Mod end, Used),
|
||||
Mods = [Mod || {Mod, Purgeable} <- Used, Purgeable],
|
||||
Opts = [{monitor, [{tag, {purged, Mods}}]}],
|
||||
erlang:spawn_opt(fun() -> purge_used(Used) end, Opts)
|
||||
end,
|
||||
@@ -140,6 +143,20 @@ terminate(_Reason, _Config) ->
|
||||
code_change(_Old, Config, _Extra) ->
|
||||
{ok, Config}.
|
||||
|
||||
release(Path, Config) ->
|
||||
Current = Config#elixir_code_server.required,
|
||||
case maps:find(Path, Current) of
|
||||
{ok, []} ->
|
||||
Released = maps:remove(Path, Current),
|
||||
Config#elixir_code_server{required=Released};
|
||||
{ok, [Next | Waiting]} ->
|
||||
_ = gen_server:reply(Next, proceed),
|
||||
Released = maps:put(Path, Waiting, Current),
|
||||
Config#elixir_code_server{required=Released};
|
||||
error ->
|
||||
Config
|
||||
end.
|
||||
|
||||
compiler_module(I) ->
|
||||
list_to_atom("elixir_compiler_" ++ integer_to_list(I)).
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user