Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
2ab126e223 | ||
|
|
f2e6d6a2fb | ||
|
|
a5361436c4 | ||
|
|
0b1d3fcaf9 | ||
|
|
f7b179ac1d | ||
|
|
426a4297c9 | ||
|
|
be34850da4 | ||
|
|
3fbe977035 | ||
|
|
cebc5a76fb | ||
|
|
c40c140e3f | ||
|
|
7fef1c58e4 | ||
|
|
7048f60539 | ||
|
|
4940d0f99e | ||
|
|
7b22b8e7eb | ||
|
|
ba86bebd4c | ||
|
|
5093bab3ab | ||
|
|
b381ab3ae4 | ||
|
|
30b59edf39 | ||
|
|
ff132444ca | ||
|
|
d031425cbd | ||
|
|
dc7b20fe0a | ||
|
|
47d2b6d305 | ||
|
|
663019d048 | ||
|
|
73adf727f9 | ||
|
|
22006fd77a | ||
|
|
e87b9359c4 | ||
|
|
b6a0097226 | ||
|
|
cbe3d72666 | ||
|
|
44abf19c97 | ||
|
|
a26c321e79 | ||
|
|
32082d1367 | ||
|
|
45734fe216 | ||
|
|
ebfebede92 | ||
|
|
8def030f38 | ||
|
|
61d82a94f8 | ||
|
|
1b0dff7e6a | ||
|
|
0862491255 | ||
|
|
19cddfeea1 | ||
|
|
b523a8e706 | ||
|
|
3b75c46513 | ||
|
|
3bb5f92fa9 | ||
|
|
cb472c10f3 | ||
|
|
e5dc69398e | ||
|
|
fd6c6af81d | ||
|
|
a9a0969c28 | ||
|
|
dd284bbf9b | ||
|
|
bc1a862c93 | ||
|
|
ae383313f6 | ||
|
|
f00cb3a9a0 | ||
|
|
9af054f9bc | ||
|
|
61f55052fe | ||
|
|
d8322e3f76 | ||
|
|
f2ff3a7a86 | ||
|
|
7f5e4e3bb2 | ||
|
|
bfa154e70b | ||
|
|
e586ed519e | ||
|
|
8767c5b530 | ||
|
|
7c13e99412 | ||
|
|
d8ed2b9ae4 | ||
|
|
d653d1522a | ||
|
|
1752058872 | ||
|
|
0f9d919074 | ||
|
|
45de6e5dc3 | ||
|
|
15f2165903 | ||
|
|
2a2be0acde | ||
|
|
583737d77d | ||
|
|
205a1ecb4e | ||
|
|
9721759d35 | ||
|
|
6bace04dc9 | ||
|
|
4ee0241a5e | ||
|
|
1b234cadb1 | ||
|
|
99eae9536b | ||
|
|
7fb01d3f47 | ||
|
|
386c5d9d72 | ||
|
|
ad7559f6de | ||
|
|
98155171a2 | ||
|
|
5abc024203 | ||
|
|
ea59662fa0 | ||
|
|
be0f3ae3e9 | ||
|
|
aa222426d0 | ||
|
|
caea8bcfa3 | ||
|
|
a4e7a2d19d | ||
|
|
ad67ac0a1a | ||
|
|
88cbabfd84 | ||
|
|
0f67706cf9 | ||
|
|
5b4ee56d7e | ||
|
|
c35f651309 | ||
|
|
f1bbb2cd32 | ||
|
|
3884e7a91c | ||
|
|
94ba62178b | ||
|
|
a077a6e3fb | ||
|
|
fff97fd3cc | ||
|
|
53a567556a | ||
|
|
56259933ea | ||
|
|
19c628ae23 | ||
|
|
5f616e254f | ||
|
|
ee87c1c8a9 | ||
|
|
6d433580f3 | ||
|
|
3c8fe8606c | ||
|
|
8d8111af07 | ||
|
|
fc747ffbe3 | ||
|
|
e6f90bd55c | ||
|
|
7a01f5bab2 | ||
|
|
f7904a4aa6 | ||
|
|
6948d08d51 | ||
|
|
0473fec367 | ||
|
|
2cd8a57b37 | ||
|
|
07889d8c52 | ||
|
|
1445e8d021 | ||
|
|
1921765c78 | ||
|
|
0e3d22fd79 | ||
|
|
f9ebeaa5a7 | ||
|
|
6ce5044521 | ||
|
|
5f60c39d66 | ||
|
|
9d8df88f80 | ||
|
|
743fa8ada5 | ||
|
|
995f7fc2c4 | ||
|
|
67431fc48a | ||
|
|
ec267745e6 | ||
|
|
bb9721f27c | ||
|
|
68207c0186 | ||
|
|
14bd4b5401 | ||
|
|
0f00cb0113 | ||
|
|
e622a26420 | ||
|
|
72435c44a5 | ||
|
|
743fea7a11 | ||
|
|
67baf686eb | ||
|
|
297bd84ac2 | ||
|
|
e609d29bc6 | ||
|
|
0f3fdcd84f | ||
|
|
2d327241de | ||
|
|
f58e02adf1 | ||
|
|
5c586dae68 | ||
|
|
af3624c823 | ||
|
|
bd909cb658 | ||
|
|
aee1747c5d | ||
|
|
fabbf5e9c9 | ||
|
|
0435602337 | ||
|
|
b60e424204 | ||
|
|
70e23a407e | ||
|
|
a9aac5216f | ||
|
|
d831a00063 | ||
|
|
a09fa1075a | ||
|
|
0b3dc130cd | ||
|
|
62d3e61e89 | ||
|
|
de32dff024 | ||
|
|
a6ba0cb329 | ||
|
|
18c81d66e2 | ||
|
|
7fbf48ccc7 | ||
|
|
77ef258ee6 | ||
|
|
ab843d0342 | ||
|
|
79c28420d4 | ||
|
|
0e3465d1fc | ||
|
|
ee75b22a0c | ||
|
|
1110a9511f | ||
|
|
5f608c4ad3 | ||
|
|
c80a3b1a0e | ||
|
|
b79b08d819 | ||
|
|
34004a2b90 | ||
|
|
09fc63d475 | ||
|
|
e1ff7819b8 | ||
|
|
82983cf979 | ||
|
|
904adda55f | ||
|
|
8fc181fb27 | ||
|
|
d30b22f5d4 | ||
|
|
030660aaf7 | ||
|
|
fc07f452b3 | ||
|
|
4c10b43e3d | ||
|
|
08b559fe96 | ||
|
|
1cadefffc7 | ||
|
|
ac44e723ba | ||
|
|
2ac361a4a4 | ||
|
|
509e715f60 | ||
|
|
a6951d0448 | ||
|
|
3c5e968a54 | ||
|
|
cad69f790f | ||
|
|
1aa188f115 | ||
|
|
41f9bcf621 | ||
|
|
2b63546b19 | ||
|
|
a13f38edbe | ||
|
|
31c9c84ebe | ||
|
|
284b190bb0 | ||
|
|
b58cdd823c | ||
|
|
e66a799b2f | ||
|
|
bd4c232a28 | ||
|
|
521b16ce21 | ||
|
|
3172f7487d | ||
|
|
6b519ae782 | ||
|
|
6710d7e197 | ||
|
|
7ad99b5590 | ||
|
|
ff2b73861d | ||
|
|
e2c4c07b9a | ||
|
|
2acf5804d3 | ||
|
|
1da7feb387 | ||
|
|
31ec8cc703 | ||
|
|
abd73a841f | ||
|
|
0b4d0f360d | ||
|
|
10bc090ac1 | ||
|
|
68ebb78d66 | ||
|
|
66165e6b0f | ||
|
|
bf2298e050 | ||
|
|
75c5aa0a6b | ||
|
|
19dcd95a36 | ||
|
|
da1481e433 | ||
|
|
c4c5c060d6 | ||
|
|
1b215b7a36 | ||
|
|
81ef21272d | ||
|
|
7da1b76b6b | ||
|
|
f52f395fcf | ||
|
|
e55388d6e6 | ||
|
|
912f3d546a | ||
|
|
7eea067534 | ||
|
|
dc3986833e | ||
|
|
a2f21c91a4 | ||
|
|
17e244c0a5 | ||
|
|
2219d74c38 | ||
|
|
0055f2fe53 | ||
|
|
522aebdf74 | ||
|
|
1fa63ffe7a | ||
|
|
16862cef79 | ||
|
|
ae752c7bb0 | ||
|
|
f9745a25cf | ||
|
|
1b92f3ac87 | ||
|
|
9ec2eb41ae | ||
|
|
47aab3b37c | ||
|
|
7ef4a3be97 | ||
|
|
d56546d81e | ||
|
|
e56b730c8c | ||
|
|
98b35eaa53 | ||
|
|
5732283ada | ||
|
|
46807cfcdf | ||
|
|
7989e87716 | ||
|
|
ef52e13938 | ||
|
|
8fb1ddf060 | ||
|
|
51e25fbf4a | ||
|
|
cf454cd1f1 | ||
|
|
f12ff1d7db | ||
|
|
c7a9d8544a | ||
|
|
c80fc6da1a | ||
|
|
cbb508600c | ||
|
|
f248994830 | ||
|
|
b45853f043 | ||
|
|
6f26bb0b05 | ||
|
|
ee469e56e6 | ||
|
|
0126f1f1a3 | ||
|
|
edb9431df6 | ||
|
|
71a8f53521 | ||
|
|
541f641a92 | ||
|
|
c0933fbbac | ||
|
|
06619c2fcc | ||
|
|
a0eb603091 | ||
|
|
0df110c9c0 | ||
|
|
22635e6bda | ||
|
|
4a8d4f29ae | ||
|
|
6460c642c3 | ||
|
|
31905ca136 | ||
|
|
3c1514aef6 | ||
|
|
6402967c4b | ||
|
|
757ab8c456 | ||
|
|
0fac5d5f4a | ||
|
|
21450cf838 | ||
|
|
c18f53ed93 | ||
|
|
9a1c722737 | ||
|
|
8a513afc60 | ||
|
|
01d9c505bb | ||
|
|
5ca2c15c75 | ||
|
|
3a290c30c1 | ||
|
|
7fa1914e88 | ||
|
|
aef3dcecd1 | ||
|
|
25771dd347 | ||
|
|
e309da46cb | ||
|
|
e5ace9bba6 | ||
|
|
1a2c0a24e7 | ||
|
|
bd3d68cccc | ||
|
|
668fb69a47 | ||
|
|
bb262d8302 | ||
|
|
bd3b2f6e00 | ||
|
|
58c45612dc | ||
|
|
e9370bb023 | ||
|
|
f175f5f3fa | ||
|
|
a7258fe26d | ||
|
|
a8000f211a | ||
|
|
5bcfbd38be | ||
|
|
720d77361c | ||
|
|
cf7d329915 | ||
|
|
7eb5b3ae17 | ||
|
|
7a1dde6034 | ||
|
|
260149951e | ||
|
|
dc6647c0e9 | ||
|
|
d18088a3b4 | ||
|
|
22b47d7f48 | ||
|
|
e1214a64c9 | ||
|
|
5d29d61890 | ||
|
|
faa8d4722e | ||
|
|
809b035dcc | ||
|
|
50a6f0f5e6 | ||
|
|
e1772ef73a | ||
|
|
4e867b3775 | ||
|
|
3992ad4ade | ||
|
|
d11a84dfd6 | ||
|
|
9cc24c9d92 | ||
|
|
43aeded377 | ||
|
|
c265744b9c | ||
|
|
77f30d6321 | ||
|
|
d596853211 | ||
|
|
640275b8ba | ||
|
|
53fb96c808 | ||
|
|
504c680870 | ||
|
|
f4aa384d1b | ||
|
|
7612567830 | ||
|
|
b727a502d6 | ||
|
|
33467dad7b | ||
|
|
a4bbb96d7a | ||
|
|
0f597b7327 | ||
|
|
b2c1c3958b | ||
|
|
d792fe55f7 | ||
|
|
06ef46ceb5 | ||
|
|
da20a70810 | ||
|
|
e07f0117bd | ||
|
|
36b0a69d3c | ||
|
|
977efeac99 | ||
|
|
e7c121609e | ||
|
|
fecb22116c | ||
|
|
e4d0a0d252 | ||
|
|
92a28606ac | ||
|
|
e3cd399505 | ||
|
|
96fe37e587 | ||
|
|
874a390881 | ||
|
|
781d500246 | ||
|
|
aec930cd21 | ||
|
|
c6d3e48474 | ||
|
|
7afabc99cb | ||
|
|
3ccc915e66 | ||
|
|
b4cab3641a | ||
|
|
20564ba6d9 | ||
|
|
f6c62669cd | ||
|
|
184c7717f6 | ||
|
|
2b57a97d0a | ||
|
|
26612e59c8 | ||
|
|
f646c89315 | ||
|
|
898e61a843 | ||
|
|
16fb81545d | ||
|
|
3146daf162 | ||
|
|
5a2a5ae96c | ||
|
|
0685a35c63 | ||
|
|
f310ed9d82 | ||
|
|
bba5542393 | ||
|
|
c9b529a43c | ||
|
|
81cd55beef | ||
|
|
2b424ca674 | ||
|
|
f089571351 | ||
|
|
811767fa6c | ||
|
|
20c7d07345 | ||
|
|
9a8794183e | ||
|
|
514bc86a71 | ||
|
|
29e8883134 | ||
|
|
2ee1d0eb78 | ||
|
|
2e7ee76ac2 | ||
|
|
6fccf34e6b | ||
|
|
2474303f93 | ||
|
|
2e55f40713 | ||
|
|
479849475e | ||
|
|
7d521de63a | ||
|
|
2dc3d2713c | ||
|
|
59c0a38bb1 | ||
|
|
1be424db50 | ||
|
|
3ae49eb4ec | ||
|
|
175f54869b | ||
|
|
b9387d34d8 | ||
|
|
95265ba1c2 | ||
|
|
0dd3985f16 | ||
|
|
f9dbdb88c9 | ||
|
|
e2f6bbafaa | ||
|
|
469a1cc05e | ||
|
|
4008142018 | ||
|
|
56333d451e | ||
|
|
21c76b58ba | ||
|
|
a5bdf8a881 | ||
|
|
bedb6777c1 | ||
|
|
6a7511f9af | ||
|
|
78fb312013 | ||
|
|
90e1826c7e | ||
|
|
02968a46ff | ||
|
|
33ee657a7f | ||
|
|
b87a9fa35b | ||
|
|
827e65a5cb | ||
|
|
54321de136 | ||
|
|
6ee313a835 | ||
|
|
c5f1c64be3 | ||
|
|
848fc1d6df | ||
|
|
65ff52aadf | ||
|
|
032d5660f3 | ||
|
|
dd45d9666f | ||
|
|
9ae79d1385 | ||
|
|
cff61f2404 | ||
|
|
7a8dc78593 | ||
|
|
eee42e4448 | ||
|
|
53be491e9f | ||
|
|
5a8ba81b05 | ||
|
|
06fc222e53 | ||
|
|
32a5c97d5a | ||
|
|
86d5b3a293 | ||
|
|
c7b2d6b6e5 | ||
|
|
9f8bacdb5e | ||
|
|
927f91c9fe | ||
|
|
6ea5654438 | ||
|
|
d4ba7ee92c | ||
|
|
ff9608fc22 | ||
|
|
d21a13b915 | ||
|
|
f0595a4799 | ||
|
|
c7a54aeac0 | ||
|
|
1a855ae1ba | ||
|
|
03522aaeb2 | ||
|
|
f93919f54a | ||
|
|
7d50f43217 | ||
|
|
1f786273e6 | ||
|
|
67042e8770 | ||
|
|
04f1b7ccae | ||
|
|
75a3272036 | ||
|
|
2cfd329eea | ||
|
|
06ff083e48 | ||
|
|
cacdbf65a6 | ||
|
|
b5885a6f33 | ||
|
|
2a71415c65 | ||
|
|
374f6e342a | ||
|
|
9b34ec2295 | ||
|
|
841f426a4c | ||
|
|
2367300c45 | ||
|
|
290b5d63f8 | ||
|
|
b2d548af0e | ||
|
|
23776d9e8f | ||
|
|
74df71078d | ||
|
|
8b1922150b | ||
|
|
8bdd277b0e | ||
|
|
4fc85f3d8f | ||
|
|
01b7dd32e8 | ||
|
|
f5b65f5d86 | ||
|
|
0705e3f663 | ||
|
|
a8c8535f8f | ||
|
|
494855aa26 | ||
|
|
4b48982da1 | ||
|
|
8344634bb6 | ||
|
|
d4b0b0ee9b | ||
|
|
c6ab1dbbae | ||
|
|
20cac294e9 | ||
|
|
f4fdf64b61 | ||
|
|
20daffb630 | ||
|
|
a4f40f7622 | ||
|
|
79a6a57234 | ||
|
|
fb8f6900fa | ||
|
|
d5460361b2 | ||
|
|
419ce18d2c | ||
|
|
4b09a08042 | ||
|
|
ce64fc364d | ||
|
|
be9352ed70 | ||
|
|
2f2368a2d4 | ||
|
|
bbe1709dca | ||
|
|
c1c8ebaa8f | ||
|
|
2350e83f5b | ||
|
|
677e1ec8b1 | ||
|
|
67593e1c9a | ||
|
|
a6844bf725 | ||
|
|
802ecdd4a0 | ||
|
|
36eb9daf1a | ||
|
|
c710b2afb7 | ||
|
|
352f2723a9 | ||
|
|
8bc49af49d | ||
|
|
e55c61ffaf | ||
|
|
fa51593cc6 | ||
|
|
f2804b96db | ||
|
|
29ee3a2ad8 | ||
|
|
0be3e70037 | ||
|
|
e78f105f23 | ||
|
|
1fb7ca159c | ||
|
|
785ffcc7cd | ||
|
|
cfc43b37d4 | ||
|
|
57c254f725 | ||
|
|
efcd164e38 | ||
|
|
b29c83a42f | ||
|
|
d7bdea55de | ||
|
|
824fc3bc24 | ||
|
|
3e373e8a59 | ||
|
|
0253f614b7 | ||
|
|
b26ae516ac | ||
|
|
cf8689e498 | ||
|
|
dc1890b879 | ||
|
|
e3f7759fde | ||
|
|
2fe44b747d | ||
|
|
32ed2c38b2 | ||
|
|
ad524f528f | ||
|
|
237263c446 | ||
|
|
a5d2aa88f3 | ||
|
|
b188f4d33d | ||
|
|
3482e4e740 | ||
|
|
8172ff0775 | ||
|
|
274169f628 | ||
|
|
08d3865fad | ||
|
|
1753c81f9e | ||
|
|
54489ef86f | ||
|
|
0487e4dd8a | ||
|
|
54515bf48a | ||
|
|
9d4c91be2a | ||
|
|
cc9a5e7682 | ||
|
|
b2587e8633 | ||
|
|
6ca0ad84b7 | ||
|
|
66753a868b | ||
|
|
972f9e46ed | ||
|
|
f07bc7b3d5 | ||
|
|
dc4c1cf402 | ||
|
|
e27cd15715 | ||
|
|
d59f49aa54 | ||
|
|
7ad145082b | ||
|
|
fb9a9e97d8 | ||
|
|
5f2633a305 | ||
|
|
e04444c42e | ||
|
|
7da4d6570b | ||
|
|
5946e8bb9d | ||
|
|
46b5fabc63 | ||
|
|
bff9f3ebbc | ||
|
|
ff4adca18a | ||
|
|
954133d64c | ||
|
|
319360ce9f | ||
|
|
7ef6905484 | ||
|
|
52495ba8bc | ||
|
|
514fbbaf52 | ||
|
|
34cf44587c | ||
|
|
57ed90d5b7 | ||
|
|
09dcbf7566 | ||
|
|
c001cbdd62 |
@@ -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
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: CI for Markdown content
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
pull_request:
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Markdown content
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Run markdownlint
|
||||
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
|
||||
with:
|
||||
globs: |
|
||||
lib/elixir/pages/**/*.md
|
||||
README.md
|
||||
+61
-84
@@ -1,15 +1,13 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "lib/**/*.md"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "lib/**/*.md"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
@@ -21,65 +19,64 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: "28.0"
|
||||
- otp_version: "29.0"
|
||||
deterministic: true
|
||||
- otp_version: "28.0"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "28.4"
|
||||
docs: true
|
||||
coverage: true
|
||||
- otp_version: "28.0"
|
||||
otp_latest: true
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- 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
|
||||
runs-on: ubuntu-24.04
|
||||
# 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
- name: Set ERL_COMPILER_OPTIONS
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
|
||||
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
make compile
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
continue-on-error: ${{ matrix.development == true }}
|
||||
|
||||
- name: Elixir test suite
|
||||
run: make test_elixir
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
continue-on-error: ${{ matrix.development == true }}
|
||||
env:
|
||||
COVER: "${{ matrix.coverage }}"
|
||||
- name: "Calculate Coverage"
|
||||
run: make cover | tee "$GITHUB_STEP_SUMMARY"
|
||||
if: "${{ matrix.coverage }}"
|
||||
|
||||
- name: Build docs (ExDoc main)
|
||||
if: ${{ matrix.otp_latest }}
|
||||
if: ${{ matrix.docs }}
|
||||
run: |
|
||||
cd ..
|
||||
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
|
||||
@@ -88,87 +85,67 @@ jobs:
|
||||
cd ../elixir/
|
||||
git fetch --tags
|
||||
DOCS_OPTIONS="--warnings-as-errors" make docs
|
||||
- name: Check reproducible builds
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: |
|
||||
rm -rf .git
|
||||
# Recompile System without .git
|
||||
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
|
||||
taskset 1 make check_reproducible
|
||||
|
||||
- name: "Calculate Coverage"
|
||||
if: ${{ matrix.coverage }}
|
||||
run: make cover | tee "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: "Upload Coverage Artifact"
|
||||
if: "${{ matrix.coverage }}"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: TestCoverage
|
||||
path: cover/*
|
||||
|
||||
- name: Check reproducible builds
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: taskset 1 make check_reproducible
|
||||
|
||||
- name: Check git is not required
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: |
|
||||
rm -rf .git
|
||||
cd lib/elixir
|
||||
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
|
||||
|
||||
test_windows:
|
||||
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
|
||||
name: Windows Server 2022, OTP ${{ matrix.otp_version }}
|
||||
runs-on: windows-2022
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version: ["26.2", "27.3", "28.0"]
|
||||
runs-on: windows-2022
|
||||
otp_version:
|
||||
- "29.0"
|
||||
- "28.1"
|
||||
- "27.3"
|
||||
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
Remove-Item -Recurse -Force '.git'
|
||||
make compile
|
||||
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
|
||||
- name: Elixir test suite
|
||||
run: |
|
||||
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
|
||||
make test_elixir
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
- name: Check POSIX-compliant
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||
|
||||
license_compliance:
|
||||
name: Check Licence Compliance
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Use HTTPS instead of SSH for Git cloning
|
||||
id: git-config
|
||||
shell: bash
|
||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: "Run OSS Review Toolkit"
|
||||
id: ort
|
||||
uses: ./.github/workflows/ort
|
||||
with:
|
||||
upload-reports: true
|
||||
fail-on-violation: true
|
||||
report-formats: "WebApp"
|
||||
version: "${{ github.sha }}"
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,41 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: License Compliance
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
license_compliance:
|
||||
name: Check License Compliance
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Use HTTPS instead of SSH for Git cloning
|
||||
id: git-config
|
||||
shell: bash
|
||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
uses: ./.github/workflows/ort
|
||||
with:
|
||||
upload-reports: true
|
||||
fail-on-violation: true
|
||||
report-formats: "WebApp"
|
||||
version: "${{ github.sha }}"
|
||||
@@ -0,0 +1,41 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Markdown Content
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
paths: &paths-filter
|
||||
- "**/*.md"
|
||||
- .github/workflows/markdown.yml
|
||||
- .markdownlint-cli2.jsonc
|
||||
|
||||
pull_request:
|
||||
paths: *paths-filter
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Markdown content
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
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
|
||||
@@ -62,6 +63,16 @@ runs:
|
||||
# Override Default Evaluator Rules
|
||||
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
|
||||
|
||||
# Add Package Configurations
|
||||
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
|
||||
for FILE in .ort/package-configurations/*.yml; do
|
||||
COMPONENT="$(basename "$FILE")"
|
||||
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
|
||||
sed -i -E \
|
||||
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
|
||||
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
|
||||
done
|
||||
|
||||
# Set Version in SPDX & Config
|
||||
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
|
||||
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
|
||||
@@ -80,7 +91,7 @@ runs:
|
||||
id: ort
|
||||
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
|
||||
with:
|
||||
image: ghcr.io/oss-review-toolkit/ort-minimal:54.0.0
|
||||
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
|
||||
run: >-
|
||||
labels,
|
||||
cache-dependencies,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
name: POSIX Compliance
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: &paths-filter
|
||||
- .github/workflows/posix_compliance.yml
|
||||
- bin/elixir
|
||||
- bin/elixirc
|
||||
- bin/iex
|
||||
|
||||
pull_request:
|
||||
paths: *paths-filter
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
check_posix_compliance:
|
||||
name: Check POSIX compliance
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
|
||||
- name: Run ShellCheck on bin/ dir
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && \
|
||||
echo "bin/elixir is POSIX compliant"
|
||||
|
||||
shellcheck bin/elixirc && \
|
||||
echo "bin/elixirc is POSIX compliant"
|
||||
|
||||
shellcheck bin/iex && \
|
||||
echo "bin/iex is POSIX compliant"
|
||||
+106
-80
@@ -1,16 +1,19 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Release
|
||||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*.*
|
||||
|
||||
tags:
|
||||
- v*
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ELIXIR_OPTS: "--warnings-as-errors"
|
||||
LANG: C.UTF-8
|
||||
@@ -20,64 +23,68 @@ permissions:
|
||||
|
||||
jobs:
|
||||
create_draft_release:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Create draft release
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Create draft release
|
||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# zizmor: ignore[artipacked]
|
||||
if: github.ref_type == 'branch'
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
- 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: "Build Elixir"
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- otp: 26
|
||||
otp_version: "26.0"
|
||||
- otp: 27
|
||||
otp_version: "27.0"
|
||||
|
||||
- otp: 28
|
||||
otp_version: "28.0"
|
||||
build_docs: build_docs
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
- otp: 29
|
||||
otp_version: "29.0"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Build Release"
|
||||
uses: ./.github/workflows/release_pre_built
|
||||
@@ -92,64 +99,65 @@ jobs:
|
||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
|
||||
- name: "Upload linux release artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- name: "Upload Linux release artifacts"
|
||||
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- name: "Upload Windows release artifacts"
|
||||
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: matrix.build_docs
|
||||
with:
|
||||
name: Docs
|
||||
path: Docs.zip*
|
||||
|
||||
sign:
|
||||
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
|
||||
needs: [build]
|
||||
environment: release
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
otp: [26, 27, 28]
|
||||
otp: [27, 28, 29]
|
||||
flavor: [windows, linux]
|
||||
|
||||
env:
|
||||
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
|
||||
|
||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
|
||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: "Download build"
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.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@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@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
|
||||
if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
|
||||
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
# AZURE_TENANT_ID and AZURE_CLIENT_ID should stay the same,
|
||||
# but AZURE_CLIENT_SECRET has expiration date. When it expires go to
|
||||
# App Registrations / <app> / Certificates & secrets,
|
||||
# click (+) New client secret, note the "Value" (not "Secret ID")
|
||||
# and update it:
|
||||
#
|
||||
# $ gh --repo elixir-lang/elixir secret set AZURE_CLIENT_SECRET
|
||||
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
trusted-signing-account-name: trusted-signing-elixir
|
||||
certificate-profile-name: Elixir
|
||||
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
|
||||
files-folder: ${{ github.workspace }}
|
||||
files-folder-filter: exe
|
||||
file-digest: SHA256
|
||||
@@ -173,17 +181,15 @@ jobs:
|
||||
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
|
||||
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
|
||||
|
||||
- name: "Upload linux release artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
path: ${{ env.RELEASE_FILE }}*
|
||||
|
||||
sbom:
|
||||
name: Generate SBoM
|
||||
|
||||
needs: [build, sign]
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
@@ -199,11 +205,13 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Download Build Artifacts"
|
||||
id: download-build-artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -218,7 +226,7 @@ jobs:
|
||||
|
||||
- name: Attest Distribution Assets with SBoM
|
||||
id: attest-sbom
|
||||
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
|
||||
@@ -238,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "SBoM"
|
||||
path: |
|
||||
@@ -256,37 +268,38 @@ jobs:
|
||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
||||
|
||||
- name: "Assemble Distribution Attestations"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "Attestations"
|
||||
path: "attestations/*.sigstore"
|
||||
|
||||
upload-release:
|
||||
name: Upload release
|
||||
needs: [create_draft_release, build, sign, sbom]
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
|
||||
merge-multiple: true
|
||||
|
||||
- name: Upload Pre-built
|
||||
- name: Upload Pre-build
|
||||
shell: bash
|
||||
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 \
|
||||
@@ -301,20 +314,26 @@ jobs:
|
||||
bom.*
|
||||
|
||||
upload-builds-hex-pm:
|
||||
name: Upload builds to hex.pm
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [build, sign]
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency: builds-hex-pm
|
||||
environment: release
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||
OTP_GENERIC_VERSION: "25"
|
||||
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- name: "Check if variables are set up"
|
||||
if: "${{ ! vars.HEX_AWS_REGION }}"
|
||||
run: |
|
||||
echo "Required variables for uploading to hex.pm are not set up, skipping..."
|
||||
exit 1
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -325,10 +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}" \
|
||||
@@ -336,17 +355,17 @@ jobs:
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
||||
|
||||
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
|
||||
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
|
||||
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
|
||||
|
||||
@@ -367,7 +386,9 @@ 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)
|
||||
|
||||
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
|
||||
touch builds.txt
|
||||
@@ -379,7 +400,7 @@ jobs:
|
||||
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
|
||||
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
|
||||
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
|
||||
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
|
||||
sed -i "/^${ref_name} /d" builds.txt
|
||||
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
fi
|
||||
@@ -418,3 +439,8 @@ jobs:
|
||||
for key in $(cat purge_keys.txt); do
|
||||
purge "${key}"
|
||||
done
|
||||
|
||||
env:
|
||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Notify
|
||||
name: Release Notifications
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -15,17 +15,20 @@ jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
name: Notify
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
|
||||
with:
|
||||
otp-version: "27.3"
|
||||
elixir-version: "1.18.3"
|
||||
|
||||
- name: Run Elixir script
|
||||
env:
|
||||
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"
|
||||
@@ -1,45 +1,58 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: "Release pre built"
|
||||
description: "Builds elixir release, ExDoc and generates docs"
|
||||
name: Release Pre-build
|
||||
description: "Builds Elixir release, ExDoc and generates docs"
|
||||
|
||||
inputs:
|
||||
otp:
|
||||
description: "The major OTP version"
|
||||
|
||||
otp_version:
|
||||
description: "The exact OTP version (major.minor[.patch])"
|
||||
|
||||
build_docs:
|
||||
description: "If docs have to be built or not"
|
||||
description: "Whether docs have to be built"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
|
||||
with:
|
||||
otp-version: ${{ inputs.otp_version }}
|
||||
version-type: strict
|
||||
|
||||
- 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
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y nsis
|
||||
|
||||
- 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')
|
||||
@@ -51,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
|
||||
|
||||
+3
-2
@@ -10,9 +10,10 @@
|
||||
/lib/elixir/test/ebin/
|
||||
/man/elixir.1
|
||||
/man/iex.1
|
||||
/Docs-v*.zip
|
||||
/Precompiled-v*.zip
|
||||
/Docs.zip
|
||||
/Precompiled.zip
|
||||
/.eunit
|
||||
.elixir.plt
|
||||
erl_crash.dump
|
||||
/cover/
|
||||
.tool-versions
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
{
|
||||
"globs": [
|
||||
"**/*.md"
|
||||
],
|
||||
"ignores": [
|
||||
".git/**"
|
||||
],
|
||||
"gitignore": true,
|
||||
"config": {
|
||||
// Consecutive header levels (h1 -> h2 -> h3).
|
||||
"MD001": false,
|
||||
// Header style. We use #s.
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
// Style of unordered lists..
|
||||
"MD007": {
|
||||
"indent": 2,
|
||||
"start_indented": true
|
||||
},
|
||||
// Line length. Who cares.
|
||||
"MD013": false,
|
||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
||||
// don't show output. We use those a lot, so this is fine for us.
|
||||
"MD014": false,
|
||||
// Multiple headings with the same content.
|
||||
"MD024": {
|
||||
// Duplication is allowed for headings with different parents.
|
||||
"siblings_only": true
|
||||
},
|
||||
// Trailing punctuation in heading.
|
||||
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
|
||||
// the default values.
|
||||
"MD026": {
|
||||
"punctuation": ".,;:。,;:!"
|
||||
},
|
||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
||||
"MD028": false,
|
||||
// Allowed HTML inline elements.
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"h1",
|
||||
"a",
|
||||
"br",
|
||||
"img",
|
||||
"picture",
|
||||
"source",
|
||||
"noscript",
|
||||
"p",
|
||||
"script"
|
||||
]
|
||||
},
|
||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
||||
"MD038": false,
|
||||
// Code block style. We don't care if it's fenced or indented.
|
||||
"MD046": false,
|
||||
// Our tables are too large to align.
|
||||
"MD060": false
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
{
|
||||
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
|
||||
"MD001": false,
|
||||
// Header style. We use #s.
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
// Style of unordered lists..
|
||||
"MD007": {
|
||||
"indent": 2,
|
||||
"start_indented": true
|
||||
},
|
||||
// Line length. Who cares.
|
||||
"MD013": false,
|
||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
||||
// don't show output. We use those a lot, so this is fine for us.
|
||||
"MD014": false,
|
||||
// Multiple headings with the same content. That's fine.
|
||||
"MD024": false,
|
||||
// Some headers finish with ! because it refers to a function name
|
||||
"MD026": false,
|
||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
||||
"MD028": false,
|
||||
// Allowed HTML inline elements.
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"h1",
|
||||
"a",
|
||||
"br",
|
||||
"img",
|
||||
"picture",
|
||||
"source",
|
||||
"noscript",
|
||||
"p",
|
||||
"script"
|
||||
]
|
||||
},
|
||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
||||
"MD038": false,
|
||||
// Code block style. We don't care if it's fenced or indented.
|
||||
"MD046": false
|
||||
}
|
||||
@@ -3,18 +3,6 @@
|
||||
|
||||
excludes:
|
||||
paths:
|
||||
- pattern: "lib/elixir/pages/**/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
- pattern: "lib/elixir/scripts/**/*"
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Build Tool"
|
||||
- pattern: "lib/ex_unit/examples/**/*"
|
||||
reason: "EXAMPLE_OF"
|
||||
comment: "Example"
|
||||
- pattern: "lib/*/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
- pattern: "man/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
@@ -25,8 +13,64 @@ excludes:
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Documentation"
|
||||
|
||||
# Unfortunately we'll have to repeat all package level excludes here
|
||||
# Make sure to keep them in sync with the package configuration in
|
||||
# .ort/package-configurations
|
||||
- pattern: "lib/*/pages/**/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
- pattern: "lib/*/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
- pattern: "lib/*/scripts/**/*"
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Build Tool"
|
||||
- pattern: "lib/*/examples/**/*"
|
||||
reason: "EXAMPLE_OF"
|
||||
comment: "Example"
|
||||
|
||||
curations:
|
||||
license_findings:
|
||||
# Version File
|
||||
- path: "VERSION"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to VERSION file"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Wrongly Identified
|
||||
- path: ".gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".gitattributes"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "CONTRIBUTING.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified TSL license"
|
||||
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "OPEN_SOURCE_POLICY.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified NOASSERTION"
|
||||
detected_license: "NOASSERTION"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Unfortunately we'll have to repeat all package level license curations here
|
||||
# Make sure to keep them in sync with the package configuration in
|
||||
# .ort/package-configurations
|
||||
|
||||
# Test Fixtures
|
||||
- path: "lib/*/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Logos
|
||||
- path: "lib/elixir/pages/images/logo.png"
|
||||
reason: "NOT_DETECTED"
|
||||
@@ -39,13 +83,6 @@ curations:
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
||||
|
||||
# Version File
|
||||
- path: "VERSION"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to VERSION file"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Documentation Images
|
||||
- path: "lib/elixir/pages/images/**/*.png"
|
||||
reason: "NOT_DETECTED"
|
||||
@@ -54,26 +91,11 @@ curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Test Fixtures
|
||||
- path: "lib/eex/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/elixir/test/elixir/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/ex_unit/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/mix/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Unicode
|
||||
- path: "lib/elixir/unicode/*.txt"
|
||||
@@ -89,57 +111,8 @@ curations:
|
||||
The guide mentions multiple licenses for users to choose from.
|
||||
It however is not licensed itself by the mentioned licenses.
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".gitattributes"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/elixir/scripts/windows_installer/.gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "CONTRIBUTING.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified TSL license"
|
||||
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "OPEN_SOURCE_POLICY.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified NOASSERTION"
|
||||
detected_license: "NOASSERTION"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
packages:
|
||||
- id: "SpdxDocumentFile:The Elixir Team:elixir-lang:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
|
||||
- id: "SpdxDocumentFile:The Elixir Team:eex:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:elixir:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:exunit:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:iex:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:logger:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:mix:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
ort:
|
||||
enableRepositoryPackageCurations: true
|
||||
enableRepositoryPackageConfigurations: true
|
||||
|
||||
scanner:
|
||||
skipConcluded: false
|
||||
@@ -11,4 +12,10 @@ ort:
|
||||
analyzer:
|
||||
allowDynamicVersions: true
|
||||
enabledPackageManagers: [SpdxDocumentFile]
|
||||
skipExcluded: true
|
||||
|
||||
reporter:
|
||||
reporters:
|
||||
SpdxDocument:
|
||||
options:
|
||||
creationInfoOrganization: The Elixir Team
|
||||
documentName: "Elixir Source SPDX Document"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:eex:"
|
||||
path_excludes:
|
||||
- pattern: "lib/eex/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Test Fixtures
|
||||
- path: "lib/eex/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
@@ -0,0 +1,60 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:elixir:"
|
||||
path_excludes:
|
||||
- pattern: "lib/elixir/pages/**/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
- pattern: "lib/elixir/scripts/**/*"
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Build Tool"
|
||||
- pattern: "lib/elixir/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Logos
|
||||
- path: "lib/elixir/pages/images/logo.png"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to Elixir Logo"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
||||
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to Elixir Logo"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
||||
|
||||
# Documentation Images
|
||||
- path: "lib/elixir/pages/images/**/*.png"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to all images"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Test Fixtures
|
||||
- path: "lib/elixir/test/elixir/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Unicode
|
||||
- path: "lib/elixir/unicode/*.txt"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to unicode files"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-scancode-unicode"
|
||||
|
||||
# Wrongly Identified
|
||||
- path: "lib/elixir/pages/references/library-guidelines.md"
|
||||
reason: "INCORRECT"
|
||||
comment: |
|
||||
The guide mentions multiple licenses for users to choose from.
|
||||
It however is not licensed itself by the mentioned licenses.
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/elixir/scripts/windows_installer/.gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
@@ -0,0 +1,18 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:exunit:"
|
||||
path_excludes:
|
||||
- pattern: "lib/ex_unit/examples/**/*"
|
||||
reason: "EXAMPLE_OF"
|
||||
comment: "Example"
|
||||
- pattern: "lib/ex_unit/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Test Fixtures
|
||||
- path: "lib/ex_unit/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:logger:"
|
||||
path_excludes:
|
||||
- pattern: "lib/logger/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:mix:"
|
||||
path_excludes:
|
||||
- pattern: "lib/mix/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Test Fixtures
|
||||
- path: "lib/mix/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
+241
-228
@@ -4,13 +4,55 @@
|
||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
-->
|
||||
|
||||
# Changelog for Elixir v1.19
|
||||
# Changelog for Elixir v1.20
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
## Type system improvements
|
||||
|
||||
### More type inference
|
||||
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.
|
||||
|
||||
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
|
||||
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
|
||||
|
||||
### Type inference of guards
|
||||
|
||||
This release also performs inference of guards! Let's see some examples:
|
||||
|
||||
```elixir
|
||||
def example(x, y) when is_list(x) and is_integer(y)
|
||||
```
|
||||
|
||||
The code above correctly infers `x` is a list and `y` is an integer.
|
||||
|
||||
```elixir
|
||||
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
|
||||
```
|
||||
|
||||
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
|
||||
|
||||
```elixir
|
||||
def example(x) when is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
|
||||
|
||||
```elixir
|
||||
def example(x) when not is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
|
||||
|
||||
You can also have expressions that assert on the size of data structures:
|
||||
|
||||
```elixir
|
||||
def example(x) when tuple_size(x) < 3
|
||||
```
|
||||
|
||||
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
|
||||
|
||||
### Whole-body type inference
|
||||
|
||||
Elixir also performs inference based on the function body itself. Take the following code:
|
||||
|
||||
```elixir
|
||||
def add_foo_and_bar(data) do
|
||||
@@ -28,296 +70,267 @@ def sum_to_string(a, b) do
|
||||
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.
|
||||
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.
|
||||
|
||||
### Type checking of protocol dispatch and implementations
|
||||
### Typing across clauses
|
||||
|
||||
This release also adds type checking when dispatching and implementing protocols.
|
||||
|
||||
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
|
||||
|
||||
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
|
||||
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def my_code(first..last//step = range) do
|
||||
"hello #{range}"
|
||||
case System.get_env("SOME_VAR") do
|
||||
nil -> :not_found
|
||||
value -> {:ok, String.upcase(value)}
|
||||
end
|
||||
```
|
||||
|
||||
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
|
||||
|
||||
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
|
||||
|
||||
### Typing of atom and domain keys in maps
|
||||
|
||||
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
|
||||
|
||||
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
|
||||
|
||||
```elixir
|
||||
%{123 => "hello", 456.0 => :ok}
|
||||
```
|
||||
|
||||
will have the type:
|
||||
|
||||
```elixir
|
||||
%{integer() => binary(), float() => :ok}
|
||||
```
|
||||
|
||||
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
|
||||
|
||||
```elixir
|
||||
%{integer() => integer(), root: integer()}
|
||||
```
|
||||
|
||||
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
|
||||
|
||||
### Typing of map operations
|
||||
|
||||
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
|
||||
|
||||
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
|
||||
|
||||
```elixir
|
||||
Map.put(map, :key, 123)
|
||||
#=> returns type %{..., key: integer()}
|
||||
|
||||
Map.delete(map, :key)
|
||||
#=> returns type %{..., key: not_set()}
|
||||
```
|
||||
|
||||
As you can see, we track when keys are set and also when they are removed.
|
||||
|
||||
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
|
||||
|
||||
```elixir
|
||||
Map.replace(map, :key, 123)
|
||||
#=> returns type %{..., key: if_set(integer())}
|
||||
```
|
||||
|
||||
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
|
||||
|
||||
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
def name(map), do: Map.fetch!(map, :name)
|
||||
end
|
||||
|
||||
defmodule CallsUser do
|
||||
def calls_name do
|
||||
User.name(%{})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
the above emits the following warnings:
|
||||
The code above has a type violation, which is now caught by the type system:
|
||||
|
||||
```
|
||||
warning: incompatible value given to string interpolation:
|
||||
```text
|
||||
warning: incompatible types given to User.name/1:
|
||||
|
||||
data
|
||||
|
||||
it has type:
|
||||
|
||||
%Range{first: term(), last: term(), step: term()}
|
||||
|
||||
but expected a type that implements the String.Chars protocol, it must be one of:
|
||||
|
||||
dynamic(
|
||||
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
|
||||
%Version.Requirement{}
|
||||
) or atom() or binary() or float() or integer() or list(term())
|
||||
```
|
||||
|
||||
Warnings are also emitted if you pass a data type that does not implement the `Enumerable` protocol as a generator to for-comprehensions:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def my_code(%Date{} = date) do
|
||||
for(x <- date, do: x)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
will emit:
|
||||
|
||||
```
|
||||
warning: incompatible value given to for-comprehension:
|
||||
|
||||
x <- date
|
||||
|
||||
it has type:
|
||||
|
||||
%Date{year: term(), month: term(), day: term(), calendar: term()}
|
||||
|
||||
but expected a type that implements the Enumerable protocol, it must be one of:
|
||||
|
||||
dynamic(
|
||||
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
|
||||
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
|
||||
) or fun() or list(term()) or non_struct_map()
|
||||
```
|
||||
|
||||
### Type checking and inference of anonymous functions
|
||||
|
||||
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def run do
|
||||
fun = fn %{} -> :map end
|
||||
fun.("hello")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
|
||||
|
||||
```
|
||||
warning: incompatible types given on function application:
|
||||
|
||||
fun.("hello")
|
||||
User.name(%{})
|
||||
|
||||
given types:
|
||||
|
||||
binary()
|
||||
%{name: not_set()}
|
||||
|
||||
but function has type:
|
||||
but expected one of:
|
||||
|
||||
(dynamic(map()) -> :map)
|
||||
dynamic(%{..., name: term()})
|
||||
|
||||
typing violation found at:
|
||||
type warning found at:
|
||||
│
|
||||
6 │ fun.("hello")
|
||||
│ ~
|
||||
16 │ User.name(%{})
|
||||
│ ~
|
||||
│
|
||||
└─ mod.exs:6:8: Example.run/0
|
||||
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
|
||||
```
|
||||
|
||||
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
|
||||
|
||||
### 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/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
|
||||
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/).
|
||||
|
||||
## Faster compile times in large projects
|
||||
## Compile-time improvements
|
||||
|
||||
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
|
||||
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
|
||||
|
||||
While Elixir has always compiled the given files in project or a dependency in parallel, the compiler would sometimes be unable to use all of the machine resources efficiently. This release addresses two common limitations, delivering performance improvements that scale with codebase size and available CPU 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:
|
||||
|
||||
### Code loading bottlenecks
|
||||
* Errors during compilation may have less precise stacktraces
|
||||
|
||||
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
|
||||
* 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
|
||||
|
||||
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
|
||||
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
|
||||
|
||||
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
|
||||
## v1.20.0 (2026-06-03)
|
||||
|
||||
The only potential regression in this approach happens if you have a module, which is used at compile time and defines an `@on_load` callback (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invokes another modules within the same project. For example:
|
||||
|
||||
```elixir
|
||||
defmodule MyLib.SomeModule do
|
||||
@on_load :init
|
||||
|
||||
def init do
|
||||
MyLib.AnotherModule.do_something()
|
||||
end
|
||||
|
||||
def something_else do
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
MyLib.SomeModule.something_else()
|
||||
```
|
||||
|
||||
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
|
||||
|
||||
### Parallel compilation of dependencies
|
||||
|
||||
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
|
||||
|
||||
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
|
||||
|
||||
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
|
||||
|
||||
## Improved pretty printing algorithm
|
||||
|
||||
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2, 3],
|
||||
[1, 2, ...],
|
||||
[1, ...],
|
||||
[...],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on actually filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2, 3],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
The outer list is the first element, the first nested list is the second, followed by three numbers, reaching the limit. This gives developers more precise control over pretty printing.
|
||||
|
||||
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
|
||||
|
||||
## OpenChain certification
|
||||
|
||||
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
|
||||
|
||||
* Elixir releases now include a Source SBoM in CycloneDX 1.6 or later and SPDX 2.3 or later formats.
|
||||
* Each release is attested along with the Source SBoM.
|
||||
|
||||
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
|
||||
|
||||
This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
|
||||
|
||||
## v1.19.0-dev
|
||||
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
|
||||
|
||||
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
|
||||
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
|
||||
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
|
||||
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
|
||||
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
|
||||
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
|
||||
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
|
||||
* [Code.Fragment] Add `Code.Fragment.lines/1`
|
||||
* [Enum] Provide more information on `Enum.OutOfBoundsError`
|
||||
* [Inspect] Allow `optional: :all` when deriving Inspect
|
||||
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
|
||||
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
|
||||
* [Kernel] Allow controlling which applications are used during inference
|
||||
* [Kernel] Support `min/2` and `max/2` as guards
|
||||
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
|
||||
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
|
||||
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
|
||||
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
|
||||
* [Protocol] Type checking of protocols dispatch and implementations
|
||||
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
|
||||
* [String] Add `String.count/2` to count occurrences of a pattern
|
||||
* [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.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
|
||||
* [ExUnit.Case] Add `:test_group` to the test context
|
||||
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
|
||||
* [ExUnit.Doctest] Add `:inspect_opts` option for doctest
|
||||
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
|
||||
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
|
||||
* [IEx] Optimize autocompleting modules
|
||||
* [IEx.Helpers] Add `source/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
|
||||
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
|
||||
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
|
||||
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
|
||||
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
|
||||
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
|
||||
* [mix xref graph] Add support for `--format json`
|
||||
* [mix xref graph] Emit a warning if `--source` is part of a cycle
|
||||
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
|
||||
* [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. Bug fixes
|
||||
### 2. Potential breaking changes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
|
||||
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
|
||||
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
|
||||
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
|
||||
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
|
||||
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
|
||||
* [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
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix cmd] Preserve argument quoting in subcommands
|
||||
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
|
||||
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
|
||||
* [mix test] Preserve files with no longer filter on `mix test`
|
||||
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
### 3. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
|
||||
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
|
||||
* [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 compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
|
||||
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
|
||||
* [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
|
||||
|
||||
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
|
||||
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}` is deprecated in favor of pattern matching on the struct when the variable is defined and then using the map update syntax `%{uri | path: "/foo/bar"}`. Thanks to the type system, pattern matching on structs can find more errors, more reliably
|
||||
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
|
||||
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
|
||||
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
|
||||
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
|
||||
* [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] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
|
||||
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
|
||||
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
|
||||
|
||||
## v1.18
|
||||
## v1.19
|
||||
|
||||
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
|
||||
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).
|
||||
|
||||
+5
-5
@@ -6,7 +6,7 @@
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
Contact: elixir-lang-conduct@googlegroups.com
|
||||
Contact: <elixir-lang-conduct@googlegroups.com>
|
||||
|
||||
## Why have a Code of Conduct?
|
||||
|
||||
@@ -51,15 +51,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
|
||||
|
||||
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
|
||||
|
||||
* The [official GitHub projects][1] and code reviews.
|
||||
* The official elixir-lang mailing lists.
|
||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||
* The [official GitHub projects][1] and code reviews.
|
||||
* The official elixir-lang mailing lists.
|
||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||
|
||||
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
|
||||
|
||||
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||
|
||||
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
|
||||
|
||||
|
||||
+96
-45
@@ -78,6 +78,7 @@ introduced behavior, especially for bug fixes and major changes:
|
||||
*fails* before your change and *passes* afterward. This makes it easier to
|
||||
confirm that the fix addresses the underlying issue and helps prevent
|
||||
regressions in the future.
|
||||
|
||||
* **New Features or Major Changes:** If you are adding a new feature or making
|
||||
major changes to existing functionality, please add tests that cover the
|
||||
major parts of that functionality. Aim to have the best code coverage possible.
|
||||
@@ -88,7 +89,9 @@ We have saved some excellent pull requests we have received in the past in
|
||||
case you are looking for some examples:
|
||||
|
||||
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
|
||||
|
||||
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
|
||||
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
|
||||
## Reviewing changes
|
||||
@@ -96,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
|
||||
@@ -115,36 +114,63 @@ 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
|
||||
guidelines on licensing and compliance. Below is a summary of the key points
|
||||
affecting **all external contributors**:
|
||||
|
||||
- Accepted Licenses: Any code contributed must be licensed under the
|
||||
`Apache-2.0` license.
|
||||
- SPDX License Headers: With the exception of approved test fixture files,
|
||||
all new or modified files in a pull request must include correct SPDX
|
||||
headers. If you are creating a new file under the `Apache-2.0` license, for
|
||||
instance, please use:
|
||||
|
||||
* Accepted Licenses: Any code contributed must be licensed under the
|
||||
`Apache-2.0` license.
|
||||
|
||||
* SPDX License Headers: With the exception of approved test fixture files,
|
||||
all new or modified files in a pull request must include correct SPDX
|
||||
headers. If you are creating a new file under the `Apache-2.0` license, for
|
||||
instance, please use:
|
||||
|
||||
```elixir
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
```
|
||||
|
||||
- No Executable Binaries: Contributions must **not** include any executable
|
||||
binary files. If you require an exception (for example, certain test artifacts),
|
||||
please see the policy on how to request approval and document exceptions.
|
||||
- Preserving Copyright and License Info: If you copy code from elsewhere,
|
||||
ensure that **all original copyright and license notices remain intact**. If
|
||||
they are missing or incomplete, you must add them.
|
||||
- Failure to Comply: Pull requests that do not meet these licensing and
|
||||
compliance standards will be rejected or require modifications before merging.
|
||||
- Developer Certificate of Origin: All contributions are subject to the
|
||||
Developer Certificate of Origin.
|
||||
|
||||
```
|
||||
* No Executable Binaries: Contributions must **not** include any executable
|
||||
binary files. If you require an exception (for example, certain test artifacts),
|
||||
please see the policy on how to request approval and document exceptions.
|
||||
|
||||
* Preserving Copyright and License Info: If you copy code from elsewhere,
|
||||
ensure that **all original copyright and license notices remain intact**. If
|
||||
they are missing or incomplete, you must add them.
|
||||
|
||||
* Failure to Comply: Pull requests that do not meet these licensing and
|
||||
compliance standards will be rejected or require modifications before merging.
|
||||
|
||||
* Developer Certificate of Origin: All contributions are subject to the
|
||||
Developer Certificate of Origin.
|
||||
|
||||
```text
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
@@ -171,27 +197,52 @@ affecting **all external contributors**:
|
||||
involved.
|
||||
```
|
||||
|
||||
See http://developercertificate.org/ for a copy of the Developer Certificate
|
||||
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.
|
||||
|
||||
```sh
|
||||
# After cloning and compiling Elixir, in its parent directory:
|
||||
git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
|
||||
```
|
||||
That said, here are examples of how one might (or might not) use AI and coding
|
||||
agents in Elixir spaces:
|
||||
|
||||
Now go back to Elixir's root directory and run:
|
||||
* 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.
|
||||
|
||||
```sh
|
||||
make docs # to generate HTML pages
|
||||
make docs DOCS_FORMAT=epub # to generate EPUB documents
|
||||
```
|
||||
* Do not use coding agents to tackle existing issues unless they have the
|
||||
"Contributions Welcome" label.
|
||||
|
||||
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).
|
||||
* 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.
|
||||
|
||||
* 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
|
||||
@@ -107,8 +107,10 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
|
||||
fi
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
|
||||
|
||||
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
|
||||
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP)
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
|
||||
unicode: $(UNICODE)
|
||||
|
||||
+10
-13
@@ -20,7 +20,7 @@ ensuring that Elixir remains a trusted and innovative open source project.
|
||||
## 2. Scope
|
||||
|
||||
This policy applies to the Elixir Programming language, located at
|
||||
https://github.com/elixir-lang/elixir. It covers every file, and contribution
|
||||
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution
|
||||
made, including documentation and any associated assets.
|
||||
|
||||
## 3. Licensing
|
||||
@@ -29,18 +29,19 @@ All code released by the Elixir team is licensed under the
|
||||
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
|
||||
licenses are recognized as permissible in this project:
|
||||
|
||||
- The Unicode license, as documented at
|
||||
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
|
||||
- The Elixir Trademark Policy, as documented at
|
||||
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
|
||||
- The Unicode license, as documented at
|
||||
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
|
||||
|
||||
- The Elixir Trademark Policy, as documented at
|
||||
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
|
||||
|
||||
These licenses are considered acceptable for any files or code that form part of
|
||||
an Elixir repository. If a contribution requires a different license, it must
|
||||
either be rejected or prompt an update to this policy.
|
||||
|
||||
## 4. Contributing to Elixir Projects
|
||||
## 4. Contributing to the Elixir repository
|
||||
|
||||
Any code contributed to Elixir repositories must fall under one of the accepted
|
||||
Any code contributed to the Elixir repository must fall under one of the accepted
|
||||
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
|
||||
other license will be rejected unless this policy is formally revised to include
|
||||
that license. All files except those specifically exempted (e.g., certain test
|
||||
@@ -51,13 +52,9 @@ configuration and undergo review.
|
||||
|
||||
Contributions must not introduce executable binary files into the codebase.
|
||||
|
||||
Every Elixir project within the organization will have an automated GitHub
|
||||
Action to enforce these rules. This mechanism aids in detecting non-compliant
|
||||
licenses or files early in the review process.
|
||||
|
||||
## 5. Preservation of Copyright and License Information
|
||||
|
||||
Any third-party code incorporated into Elixir projects must retain original
|
||||
Any third-party code incorporated into the Elixir repository must retain original
|
||||
copyright and license headers. If no such headers exist in the source, they must
|
||||
be added. This practice ensures that original authors receive proper credit and
|
||||
that the licensing lineage is preserved.
|
||||
@@ -165,4 +162,4 @@ necessary, by the EEF CISO. Any significant changes will be communicated to
|
||||
contributors and made publicly available.
|
||||
|
||||
*Effective Date: 2025-02-20*
|
||||
*Last Reviewed: 2025-02-20*
|
||||
*Last Reviewed: 2025-11-20*
|
||||
|
||||
@@ -59,7 +59,7 @@ Our *actionable item policy* has some important consequences, such as:
|
||||
comment and we can always reopen the issue.
|
||||
|
||||
By keeping the overall issues tracker tidy and organized, the community
|
||||
can easily peak at what is coming in new releases and also get involved
|
||||
can easily peek at what is coming in new releases and also get involved
|
||||
by commenting on existing issues and submitting pull requests. Please
|
||||
remember to keep the tone positive and be kind! For more information,
|
||||
see the [Code of Conduct][1].
|
||||
@@ -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
|
||||
|
||||
|
||||
+4
-10
@@ -8,15 +8,9 @@
|
||||
|
||||
## Shipping a new version
|
||||
|
||||
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
|
||||
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
|
||||
|
||||
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
|
||||
- If this release addresses any publicly known security vulnerabilities with
|
||||
assigned CVEs, add a "Security" section to `CHANGELOG.md`. For example:
|
||||
```md
|
||||
## Security
|
||||
- Fixed CVE-2025-00000: Description of the vulnerability
|
||||
```
|
||||
|
||||
3. Update "Compatibility and Deprecations" if a new OTP version is supported
|
||||
|
||||
@@ -30,11 +24,11 @@
|
||||
|
||||
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
|
||||
|
||||
## Creating a new vMAJOR.MINOR branch (before first rc)
|
||||
## Creating a new vMAJOR.MINOR branch (usually before first rc)
|
||||
|
||||
### In the new branch
|
||||
|
||||
1. Comment the `CANONICAL=` in /Makefile
|
||||
1. Comment out `CANONICAL := main/` in /Makefile
|
||||
|
||||
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
|
||||
|
||||
@@ -42,7 +36,7 @@
|
||||
|
||||
### Back in main
|
||||
|
||||
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
|
||||
1. Bump /VERSION file, bin/elixir, and bin/elixir.bat
|
||||
|
||||
2. Start new /CHANGELOG.md
|
||||
|
||||
|
||||
+4
-5
@@ -12,16 +12,15 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.19 | Development
|
||||
1.18 | 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
|
||||
1.14 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.19.0-dev
|
||||
ELIXIR_VERSION=1.20.0
|
||||
|
||||
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.19.0-dev
|
||||
set ELIXIR_VERSION=1.20.0
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+23
-6
@@ -118,6 +118,19 @@ defmodule EEx do
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
|
||||
| {:eof, metadata}
|
||||
|
||||
@type tokenize_opt ::
|
||||
{:file, binary()}
|
||||
| {:line, line}
|
||||
| {:column, column}
|
||||
| {:indentation, non_neg_integer}
|
||||
| {:trim, boolean()}
|
||||
|
||||
@type compile_opt ::
|
||||
tokenize_opt
|
||||
| {:engine, module()}
|
||||
| {:parser_options, Code.parser_opts()}
|
||||
| {atom(), term()}
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the given string.
|
||||
|
||||
@@ -128,6 +141,7 @@ defmodule EEx do
|
||||
template.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-options).
|
||||
Additional options are passed to the underlying engine.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -220,9 +234,11 @@ defmodule EEx do
|
||||
"3"
|
||||
|
||||
"""
|
||||
@spec compile_string(String.t(), keyword) :: Macro.t()
|
||||
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
|
||||
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
||||
case tokenize(source, options) do
|
||||
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
|
||||
|
||||
case tokenize(source, tokenize_opts) do
|
||||
{:ok, tokens} ->
|
||||
EEx.Compiler.compile(tokens, source, options)
|
||||
|
||||
@@ -259,7 +275,7 @@ defmodule EEx do
|
||||
#=> "3"
|
||||
|
||||
"""
|
||||
@spec compile_file(Path.t(), keyword) :: Macro.t()
|
||||
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
|
||||
def compile_file(filename, options \\ []) when is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
options = Keyword.merge([file: filename, line: 1], options)
|
||||
@@ -277,7 +293,7 @@ defmodule EEx do
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), keyword, keyword) :: String.t()
|
||||
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
|
||||
def eval_string(source, bindings \\ [], options \\ [])
|
||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||
compiled = compile_string(source, options)
|
||||
@@ -299,7 +315,7 @@ defmodule EEx do
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
|
||||
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
|
||||
def eval_file(filename, bindings \\ [], options \\ [])
|
||||
when is_list(bindings) and is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
@@ -339,7 +355,7 @@ defmodule EEx do
|
||||
Note new tokens may be added in the future.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec tokenize([char()] | String.t(), opts :: keyword) ::
|
||||
@spec tokenize([char()] | String.t(), [tokenize_opt]) ::
|
||||
{:ok, [token()]} | {:error, String.t(), metadata()}
|
||||
def tokenize(contents, opts \\ []) do
|
||||
EEx.Compiler.tokenize(contents, opts)
|
||||
@@ -348,6 +364,7 @@ defmodule EEx do
|
||||
### Helpers
|
||||
|
||||
defp do_eval(compiled, bindings, options) do
|
||||
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
|
||||
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
||||
result
|
||||
end
|
||||
|
||||
+27
-14
@@ -96,7 +96,7 @@ defmodule EEx.Compiler do
|
||||
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
|
||||
"please remove \"#{marker}\""
|
||||
|
||||
:elixir_errors.erl_warn({line, column}, state.file, message)
|
||||
IO.warn(message, file: state.file, line: line, column: column)
|
||||
~c""
|
||||
else
|
||||
marker
|
||||
@@ -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,17 +366,17 @@ 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
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
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
|
||||
|
||||
@@ -17,6 +17,10 @@ defmodule EEx.Engine do
|
||||
@doc """
|
||||
Called at the beginning of every template.
|
||||
|
||||
It receives the options during compilation, including the
|
||||
ones managed by EEx, such as `:line` and `:file`, as well
|
||||
as custom engine options.
|
||||
|
||||
It must return the initial state.
|
||||
"""
|
||||
@callback init(opts :: keyword) :: state
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -873,11 +873,6 @@ defmodule Access do
|
||||
...> end)
|
||||
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
||||
|
||||
An error is raised if the predicate is not a function or is of the incorrect arity:
|
||||
|
||||
iex> get_in([], [Access.filter(5)])
|
||||
** (FunctionClauseError) no function clause matching in Access.filter/1
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
|
||||
@@ -891,7 +886,7 @@ defmodule Access do
|
||||
end
|
||||
|
||||
defp filter(:get, data, func, next) when is_list(data) do
|
||||
data |> Enum.filter(func) |> Enum.map(next)
|
||||
for elem <- data, func.(elem), do: next.(elem)
|
||||
end
|
||||
|
||||
defp filter(:get_and_update, data, func, next) when is_list(data) do
|
||||
@@ -1096,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
|
||||
@@ -1154,11 +1154,6 @@ defmodule Access do
|
||||
...> end)
|
||||
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
||||
|
||||
An error is raised if the predicate is not a function or is of the incorrect arity:
|
||||
|
||||
iex> get_in([], [Access.find(5)])
|
||||
** (FunctionClauseError) no function clause matching in Access.find/1
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
|
||||
|
||||
@@ -507,7 +507,7 @@ defmodule Application do
|
||||
of all loaded applications. Returns `nil` if
|
||||
the module is not listed in any application spec.
|
||||
"""
|
||||
@spec get_application(atom) :: atom | nil
|
||||
@spec get_application(module) :: app | nil
|
||||
def get_application(module) when is_atom(module) do
|
||||
case :application.get_application(module) do
|
||||
{:ok, app} -> app
|
||||
@@ -680,13 +680,13 @@ defmodule Application do
|
||||
## Examples
|
||||
|
||||
`get_env/3` is commonly used to read the configuration of your OTP applications.
|
||||
Since Mix configurations are commonly used to configure applications, we will use
|
||||
this as a point of illustration.
|
||||
Since Mix configurations are commonly used to configure applications (including
|
||||
your dependencies), we will use this as a point of illustration.
|
||||
|
||||
Consider a new application `:my_app`. `:my_app` contains a database engine which
|
||||
supports a pool of databases. The database engine needs to know the configuration for
|
||||
each of those databases, and that configuration is supplied by key-value pairs in
|
||||
environment of `:my_app`.
|
||||
environment of `:my_app`. For example, your `config/runtime.exs` file might have:
|
||||
|
||||
config :my_app, Databases.RepoOne,
|
||||
# A database configuration
|
||||
@@ -696,7 +696,7 @@ defmodule Application do
|
||||
config :my_app, Databases.RepoTwo,
|
||||
# Another database configuration (for the same OTP app)
|
||||
ip: "localhost",
|
||||
port: 20717
|
||||
port: 20_717
|
||||
|
||||
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
|
||||
|
||||
@@ -714,6 +714,11 @@ defmodule Application do
|
||||
config = Application.get_env(:my_app, Databases.RepoOne)
|
||||
config[:ip]
|
||||
|
||||
The sample `config/runtime.exs` above could be used both for `:my_app` to
|
||||
configure itself but also to allow any application that depends on `:my_app`
|
||||
to configure how it works. However, one should keep in mind the caveats described
|
||||
in the `Application` module documentation: the application environment is global
|
||||
state which should be avoided if possible.
|
||||
"""
|
||||
@spec get_env(app, key, value) :: value
|
||||
def get_env(app, key, default \\ nil) when is_atom(app) do
|
||||
@@ -814,7 +819,7 @@ defmodule Application do
|
||||
stick after the application is loaded and also on application reload.
|
||||
"""
|
||||
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) do
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
@@ -856,7 +861,7 @@ defmodule Application do
|
||||
It receives the same options as `put_env/4`. Returns `:ok`.
|
||||
"""
|
||||
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) do
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.unset_env(app, key, opts)
|
||||
end
|
||||
@@ -903,13 +908,13 @@ defmodule Application do
|
||||
@doc """
|
||||
Ensures the given `app` or `apps` and their child applications are started.
|
||||
|
||||
The second argument is either the `t:restart_type/1` (for consistency with
|
||||
The second argument is either the `t:restart_type/0` (for consistency with
|
||||
`start/2`) or a keyword list.
|
||||
|
||||
## Options
|
||||
|
||||
* `:type` - if the application should be started `:temporary` (default),
|
||||
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
|
||||
`:permanent`, or `:transient`. See `t:restart_type/0` for more information.
|
||||
|
||||
* `:mode` - (since v1.15.0) if the applications should be started serially
|
||||
(`:serial`, default) or concurrently (`:concurrent`).
|
||||
@@ -921,11 +926,11 @@ defmodule Application do
|
||||
{:ok, [app]} | {:error, term}
|
||||
def ensure_all_started(app_or_apps, type_or_opts \\ [])
|
||||
|
||||
def ensure_all_started(app, type) when is_atom(type) do
|
||||
ensure_all_started(app, type: type)
|
||||
def ensure_all_started(app_or_apps, type) when is_atom(type) do
|
||||
ensure_all_started(app_or_apps, type: type)
|
||||
end
|
||||
|
||||
def ensure_all_started(app, opts) when is_atom(app) do
|
||||
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do
|
||||
ensure_all_started([app], opts)
|
||||
end
|
||||
|
||||
@@ -1056,7 +1061,8 @@ defmodule Application do
|
||||
Returns a list with information about the applications which are currently running.
|
||||
"""
|
||||
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
|
||||
def started_applications(timeout \\ 5000) do
|
||||
def started_applications(timeout \\ 5000)
|
||||
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
|
||||
:application.which_applications(timeout)
|
||||
end
|
||||
|
||||
|
||||
+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 Mycroft singleton. Trick lifted from
|
||||
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
|
||||
@swar_ge_slash 0x51515151515151
|
||||
|
||||
# Mycroft zero-byte detection for base64 singletons (+, -, _).
|
||||
# Per lane: high bit set iff `bxor(w, K*ones) - 0x01..01` has its high bit
|
||||
# set, i.e. that byte's V value was 0 → original byte was K. Simplified
|
||||
# (no `bnot V` term) — for ASCII-gated `w`, borrow-propagation false
|
||||
# positives only occur for adjacent bytes that happen to equal `K xor 0x01`,
|
||||
# which is outside the base64 alphabet, so it never matters here.
|
||||
# Pattern follows https://github.com/elixir-lang/elixir/pull/15255.
|
||||
@swar_mask01 0x01010101010101
|
||||
@swar_plus_x7 0x2B2B2B2B2B2B2B
|
||||
@swar_dash_x7 0x2D2D2D2D2D2D2D
|
||||
@swar_under_x7 0x5F5F5F5F5F5F5F
|
||||
|
||||
# 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 Mycroft singleton.
|
||||
# For url, the singletons '-' and '_' are detected via two Mycroft terms.
|
||||
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_plus_x7) - @swar_mask01
|
||||
),
|
||||
@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_dash_x7) - @swar_mask01,
|
||||
bxor(w, @swar_under_x7) - @swar_mask01
|
||||
)
|
||||
),
|
||||
@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)
|
||||
|
||||
|
||||
@@ -162,6 +162,22 @@ defmodule Calendar do
|
||||
"""
|
||||
@type time_zone_database :: module()
|
||||
|
||||
@typedoc """
|
||||
Options for formatting dates and times with `strftime/3`.
|
||||
"""
|
||||
@type strftime_opts :: [
|
||||
preferred_datetime: String.t(),
|
||||
preferred_date: String.t(),
|
||||
preferred_time: String.t(),
|
||||
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
|
||||
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
||||
abbreviated_month_names:
|
||||
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
||||
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
||||
abbreviated_day_of_week_names:
|
||||
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
|
||||
]
|
||||
|
||||
@doc """
|
||||
Returns how many days there are in the given month of the given year.
|
||||
"""
|
||||
@@ -617,7 +633,7 @@ defmodule Calendar do
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec strftime(map(), String.t(), keyword()) :: String.t()
|
||||
@spec strftime(map(), String.t(), strftime_opts()) :: String.t()
|
||||
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
|
||||
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
|
||||
parse(
|
||||
|
||||
@@ -53,7 +53,7 @@ defmodule Date do
|
||||
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
|
||||
14716
|
||||
|
||||
iex> Date.add(~D[1970-01-01], 14716)
|
||||
iex> Date.add(~D[1970-01-01], 14_716)
|
||||
~D[2010-04-17]
|
||||
|
||||
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
|
||||
@@ -321,7 +321,7 @@ defmodule Date do
|
||||
@doc """
|
||||
Converts the given date to a string according to its calendar.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Date.to_string(~D[2000-02-28])
|
||||
"2000-02-28"
|
||||
@@ -399,7 +399,7 @@ defmodule Date do
|
||||
or other calendars in which the days also start at midnight.
|
||||
Attempting to convert dates from other calendars will raise an `ArgumentError`.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Date.to_iso8601(~D[2000-02-28])
|
||||
"2000-02-28"
|
||||
@@ -633,7 +633,7 @@ defmodule Date do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
|
||||
@@ -667,7 +667,7 @@ defmodule Date do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
|
||||
@@ -691,10 +691,15 @@ defmodule Date do
|
||||
@doc """
|
||||
Adds the number of days to the given `date`.
|
||||
|
||||
The days are counted as Gregorian days. The date is returned in the same
|
||||
calendar as it was given in.
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/2` always considers a day to be measured according to the
|
||||
> `Calendar.ISO`.
|
||||
|
||||
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
|
||||
The days are counted as Gregorian days, independent of the underlying
|
||||
calendar. The date is returned in the same calendar as it was given in.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -95,7 +104,7 @@ defmodule Date.Range do
|
||||
[date_from_iso_days(current, calendar)]
|
||||
end
|
||||
|
||||
defp slice(current, step, remaining, calendar) do
|
||||
defp slice(current, step, remaining, calendar) when remaining > 1 do
|
||||
[
|
||||
date_from_iso_days(current, calendar)
|
||||
| slice(current + step, step, remaining - 1, calendar)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1046,7 +1046,7 @@ defmodule DateTime do
|
||||
its abbreviation, which means information is lost when converting to such
|
||||
format.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||
@@ -1390,7 +1390,7 @@ defmodule DateTime do
|
||||
custom (but relatively common) representation which appends the time
|
||||
zone abbreviation and full name to the datetime.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||
@@ -1611,32 +1611,45 @@ defmodule DateTime do
|
||||
@doc """
|
||||
Adds a specified amount of time to a `DateTime`.
|
||||
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/3` provides a lower-level API which only supports fixed units
|
||||
> such as `:hour` and `:second`, but not `:month` (as the exact length
|
||||
> of a month depends on the current month). `add/3` always considers
|
||||
> the unit to be computed according to the `Calendar.ISO`.
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always considers the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
This function relies on a contiguous representation of time,
|
||||
ignoring the wall time and timezone changes. For example, if you add
|
||||
one day when there are summer time/daylight saving time changes,
|
||||
it will also change the time forward or backward by one hour,
|
||||
so the elapsed time is precisely 24 hours. Similarly, adding just
|
||||
a few seconds to a datetime just before "spring forward" can cause
|
||||
wall time to increase by more than an hour.
|
||||
ignoring timezone changes. For example, if you add one day when there
|
||||
are summer time/daylight saving time changes, it will also change the
|
||||
time forward or backward by one hour, so the elapsed time is precisely
|
||||
24 hours. Similarly, adding just a few seconds to a datetime just before
|
||||
"spring forward" can cause wall time to increase by more than an hour.
|
||||
|
||||
While this means this function is precise in terms of elapsed time,
|
||||
its result may be misleading in certain use cases. For example, if a
|
||||
its result may be confusing in certain use cases. For example, if a
|
||||
user requests a meeting to happen every day at 15:00 and you use this
|
||||
function to compute all future meetings by adding day after day, this
|
||||
function may change the meeting time to 14:00 or 16:00 if there are
|
||||
changes to the current timezone. Computing of recurring datetimes is
|
||||
not currently supported in Elixir's standard library but it is available
|
||||
by third-party libraries.
|
||||
changes to the current timezone.
|
||||
|
||||
### Examples
|
||||
In case you don't want these changes to happen automatically or you
|
||||
want to surface time zone conflicts to the user, you can add to
|
||||
the datetime as a naive datetime and then use `from_naive/2`:
|
||||
|
||||
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
|
||||
|
||||
The above will surface time jumps and ambiguous datetimes, allowing you
|
||||
to deal with them accordingly.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
|
||||
@@ -1664,8 +1677,6 @@ defmodule DateTime do
|
||||
iex> result.microsecond
|
||||
{21000, 3}
|
||||
|
||||
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec add(
|
||||
@@ -1739,7 +1750,7 @@ defmodule DateTime do
|
||||
to UTC, and finally computing the new timezone in case of shifts.
|
||||
This ensures `shift/3` always returns a valid datetime.
|
||||
|
||||
On the other hand, time zones that observe "Daylight Saving Time"
|
||||
Consequently, time zones that observe "Daylight Saving Time"
|
||||
or other changes, across summer/winter time will add/remove hours
|
||||
from the resulting datetime:
|
||||
|
||||
@@ -1751,12 +1762,22 @@ defmodule DateTime do
|
||||
DateTime.shift(dt, hour: 2)
|
||||
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
|
||||
|
||||
Although the first example shows a difference of 2 hours when
|
||||
comparing the wall clocks of the given datetime with the returned one,
|
||||
due to the "spring forward" time jump, the actual elapsed time is
|
||||
still exactly of 1 hour.
|
||||
|
||||
In case you don't want these changes to happen automatically or you
|
||||
want to surface time zone conflicts to the user, you can shift
|
||||
the datetime as a naive datetime and then use `from_naive/2`:
|
||||
|
||||
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
|
||||
|
||||
The above will surface time jumps and ambiguous datetimes, allowing you
|
||||
to deal with them accordingly.
|
||||
|
||||
## ISO calendar considerations
|
||||
|
||||
When using the default ISO calendar, durations are collapsed and
|
||||
applied in the order of months, then seconds and microseconds:
|
||||
|
||||
@@ -1922,7 +1943,7 @@ defmodule DateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
||||
@@ -1969,7 +1990,7 @@ defmodule DateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
||||
|
||||
@@ -161,6 +161,22 @@ defmodule Duration do
|
||||
"""
|
||||
@type duration :: t | [unit_pair]
|
||||
|
||||
@typedoc """
|
||||
Options for `Duration.to_string/2`.
|
||||
"""
|
||||
@type to_string_opts :: [
|
||||
units: [
|
||||
year: String.t(),
|
||||
month: String.t(),
|
||||
week: String.t(),
|
||||
day: String.t(),
|
||||
hour: String.t(),
|
||||
minute: String.t(),
|
||||
second: String.t()
|
||||
],
|
||||
separator: String.t()
|
||||
]
|
||||
|
||||
@microseconds_per_second 1_000_000
|
||||
|
||||
@doc """
|
||||
@@ -216,7 +232,7 @@ defmodule Duration do
|
||||
@doc """
|
||||
Adds units of given durations `d1` and `d2`.
|
||||
|
||||
Respects the the highest microsecond precision of the two.
|
||||
Respects the highest microsecond precision of the two.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -246,7 +262,7 @@ defmodule Duration do
|
||||
@doc """
|
||||
Subtracts units of given durations `d1` and `d2`.
|
||||
|
||||
Respects the the highest microsecond precision of the two.
|
||||
Respects the highest microsecond precision of the two.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -436,6 +452,7 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec to_string(t, to_string_opts) :: String.t()
|
||||
def to_string(%Duration{} = duration, opts \\ []) do
|
||||
units = Keyword.get(opts, :units, [])
|
||||
separator = Keyword.get(opts, :separator, " ")
|
||||
|
||||
+84
-154
@@ -182,7 +182,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
@type day_of_year :: 1..366
|
||||
@type quarter_of_year :: 1..4
|
||||
@type year_of_era :: {1..10000, era}
|
||||
@type year_of_era :: {1..10_000, era}
|
||||
|
||||
@seconds_per_minute 60
|
||||
@seconds_per_hour 60 * 60
|
||||
@@ -196,14 +196,28 @@ defmodule Calendar.ISO do
|
||||
@ext_date_sep ?-
|
||||
@ext_time_sep ?:
|
||||
|
||||
@days_per_nonleap_year 365
|
||||
@days_per_leap_year 366
|
||||
|
||||
# The ISO epoch starts, in this implementation,
|
||||
# with ~D[0000-01-01]. Era "1" starts
|
||||
# on ~D[0001-01-01] which is 366 days later.
|
||||
@iso_epoch 366
|
||||
|
||||
# Constants for date calculations using 400-year era cycles.
|
||||
# The algorithm uses a March-based year where March 1 is day 0.
|
||||
# Reference: Neri C, Schneider L. "Euclidean Affine Functions and
|
||||
# their Application to Calendar Algorithms". Softw Pract Exper. 2022.
|
||||
@days_per_year 365
|
||||
@years_per_era 400
|
||||
@days_per_era @years_per_era * @days_per_year + 97
|
||||
@days_per_4_years 4 * @days_per_year
|
||||
@days_per_100_years 100 * @days_per_year + 24
|
||||
@march_1_offset 31 + 29
|
||||
@unix_epoch_days 719_528
|
||||
|
||||
# Month calculation constants: in a March-based year, each 5-month
|
||||
# cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
|
||||
@days_per_5_months 153
|
||||
@months_per_cycle 5
|
||||
|
||||
[match_basic_date, match_ext_date, guard_date, read_date] =
|
||||
quote do
|
||||
[
|
||||
@@ -652,12 +666,12 @@ defmodule Calendar.ISO do
|
||||
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
|
||||
|
||||
{{year, month, day}, {hour, minute, second, _}} =
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
|
||||
{0, day_fraction} ->
|
||||
{{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
|
||||
|
||||
@@ -784,13 +798,13 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86_400}})
|
||||
{0, 1, 1, 0, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86_400}})
|
||||
{2000, 1, 1, 0, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43_200, 86_400}})
|
||||
{2000, 1, 1, 12, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86400000000}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86_400_000_000}})
|
||||
{-1, 1, 1, 0, 0, 0, {0, 6}}
|
||||
|
||||
"""
|
||||
@@ -878,28 +892,52 @@ 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
|
||||
end
|
||||
|
||||
def date_to_iso_days(1970, 1, 1) do
|
||||
719_528
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
|
||||
1
|
||||
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
|
||||
month_prime = if month > 2, do: month - 3, else: month + 9
|
||||
day_of_year = div(@days_per_5_months * month_prime + 2, @months_per_cycle) + day - 1
|
||||
|
||||
day_of_era =
|
||||
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year
|
||||
|
||||
era * @days_per_era + day_of_era + @march_1_offset
|
||||
end
|
||||
|
||||
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
|
||||
@doc false
|
||||
def date_from_iso_days(days) do
|
||||
{year, day_of_year} = days_to_year(days)
|
||||
extra_day = if leap_year?(year), do: 1, else: 0
|
||||
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
|
||||
{year, month, day_in_month + 1}
|
||||
z = days - @march_1_offset
|
||||
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era)
|
||||
day_of_era = z - era * @days_per_era
|
||||
|
||||
year_of_era =
|
||||
div(
|
||||
day_of_era - div(day_of_era, @days_per_4_years) + div(day_of_era, @days_per_100_years) -
|
||||
div(day_of_era, @days_per_era - 1),
|
||||
@days_per_year
|
||||
)
|
||||
|
||||
day_of_year =
|
||||
day_of_era -
|
||||
(@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100))
|
||||
|
||||
month_prime = div(@months_per_cycle * day_of_year + 2, @days_per_5_months)
|
||||
day = day_of_year - div(@days_per_5_months * month_prime + 2, @months_per_cycle) + 1
|
||||
month = if month_prime < 10, do: month_prime + 3, else: month_prime - 9
|
||||
year = year_of_era + era * @years_per_era
|
||||
year = if month <= 2, do: year + 1, else: year
|
||||
|
||||
{year, month, day}
|
||||
end
|
||||
|
||||
defp div_rem(int1, int2) do
|
||||
@@ -913,6 +951,9 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp floor_div_positive_divisor(int1, int2) when int1 >= 0, do: div(int1, int2)
|
||||
defp floor_div_positive_divisor(int1, int2), do: -div(-int1 - 1, int2) - 1
|
||||
|
||||
@doc """
|
||||
Returns how many days there are in the given year-month.
|
||||
|
||||
@@ -1133,7 +1174,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec year_of_era(year) :: {1..10000, era}
|
||||
@spec year_of_era(year) :: {1..10_000, era}
|
||||
def year_of_era(year) when is_year_CE(year), do: {year, 1}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
|
||||
|
||||
@@ -1159,7 +1200,7 @@ defmodule Calendar.ISO do
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@impl true
|
||||
@spec year_of_era(year, month, day) :: {1..10000, era}
|
||||
@spec year_of_era(year, month, day) :: {1..10_000, era}
|
||||
def year_of_era(year, _month, _day), do: year_of_era(year)
|
||||
|
||||
@doc """
|
||||
@@ -1704,11 +1745,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86_400_000_000}})
|
||||
{0, {0, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {43200000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {43_200_000_000, 86_400_000_000}})
|
||||
{730485, {0, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {46800000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {46_800_000_000, 86_400_000_000}})
|
||||
{730485, {0, 86400000000}}
|
||||
|
||||
"""
|
||||
@@ -1724,11 +1765,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86_400_000_000}})
|
||||
{0, {86399999999, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {43200000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {43_200_000_000, 86_400_000_000}})
|
||||
{730485, {86399999999, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {46800000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {46_800_000_000, 86_400_000_000}})
|
||||
{730485, {86399999999, 86400000000}}
|
||||
|
||||
"""
|
||||
@@ -1837,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()
|
||||
|
||||
@@ -1848,7 +1889,7 @@ defmodule Calendar.ISO do
|
||||
months_in_year = 12
|
||||
total_months = year * months_in_year + month + months - 1
|
||||
|
||||
new_year = Integer.floor_div(total_months, months_in_year)
|
||||
new_year = floor_div_positive_divisor(total_months, months_in_year)
|
||||
|
||||
new_month =
|
||||
case rem(total_months, months_in_year) + 1 do
|
||||
@@ -1888,7 +1929,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
|
||||
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
|
||||
ppd = System.convert_time_unit(86400, :second, unit)
|
||||
ppd = System.convert_time_unit(86_400, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, value, ppd)
|
||||
end
|
||||
|
||||
@@ -1937,7 +1978,7 @@ defmodule Calendar.ISO do
|
||||
}) do
|
||||
[
|
||||
month: year * 12 + month,
|
||||
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
|
||||
second: week * 7 * 86_400 + day * 86_400 + hour * 3600 + minute * 60 + second,
|
||||
microsecond: microsecond
|
||||
]
|
||||
end
|
||||
@@ -1971,7 +2012,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
if total in @unix_range_microseconds do
|
||||
microseconds = Integer.mod(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
|
||||
precision = precision_for_unit(unit)
|
||||
{date, time} = iso_seconds_to_datetime(seconds)
|
||||
{:ok, date, time, {microseconds, precision}}
|
||||
@@ -2093,9 +2134,12 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
# Note that this function does not add the extra leap day for a leap year.
|
||||
# If you want to add that leap day when appropriate,
|
||||
# add the result of leap_day_offset/2 to the result of days_before_month/1.
|
||||
defp leap_day_offset(_year, month) when month < 3, do: 0
|
||||
|
||||
defp leap_day_offset(year, _month) do
|
||||
if leap_year?(year), do: 1, else: 0
|
||||
end
|
||||
|
||||
defp days_before_month(1), do: 0
|
||||
defp days_before_month(2), do: 31
|
||||
defp days_before_month(3), do: 59
|
||||
@@ -2109,120 +2153,6 @@ defmodule Calendar.ISO do
|
||||
defp days_before_month(11), do: 304
|
||||
defp days_before_month(12), do: 334
|
||||
|
||||
defp leap_day_offset(_year, month) when month < 3, do: 0
|
||||
|
||||
defp leap_day_offset(year, _month) do
|
||||
if leap_year?(year), do: 1, else: 0
|
||||
end
|
||||
|
||||
defp days_to_year(days) when days < 0 do
|
||||
year_estimate = -div(-days, @days_per_nonleap_year) - 1
|
||||
|
||||
{year, days_before_year} =
|
||||
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
|
||||
|
||||
leap_year_pad = if leap_year?(year), do: 1, else: 0
|
||||
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
|
||||
end
|
||||
|
||||
defp days_to_year(days) do
|
||||
year_estimate = div(days, @days_per_nonleap_year)
|
||||
|
||||
{year, days_before_year} =
|
||||
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
|
||||
|
||||
{year, days - days_before_year}
|
||||
end
|
||||
|
||||
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
|
||||
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
|
||||
end
|
||||
|
||||
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
|
||||
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
|
||||
end
|
||||
|
||||
defp days_to_year(year, _days1, days2) do
|
||||
{year, days2}
|
||||
end
|
||||
|
||||
defp days_to_end_of_epoch(year) when year < 0 do
|
||||
previous_year = year + 1
|
||||
|
||||
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
|
||||
previous_year * @days_per_nonleap_year
|
||||
end
|
||||
|
||||
defp days_in_previous_years(0), do: 0
|
||||
|
||||
# A concise version of the algorithm would use floor_div instead of div.
|
||||
# However, floor_div would check the operands on every operation.
|
||||
# We optimize this by providing a positive and negative version of each algorithm.
|
||||
defp days_in_previous_years(year) when year > 0 do
|
||||
previous_year = year - 1
|
||||
|
||||
div(previous_year, 4) - div(previous_year, 100) +
|
||||
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
|
||||
@days_per_leap_year
|
||||
end
|
||||
|
||||
defp days_in_previous_years(year) when year < 0 do
|
||||
previous_year = year - 1
|
||||
|
||||
div(year, 4) - div(year, 100) +
|
||||
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
|
||||
@days_per_leap_year
|
||||
end
|
||||
|
||||
# Note that 0 is the first day of the month.
|
||||
defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
|
||||
{1, day_of_year}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
|
||||
{2, day_of_year - 31}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
|
||||
{3, day_of_year - (59 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
|
||||
{4, day_of_year - (90 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
|
||||
{5, day_of_year - (120 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
|
||||
{6, day_of_year - (151 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
|
||||
{7, day_of_year - (181 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
|
||||
{8, day_of_year - (212 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
|
||||
{9, day_of_year - (243 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
|
||||
{10, day_of_year - (273 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
|
||||
{11, day_of_year - (304 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) do
|
||||
{12, day_of_year - (334 + extra_day)}
|
||||
end
|
||||
|
||||
defp iso_seconds_to_datetime(seconds) do
|
||||
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
|
||||
|
||||
|
||||
@@ -391,13 +391,20 @@ defmodule NaiveDateTime do
|
||||
@doc """
|
||||
Adds a specified amount of time to a `NaiveDateTime`.
|
||||
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/3` provides a lower-level API which only supports fixed units
|
||||
> such as `:hour` and `:second`, but not `:month` (as the exact length
|
||||
> of a month depends on the current month). `add/3` always considers
|
||||
> the unit to be computed according to the `Calendar.ISO`.
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always consider the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -447,8 +454,6 @@ defmodule NaiveDateTime do
|
||||
iex> NaiveDateTime.add(dt, 21, :second)
|
||||
~N[2000-02-29 23:00:28]
|
||||
|
||||
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
|
||||
@@ -710,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
|
||||
|
||||
@@ -736,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,
|
||||
@@ -761,7 +770,7 @@ defmodule NaiveDateTime do
|
||||
For readability, this function follows the RFC3339 suggestion of removing
|
||||
the "T" separator between the date and time components.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
|
||||
"2000-02-28 23:00:13"
|
||||
@@ -908,7 +917,7 @@ defmodule NaiveDateTime do
|
||||
Only supports converting naive datetimes which are in the ISO calendar,
|
||||
attempting to convert naive datetimes from other calendars will raise.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
|
||||
"2000-02-28T23:00:13"
|
||||
@@ -1141,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,
|
||||
@@ -1261,7 +1272,7 @@ defmodule NaiveDateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
||||
@@ -1327,7 +1338,7 @@ defmodule NaiveDateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
||||
|
||||
@@ -225,7 +225,7 @@ defmodule Time do
|
||||
@doc """
|
||||
Converts the given `time` to a string.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Time.to_string(~T[23:00:00])
|
||||
"23:00:00"
|
||||
@@ -334,7 +334,7 @@ defmodule Time do
|
||||
format, for human readability. It also supports the "basic" format through
|
||||
passing the `:basic` option.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Time.to_iso8601(~T[23:00:13])
|
||||
"23:00:13"
|
||||
@@ -505,13 +505,18 @@ defmodule Time do
|
||||
@doc """
|
||||
Adds the `amount_to_add` of `unit`s to the given `time`.
|
||||
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/3` always considers the unit to be computed according to
|
||||
> the `Calendar.ISO`.
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always consider the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
Note the result value represents the time of day, meaning that it is cyclic,
|
||||
for instance, it will never go over 24 hours for the ISO calendar.
|
||||
@@ -549,8 +554,6 @@ defmodule Time do
|
||||
iex> result.microsecond
|
||||
{21000, 3}
|
||||
|
||||
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
|
||||
@@ -781,7 +784,7 @@ defmodule Time do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
|
||||
@@ -837,7 +840,7 @@ defmodule Time do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
|
||||
|
||||
+173
-73
@@ -248,6 +248,64 @@ defmodule Code do
|
||||
"""
|
||||
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
|
||||
|
||||
@typedoc """
|
||||
Options for code formatting functions.
|
||||
"""
|
||||
@type format_opt ::
|
||||
{:file, binary()}
|
||||
| {:line, pos_integer()}
|
||||
| {:line_length, pos_integer()}
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:force_do_end_blocks, boolean()}
|
||||
| {:migrate, boolean()}
|
||||
| {:migrate_bitstring_modifiers, boolean()}
|
||||
| {:migrate_call_parens_on_pipe, boolean()}
|
||||
| {:migrate_charlists_as_sigils, boolean()}
|
||||
| {:migrate_unless, boolean()}
|
||||
| {atom(), term()}
|
||||
|
||||
@typedoc """
|
||||
Options for `quoted_to_algebra/2`.
|
||||
"""
|
||||
@type quoted_to_algebra_opt ::
|
||||
{:line, pos_integer() | nil}
|
||||
| {:escape, boolean()}
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:comments, [term()]}
|
||||
|
||||
@typedoc """
|
||||
Options for parsing functions that convert strings to quoted expressions.
|
||||
"""
|
||||
@type parser_opts :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
column: pos_integer(),
|
||||
indentation: non_neg_integer(),
|
||||
columns: boolean(),
|
||||
unescape: boolean(),
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
|
||||
emit_warnings: boolean()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for evaluation environment, accepted by `env_for_eval/1`.
|
||||
"""
|
||||
@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,
|
||||
:debug_info,
|
||||
@@ -260,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.
|
||||
@@ -516,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
|
||||
@@ -548,11 +613,11 @@ 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:
|
||||
|
||||
iex> require Integer
|
||||
iex> require Integer, warn: false
|
||||
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
|
||||
iex> result
|
||||
3
|
||||
@@ -560,21 +625,28 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {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
|
||||
|
||||
@@ -615,7 +687,8 @@ defmodule Code do
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
|
||||
@spec with_diagnostics([log: boolean()], (-> result)) ::
|
||||
{result, [diagnostic(:warning | :error)]}
|
||||
when result: term()
|
||||
def with_diagnostics(opts \\ [], fun) do
|
||||
value = :erlang.get(:elixir_code_diagnostics)
|
||||
@@ -648,7 +721,7 @@ defmodule Code do
|
||||
Defaults to `true`.
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
|
||||
@spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok
|
||||
def print_diagnostic(diagnostic, opts \\ []) do
|
||||
read_snippet? = Keyword.get(opts, :snippet, true)
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
|
||||
@@ -672,7 +745,7 @@ defmodule Code do
|
||||
* `:line` - the line the string starts, used for error reporting
|
||||
|
||||
* `:line_length` - the line length to aim for when formatting
|
||||
the document. Defaults to 98. This value indicates when an expression
|
||||
the document. Defaults to `98`. This value indicates when an expression
|
||||
should be broken over multiple lines but it is not guaranteed
|
||||
to do so. See the "Line length" section below for more information
|
||||
|
||||
@@ -1035,9 +1108,9 @@ defmodule Code do
|
||||
address the deprecation warnings.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec format_string!(binary, keyword) :: iodata
|
||||
@spec format_string!(binary, [format_opt]) :: iodata
|
||||
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
|
||||
line_length = Keyword.get(opts, :line_length, 98)
|
||||
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
|
||||
|
||||
to_quoted_opts =
|
||||
[
|
||||
@@ -1060,7 +1133,7 @@ defmodule Code do
|
||||
available options.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec format_file!(binary, keyword) :: iodata
|
||||
@spec format_file!(binary, [format_opt]) :: iodata
|
||||
def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
string = File.read!(file)
|
||||
formatted = format_string!(string, [file: file, line: 1] ++ opts)
|
||||
@@ -1076,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
|
||||
|
||||
@@ -1098,11 +1172,20 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {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
|
||||
|
||||
@@ -1129,8 +1212,10 @@ defmodule Code do
|
||||
* `:line` - the line on which the script starts
|
||||
|
||||
* `:module` - the module to run the environment on
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@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 """
|
||||
@@ -1150,9 +1235,13 @@ defmodule Code do
|
||||
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(), keyword) ::
|
||||
@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
|
||||
@@ -1171,14 +1260,14 @@ defmodule Code do
|
||||
Defaults to `"nofile"`.
|
||||
|
||||
* `:line` - the starting line of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:column` - (since v1.11.0) the starting column of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:indentation` - (since v1.19.0) the indentation for the string being parsed.
|
||||
This is useful when the code parsed is embedded within another document.
|
||||
Defaults to 0.
|
||||
Defaults to `0`.
|
||||
|
||||
* `:columns` - when `true`, attach a `:column` key to the quoted
|
||||
metadata. Defaults to `false`.
|
||||
@@ -1230,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
|
||||
@@ -1263,20 +1352,13 @@ defmodule Code do
|
||||
{:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
|
||||
|
||||
"""
|
||||
@spec string_to_quoted(List.Chars.t(), keyword) ::
|
||||
@spec string_to_quoted(List.Chars.t(), parser_opts) ::
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
||||
def string_to_quoted(string, opts \\ []) when is_list(opts) 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 """
|
||||
@@ -1290,7 +1372,7 @@ defmodule Code do
|
||||
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
@spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
|
||||
@spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t()
|
||||
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
@@ -1341,7 +1423,7 @@ defmodule Code do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
|
||||
@spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) ::
|
||||
{:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
|
||||
def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
|
||||
charlist = to_charlist(string)
|
||||
@@ -1352,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
|
||||
@@ -1371,7 +1452,7 @@ defmodule Code do
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
|
||||
@spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())}
|
||||
def string_to_quoted_with_comments!(string, opts \\ []) do
|
||||
charlist = to_charlist(string)
|
||||
|
||||
@@ -1456,6 +1537,9 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
This function accepts all options supported by `format_string!/2` for controlling
|
||||
code formatting, plus these additional options:
|
||||
|
||||
* `:comments` - the list of comments associated with the quoted expression.
|
||||
Defaults to `[]`. It is recommended that both `:token_metadata` and
|
||||
`:literal_encoder` options are given to `string_to_quoted_with_comments/2`
|
||||
@@ -1466,17 +1550,13 @@ defmodule Code do
|
||||
`string_to_quoted/2`, setting this option to `false` will prevent it from
|
||||
escaping the sequences twice. Defaults to `true`.
|
||||
|
||||
* `:locals_without_parens` - a keyword list of name and arity
|
||||
pairs that should be kept without parens whenever possible.
|
||||
The arity may be the atom `:*`, which implies all arities of
|
||||
that name. The formatter already includes a list of functions
|
||||
and this option augments this list.
|
||||
|
||||
* `:syntax_colors` - a keyword list of colors the output is colorized.
|
||||
See `Inspect.Opts` for more information.
|
||||
See `format_string!/2` for the full list of formatting options including
|
||||
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
|
||||
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
|
||||
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
|
||||
Inspect.Algebra.t()
|
||||
def quoted_to_algebra(quoted, opts \\ []) do
|
||||
quoted
|
||||
|> Code.Normalizer.normalize(opts)
|
||||
@@ -1655,9 +1735,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`.
|
||||
@@ -1673,6 +1750,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`.
|
||||
@@ -1684,24 +1764,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 current module
|
||||
may be compiled from scratch. `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
|
||||
@@ -1712,14 +1805,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.
|
||||
|
||||
@@ -1759,6 +1850,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
|
||||
|
||||
@@ -158,6 +158,7 @@ defmodule Code.Formatter do
|
||||
@doc """
|
||||
Converts the quoted expression into an algebra document.
|
||||
"""
|
||||
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
|
||||
def to_algebra(quoted, opts \\ []) do
|
||||
comments = Keyword.get(opts, :comments, [])
|
||||
|
||||
|
||||
@@ -11,6 +11,26 @@ defmodule Code.Fragment do
|
||||
|
||||
@type position :: {line :: pos_integer(), column :: pos_integer()}
|
||||
|
||||
@typedoc """
|
||||
Options for cursor context functions.
|
||||
|
||||
Currently, these options are not used but reserved for future extensibility.
|
||||
"""
|
||||
@type cursor_opts :: []
|
||||
|
||||
@typedoc """
|
||||
Options for converting code fragments to quoted expressions.
|
||||
"""
|
||||
@type container_cursor_to_quoted_opts :: [
|
||||
file: String.t(),
|
||||
line: pos_integer(),
|
||||
column: pos_integer(),
|
||||
columns: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
trailing_fragment: String.t()
|
||||
]
|
||||
|
||||
@doc ~S"""
|
||||
Returns the list of lines in the given string, preserving their line endings.
|
||||
|
||||
@@ -172,7 +192,7 @@ defmodule Code.Fragment do
|
||||
references, and more.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec cursor_context(List.Chars.t(), keyword()) ::
|
||||
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
|
||||
{:alias, charlist}
|
||||
| {:alias, inside_alias, charlist}
|
||||
| {:block_keyword_or_binary_operator, charlist}
|
||||
@@ -282,7 +302,8 @@ defmodule Code.Fragment do
|
||||
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
|
||||
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
|
||||
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
|
||||
{_, _} -> {:none, 0}
|
||||
{{:sigil, _}, _} -> {:none, 0}
|
||||
{_, _} -> {{:operator, ~c"/"}, 1}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -315,7 +336,7 @@ defmodule Code.Fragment do
|
||||
end
|
||||
|
||||
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
|
||||
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
|
||||
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
|
||||
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
|
||||
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
|
||||
|
||||
@@ -662,7 +683,7 @@ defmodule Code.Fragment do
|
||||
of examples and their return values.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec surround_context(List.Chars.t(), position(), keyword()) ::
|
||||
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
|
||||
%{begin: position, end: position, context: context} | :none
|
||||
when context:
|
||||
{:alias, charlist}
|
||||
@@ -771,6 +792,12 @@ defmodule Code.Fragment do
|
||||
{{:local_or_var, acc}, offset} ->
|
||||
build_surround({:local_or_var, acc}, reversed, line, offset)
|
||||
|
||||
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
|
||||
build_surround({:operator, acc}, reversed, line, offset)
|
||||
|
||||
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
|
||||
build_surround({:keyword, acc}, reversed, line, offset)
|
||||
|
||||
{{:module_attribute, ~c""}, offset} ->
|
||||
build_surround({:operator, ~c"@"}, reversed, line, offset)
|
||||
|
||||
@@ -1187,10 +1214,10 @@ defmodule Code.Fragment do
|
||||
Defaults to `"nofile"`.
|
||||
|
||||
* `:line` - the starting line of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:column` - the starting column of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:columns` - when `true`, attach a `:column` key to the quoted
|
||||
metadata. Defaults to `false`.
|
||||
@@ -1207,14 +1234,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(), keyword()) ::
|
||||
@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)
|
||||
@@ -1234,7 +1290,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 =
|
||||
@@ -1242,7 +1301,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 =
|
||||
@@ -1261,10 +1320,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
|
||||
|
||||
@@ -1305,7 +1366,7 @@ defmodule Code.Fragment do
|
||||
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
||||
|
||||
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
|
||||
defp drop_tokens([], 0), do: []
|
||||
defp drop_tokens([], _counter), do: []
|
||||
|
||||
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
|
||||
|
||||
@@ -14,6 +14,7 @@ defmodule Code.Normalizer do
|
||||
Wraps literals in the quoted expression to conform to the AST format expected
|
||||
by the formatter.
|
||||
"""
|
||||
@spec normalize(Macro.t(), keyword()) :: Macro.t()
|
||||
def normalize(quoted, opts \\ []) do
|
||||
line = Keyword.get(opts, :line, nil)
|
||||
escape = Keyword.get(opts, :escape, true)
|
||||
@@ -67,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
|
||||
@@ -88,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
|
||||
|
||||
@@ -117,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)
|
||||
@@ -404,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 =
|
||||
@@ -426,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
|
||||
|
||||
@@ -98,6 +98,12 @@ defmodule Config do
|
||||
(assembled with `mix release`).
|
||||
"""
|
||||
|
||||
@type config_opts :: [
|
||||
imports: [Path.t()] | :disabled,
|
||||
env: atom(),
|
||||
target: atom()
|
||||
]
|
||||
|
||||
@opts_key {__MODULE__, :opts}
|
||||
@config_key {__MODULE__, :config}
|
||||
@imports_key {__MODULE__, :imports}
|
||||
@@ -306,7 +312,7 @@ defmodule Config do
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
|
||||
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
|
||||
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
env = Keyword.get(opts, :env)
|
||||
target = Keyword.get(opts, :target)
|
||||
@@ -371,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
|
||||
|
||||
@@ -111,6 +111,16 @@ defmodule Config.Provider do
|
||||
"""
|
||||
@type config_path :: {:system, binary(), binary()} | binary()
|
||||
|
||||
@typedoc """
|
||||
Options for `init/3`.
|
||||
"""
|
||||
@type init_opts :: [
|
||||
extra_config: config(),
|
||||
prune_runtime_sys_config_after_boot: boolean(),
|
||||
reboot_system_after_config: boolean(),
|
||||
validate_compile_env: [{atom(), [atom()], term()}]
|
||||
]
|
||||
|
||||
@doc """
|
||||
Invoked when initializing a config provider.
|
||||
|
||||
@@ -196,6 +206,7 @@ defmodule Config.Provider do
|
||||
@reboot_mode_key :config_provider_reboot_mode
|
||||
|
||||
@doc false
|
||||
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
|
||||
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
|
||||
validate_config_path!(config_path)
|
||||
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
|
||||
|
||||
@@ -46,6 +46,12 @@ defmodule Config.Reader do
|
||||
|
||||
@behaviour Config.Provider
|
||||
|
||||
@type config_opts :: [
|
||||
imports: [Path.t()] | :disabled,
|
||||
env: atom(),
|
||||
target: atom()
|
||||
]
|
||||
|
||||
@impl true
|
||||
def init(opts) when is_list(opts) do
|
||||
{path, opts} = Keyword.pop!(opts, :path)
|
||||
@@ -68,7 +74,7 @@ defmodule Config.Reader do
|
||||
Accepts the same options as `read!/2`.
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec eval!(Path.t(), binary, keyword) :: keyword
|
||||
@spec eval!(Path.t(), binary, config_opts) :: keyword
|
||||
def eval!(file, contents, opts \\ [])
|
||||
when is_binary(file) and is_binary(contents) and is_list(opts) do
|
||||
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
|
||||
@@ -90,7 +96,7 @@ defmodule Config.Reader do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read!(Path.t(), keyword) :: keyword
|
||||
@spec read!(Path.t(), config_opts) :: keyword
|
||||
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
file = Path.expand(file)
|
||||
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
|
||||
@@ -104,7 +110,7 @@ defmodule Config.Reader do
|
||||
option cannot be disabled in `read_imports!/2`.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
|
||||
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
|
||||
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
if opts[:imports] == :disabled do
|
||||
raise ArgumentError, ":imports must be a list of paths"
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule DynamicSupervisor do
|
||||
|
||||
## Examples
|
||||
|
||||
A dynamic supervisor is started with no children and often a name:
|
||||
A dynamic supervisor is started with no children and often with a name:
|
||||
|
||||
children = [
|
||||
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
|
||||
@@ -137,67 +137,6 @@ defmodule DynamicSupervisor do
|
||||
|
||||
A supervisor is bound to the same name registration rules as a `GenServer`.
|
||||
Read more about these rules in the documentation for `GenServer`.
|
||||
|
||||
## Migrating from Supervisor's :simple_one_for_one
|
||||
|
||||
In case you were using the deprecated `:simple_one_for_one` strategy from
|
||||
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
|
||||
few steps.
|
||||
|
||||
Imagine the given "old" code:
|
||||
|
||||
defmodule MySupervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link(init_arg) do
|
||||
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
def start_child(foo, bar, baz) do
|
||||
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
|
||||
Supervisor.start_child(__MODULE__, [foo, bar, baz])
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(init_arg) do
|
||||
children = [
|
||||
# Or the deprecated: worker(MyWorker, [init_arg])
|
||||
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :simple_one_for_one)
|
||||
end
|
||||
end
|
||||
|
||||
It can be upgraded to the DynamicSupervisor like this:
|
||||
|
||||
defmodule MySupervisor do
|
||||
use DynamicSupervisor
|
||||
|
||||
def start_link(init_arg) do
|
||||
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
def start_child(foo, bar, baz) do
|
||||
# If MyWorker is not using the new child specs, we need to pass a map:
|
||||
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
|
||||
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
|
||||
DynamicSupervisor.start_child(__MODULE__, spec)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(init_arg) do
|
||||
DynamicSupervisor.init(
|
||||
strategy: :one_for_one,
|
||||
extra_arguments: [init_arg]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
The difference is that the `DynamicSupervisor` expects the child specification
|
||||
at the moment `start_child/2` is called, and no longer on the init callback.
|
||||
If there are any initial arguments given on initialization, such as `[initial_arg]`,
|
||||
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
|
||||
"""
|
||||
|
||||
@behaviour GenServer
|
||||
@@ -233,9 +172,10 @@ defmodule DynamicSupervisor do
|
||||
@typedoc """
|
||||
Return values of `start_child` functions.
|
||||
|
||||
Unlike `Supervisor`, this module ignores the child spec ids, so
|
||||
`{:error, {:already_started, pid}}` is not returned for child specs given with the same id.
|
||||
`{:error, {:already_started, pid}}` is returned however if a duplicate name is used when using
|
||||
Unlike `Supervisor`, this module ignores the child spec ids,
|
||||
so `{:error, {:already_started, pid}}` is not returned for child specs
|
||||
given with the same id. `{:error, {:already_started, pid}}` is returned
|
||||
however if a duplicate name is used when using
|
||||
[name registration](`m:GenServer#module-name-registration`).
|
||||
"""
|
||||
@type on_start_child ::
|
||||
@@ -266,6 +206,7 @@ defmodule DynamicSupervisor do
|
||||
See `Supervisor` for more information about child specifications.
|
||||
"""
|
||||
@doc since: "1.6.1"
|
||||
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
|
||||
def child_spec(options) when is_list(options) do
|
||||
id =
|
||||
case Keyword.get(options, :name, DynamicSupervisor) do
|
||||
@@ -415,6 +356,10 @@ defmodule DynamicSupervisor do
|
||||
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
|
||||
used when using [name registration](`m:GenServer#module-name-registration`).
|
||||
|
||||
This function will block the `DynamicSupervisor` until the child initializes.
|
||||
When starting too many processes dynamically, you may want to use a
|
||||
`PartitionSupervisor` to split the work across multiple processes.
|
||||
|
||||
If the child process start function returns `{:ok, child}` or `{:ok, child,
|
||||
info}`, then child specification and PID are added to the supervisor and
|
||||
this function returns the same value.
|
||||
@@ -518,6 +463,14 @@ defmodule DynamicSupervisor do
|
||||
@doc """
|
||||
Terminates the given child identified by `pid`.
|
||||
|
||||
This function will block the `DynamicSupervisor` until the child
|
||||
terminates, which may take an arbitrary amount of time if the child
|
||||
is trapping exits and implements its own terminate callback.
|
||||
For this reason, it is often better to ask the child process
|
||||
itself to terminate, often by declaring in its child spec it has
|
||||
a restart strategy of `:transient` (or `:temporary`) and then
|
||||
sending it a message to stop with reason `:shutdown`.
|
||||
|
||||
If successful, this function returns `:ok`. If there is no process with
|
||||
the given PID, this function returns `{:error, :not_found}`.
|
||||
"""
|
||||
@@ -528,11 +481,11 @@ defmodule DynamicSupervisor do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with information about all children.
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
|
||||
Note that calling this function when supervising a large number
|
||||
of children under low memory conditions can cause an out of memory
|
||||
exception.
|
||||
of children under low memory conditions can bring the system down due to an
|
||||
out of memory error.
|
||||
|
||||
This function returns a list of tuples containing:
|
||||
|
||||
|
||||
+180
-61
@@ -39,6 +39,20 @@ defprotocol Enumerable do
|
||||
`reduce/3` function. All other functions exist as optimizations paths
|
||||
for data structures that can implement certain properties in better
|
||||
than linear time.
|
||||
|
||||
## Default implementation for lists
|
||||
|
||||
Sometimes you may want to implement this protocol for a list contained
|
||||
in struct. This can be done by delegating to the `Enumerable.List` module
|
||||
in the `reduce/3` implementation and providing a straight-forward
|
||||
implementation for the remaining ones:
|
||||
|
||||
defimpl Enumerable, for: CustomStruct do
|
||||
def count(struct), do: {:ok, length(struct.items)}
|
||||
def member?(struct, value), do: {:ok, value in struct.items}
|
||||
def slice(struct), do: {:error, __MODULE__}
|
||||
def reduce(struct, acc, fun), do: Enumerable.List.reduce(struct.items, acc, fun)
|
||||
end
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
@@ -766,6 +780,10 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def count_until(_enumerable, limit) when is_integer(limit) do
|
||||
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Counts the elements in the enumerable for which `fun` returns a truthy value, stopping at `limit`.
|
||||
|
||||
@@ -787,6 +805,10 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def count_until(_enumerable, _fun, limit) when is_integer(limit) do
|
||||
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enumerates the `enumerable`, returning a list where all consecutive
|
||||
duplicate elements are collapsed to a single element.
|
||||
@@ -807,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
|
||||
@@ -951,8 +973,8 @@ defmodule Enum do
|
||||
## Examples
|
||||
|
||||
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
|
||||
"some"
|
||||
"example"
|
||||
some
|
||||
example
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@@ -1214,7 +1236,7 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps the given `fun` over `enumerable` and flattens the result.
|
||||
Maps the given `fun` over `enumerable` and flattens the result only one level deep.
|
||||
|
||||
This function returns a new enumerable built by appending the result of invoking `fun`
|
||||
on each element of `enumerable` together; conceptually, this is similar to a
|
||||
@@ -1231,7 +1253,7 @@ defmodule Enum do
|
||||
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
|
||||
[[:a], [:b], [:c]]
|
||||
|
||||
This is frequently used to to transform and filter in one pass, returning empty
|
||||
This is frequently used to transform and filter in one pass, returning empty
|
||||
lists to exclude results:
|
||||
|
||||
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
|
||||
@@ -1262,13 +1284,16 @@ defmodule Enum do
|
||||
defp flat_reverse([], acc), do: acc
|
||||
|
||||
@doc """
|
||||
Maps and reduces an `enumerable`, flattening the given results (only one level deep).
|
||||
Maps and reduces an `enumerable`, flattening the results only one level deep.
|
||||
|
||||
It expects an accumulator and a function that receives each enumerable
|
||||
element, and must return a tuple containing a new enumerable (often a list)
|
||||
with the new accumulator or a tuple with `:halt` as first element and
|
||||
the accumulator as second.
|
||||
|
||||
Returns a 2-element tuple where the first element is the results flattened one level deep and
|
||||
the second element is the last accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> enumerable = 1..100
|
||||
@@ -1493,6 +1518,14 @@ defmodule Enum do
|
||||
to_list(enumerable)
|
||||
end
|
||||
|
||||
def into(enumerable, collectable) when is_struct(collectable, MapSet) do
|
||||
if MapSet.size(collectable) == 0 do
|
||||
MapSet.new(enumerable)
|
||||
else
|
||||
MapSet.new(enumerable) |> MapSet.union(collectable)
|
||||
end
|
||||
end
|
||||
|
||||
def into(%_{} = enumerable, collectable) do
|
||||
into_protocol(enumerable, collectable)
|
||||
end
|
||||
@@ -1569,8 +1602,12 @@ defmodule Enum do
|
||||
map(enumerable, transform)
|
||||
end
|
||||
|
||||
def into(%_{} = enumerable, collectable, transform) do
|
||||
into_protocol(enumerable, collectable, transform)
|
||||
def into(enumerable, collectable, transform) when is_struct(collectable, MapSet) do
|
||||
if MapSet.size(collectable) == 0 do
|
||||
MapSet.new(enumerable, transform)
|
||||
else
|
||||
MapSet.new(enumerable, transform) |> MapSet.union(collectable)
|
||||
end
|
||||
end
|
||||
|
||||
def into(enumerable, %_{} = collectable, transform) do
|
||||
@@ -1842,7 +1879,7 @@ defmodule Enum do
|
||||
Returns the maximal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
|
||||
By default, the comparison is done with the `>=` sorter function.
|
||||
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
|
||||
If multiple elements are considered maximal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
maximal to be returned, the sorter function should not return true
|
||||
@@ -1909,7 +1946,7 @@ defmodule Enum do
|
||||
Returns the maximal element in the `enumerable` as calculated
|
||||
by the given `fun`.
|
||||
|
||||
By default, the comparison is done with the `>=` sorter function.
|
||||
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
|
||||
If multiple elements are considered maximal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
maximal to be returned, the sorter function should not return true
|
||||
@@ -1986,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
|
||||
@@ -2022,7 +2064,7 @@ defmodule Enum do
|
||||
Returns the minimal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
|
||||
By default, the comparison is done with the `<=` sorter function.
|
||||
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
|
||||
If multiple elements are considered minimal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
minimal to be returned, the sorter function should not return true
|
||||
@@ -2089,7 +2131,7 @@ defmodule Enum do
|
||||
Returns the minimal element in the `enumerable` as calculated
|
||||
by the given `fun`.
|
||||
|
||||
By default, the comparison is done with the `<=` sorter function.
|
||||
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
|
||||
If multiple elements are considered minimal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
minimal to be returned, the sorter function should not return true
|
||||
@@ -2143,28 +2185,60 @@ defmodule Enum do
|
||||
|
||||
@doc """
|
||||
Returns a tuple with the minimal and the maximal elements in the
|
||||
enumerable according to Erlang's term ordering.
|
||||
enumerable.
|
||||
|
||||
If multiple elements are considered maximal or minimal, the first one
|
||||
that was found is returned.
|
||||
|
||||
Calls the provided `empty_fallback` function and returns its value if
|
||||
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
|
||||
By default, the comparison is done with the [`<`](`</2`) sorter function,
|
||||
as the function must not return true for equal elements.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.min_max([2, 3, 1])
|
||||
{1, 3}
|
||||
|
||||
iex> Enum.min_max(["foo", "bar", "baz"])
|
||||
{"bar", "foo"}
|
||||
|
||||
iex> Enum.min_max([], fn -> {nil, nil} end)
|
||||
{nil, nil}
|
||||
|
||||
"""
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max(enumerable, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
The fact this function uses Erlang's term ordering means that the
|
||||
comparison is structural and not semantic. Therefore, if you want
|
||||
to compare structs, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
def min_max(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
iex> dates = [
|
||||
...> ~D[2019-01-01],
|
||||
...> ~D[2020-01-01],
|
||||
...> ~D[2018-01-01]
|
||||
...> ]
|
||||
iex> Enum.min_max(dates, Date)
|
||||
{~D[2018-01-01], ~D[2020-01-01]}
|
||||
|
||||
You can also pass a custom sorting function:
|
||||
|
||||
iex> Enum.min_max([2, 3, 1], &>/2)
|
||||
{3, 1}
|
||||
|
||||
Finally, if you don't want to raise on empty enumerables, you can pass
|
||||
the empty fallback:
|
||||
|
||||
iex> Enum.min_max([], fn -> nil end)
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
|
||||
{element, element} | empty_result
|
||||
when empty_result: any
|
||||
|
||||
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
|
||||
def min_max(first..last//step = range, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
case Range.size(range) do
|
||||
0 ->
|
||||
empty_fallback.()
|
||||
@@ -2175,11 +2249,39 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
def min_max(enumerable, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
min_max(enumerable, &</2, empty_fallback)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter) when is_atom(sorter) do
|
||||
min_max(enumerable, min_max_sort_fun(sorter))
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter) when is_function(sorter, 2) do
|
||||
min_max(enumerable, sorter, fn -> raise Enum.EmptyError end)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter, empty_fallback)
|
||||
when is_atom(sorter) and is_function(empty_fallback, 0) do
|
||||
min_max(enumerable, min_max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter, empty_fallback)
|
||||
when is_function(sorter, 2) and is_function(empty_fallback, 0) do
|
||||
first_fun = &[&1 | &1]
|
||||
|
||||
reduce_fun = fn entry, [min | max] ->
|
||||
[Kernel.min(min, entry) | Kernel.max(max, entry)]
|
||||
reduce_fun = fn entry, [min | max] = acc ->
|
||||
cond do
|
||||
sorter.(entry, min) ->
|
||||
[entry | max]
|
||||
|
||||
sorter.(max, entry) ->
|
||||
[min | entry]
|
||||
|
||||
true ->
|
||||
acc
|
||||
end
|
||||
end
|
||||
|
||||
case reduce_by(enumerable, first_fun, reduce_fun) do
|
||||
@@ -2200,8 +2302,8 @@ defmodule Enum do
|
||||
Returns a tuple with the minimal and the maximal elements in the
|
||||
enumerable as calculated by the given function.
|
||||
|
||||
If multiple elements are considered maximal or minimal, the first one
|
||||
that was found is returned.
|
||||
By default, the comparison is done with the [`<`](`</2`) sorter function,
|
||||
as the function must not return `true` for equal elements.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -2259,7 +2361,7 @@ defmodule Enum do
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
|
||||
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
|
||||
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
@@ -2290,7 +2392,7 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
|
||||
@doc """
|
||||
Splits the `enumerable` in two lists according to the given function `fun`.
|
||||
@@ -3611,9 +3713,14 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
def take(enumerable, amount) when is_integer(amount) and amount < 0 do
|
||||
{count, fun} = slice_count_and_fun(enumerable, 1)
|
||||
first = Kernel.max(amount + count, 0)
|
||||
fun.(first, count - first, 1)
|
||||
case slice_count_and_fun(enumerable, 1) do
|
||||
{0, _fun} ->
|
||||
[]
|
||||
|
||||
{count, fun} ->
|
||||
first = Kernel.max(amount + count, 0)
|
||||
fun.(first, count - first, 1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4089,6 +4196,11 @@ defmodule Enum do
|
||||
iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end)
|
||||
[4, 6]
|
||||
|
||||
`zip_with/2` can be used to transpose lists of lists:
|
||||
|
||||
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
|
||||
[[1, 3], [2, 4]]
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec zip_with(t, ([term] -> term)) :: [term]
|
||||
@@ -4400,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
|
||||
|
||||
@@ -5006,8 +5108,7 @@ end
|
||||
defimpl Enumerable, for: List do
|
||||
def count(list), do: {:ok, length(list)}
|
||||
|
||||
def member?([], _value), do: {:ok, false}
|
||||
def member?(_list, _value), do: {:error, __MODULE__}
|
||||
def member?(list, value), do: {:ok, :lists.member(value, list)}
|
||||
|
||||
def slice([]), do: {:ok, 0, fn _, _, _ -> [] end}
|
||||
def slice(_list), do: {:error, __MODULE__}
|
||||
@@ -5062,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
|
||||
@@ -5085,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
|
||||
@@ -5109,15 +5219,24 @@ 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
|
||||
|
||||
defp slice(_current, _step, 0), do: []
|
||||
defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)]
|
||||
defp slice(current, _step, 1), do: [current]
|
||||
|
||||
defp slice(current, step, remaining) when remaining > 1 do
|
||||
[current | slice(current + step, step, remaining - 1)]
|
||||
end
|
||||
end
|
||||
|
||||
+22
-40
@@ -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
|
||||
|
||||
@@ -188,13 +188,12 @@ defmodule Exception do
|
||||
term
|
||||
|> inspect(pretty: true)
|
||||
|> String.split("\n")
|
||||
|> Enum.map(fn
|
||||
|> Enum.map_intersperse("\n", fn
|
||||
"" -> ""
|
||||
line -> " " <> line
|
||||
end)
|
||||
|> Enum.join("\n")
|
||||
|
||||
message <> "\n\n" <> inspected
|
||||
IO.iodata_to_binary([message, "\n\n", inspected, "\n"])
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -288,10 +287,10 @@ defmodule Exception do
|
||||
end
|
||||
end
|
||||
|
||||
defp is_map_node?({:is_map, _, [_]}), do: true
|
||||
defp is_map_node?(_), do: false
|
||||
defp is_map_key_node?({:is_map_key, _, [_, _]}), do: true
|
||||
defp is_map_key_node?(_), do: false
|
||||
defp map_node?({:is_map, _, [_]}), do: true
|
||||
defp map_node?(_), do: false
|
||||
defp map_key_node?({:is_map_key, _, [_, _]}), do: true
|
||||
defp map_key_node?(_), do: false
|
||||
|
||||
defp struct_validation_node?(
|
||||
{:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]}
|
||||
@@ -305,16 +304,16 @@ defmodule Exception do
|
||||
|
||||
defp struct_validation_node?(_), do: false
|
||||
|
||||
defp is_struct_macro?(
|
||||
defp struct_macro?(
|
||||
{:and, _,
|
||||
[
|
||||
{:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [arg, _]}}]},
|
||||
%{node: node_3 = {_, _, [{_, _, [_, arg]}]}}
|
||||
]}
|
||||
),
|
||||
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
|
||||
defp is_struct_macro?(
|
||||
defp struct_macro?(
|
||||
{:and, _,
|
||||
[
|
||||
{:and, _,
|
||||
@@ -329,12 +328,12 @@ defmodule Exception do
|
||||
%{node: node_3 = {_, _, [{_, _, [_, arg]}, _]}}
|
||||
]}
|
||||
),
|
||||
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
|
||||
defp is_struct_macro?(_), do: false
|
||||
defp struct_macro?(_), do: false
|
||||
|
||||
defp translate_guard(guard) do
|
||||
if is_struct_macro?(guard) do
|
||||
if struct_macro?(guard) do
|
||||
undo_is_struct_guard(guard)
|
||||
else
|
||||
guard
|
||||
@@ -1389,6 +1388,7 @@ defmodule CompileError do
|
||||
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
|
||||
the error occurred in code that did not come from a file
|
||||
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred
|
||||
* `:description` (`t:String.t/0`) - a description of the compile error
|
||||
|
||||
This is mostly raised by Elixir tooling when compiling and evaluating code.
|
||||
"""
|
||||
@@ -1457,20 +1457,6 @@ defmodule BadFunctionError do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadStructError do
|
||||
@moduledoc deprecated:
|
||||
"This exception is deprecated alongside the struct update syntax that raises it"
|
||||
defexception [:struct, :term]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
Exception.__format_message_with_term__(
|
||||
"expected a struct named #{inspect(exception.struct)}, got:",
|
||||
exception.term
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadMapError do
|
||||
@moduledoc """
|
||||
An exception raised when a map is expected, but something else was given.
|
||||
@@ -1912,7 +1898,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
defp format_fa({_dist, fun, arity}) do
|
||||
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
|
||||
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
|
||||
end
|
||||
|
||||
defp exports_for(module) do
|
||||
@@ -1944,8 +1930,8 @@ defmodule FunctionClauseError do
|
||||
|
||||
For example:
|
||||
|
||||
iex> URI.parse(:wrong_argument)
|
||||
** (FunctionClauseError) no function clause matching in URI.parse/1
|
||||
iex> List.duplicate(:ok, -3)
|
||||
** (FunctionClauseError) no function clause matching in List.duplicate/2
|
||||
|
||||
The following fields of this exception are public and can be accessed freely:
|
||||
|
||||
@@ -2244,7 +2230,7 @@ defmodule KeyError do
|
||||
|
||||
case suggestions do
|
||||
[] -> []
|
||||
suggestions -> ["\n\nDid you mean:\n\n" | format_suggestions(suggestions)]
|
||||
suggestions -> ["\nDid you mean:\n\n" | format_suggestions(suggestions)]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2253,7 +2239,7 @@ defmodule KeyError do
|
||||
|> Enum.sort(&(elem(&1, 0) >= elem(&2, 0)))
|
||||
|> Enum.take(@max_suggestions)
|
||||
|> Enum.sort(&(elem(&1, 1) <= elem(&2, 1)))
|
||||
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|
||||
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2605,10 +2591,6 @@ defmodule ErlangError do
|
||||
%BadFunctionError{term: term}
|
||||
end
|
||||
|
||||
def normalize({:badstruct, struct, term}, _stacktrace) do
|
||||
%BadStructError{struct: struct, term: term}
|
||||
end
|
||||
|
||||
def normalize({:badmatch, term}, _stacktrace) do
|
||||
%MatchError{term: term}
|
||||
end
|
||||
|
||||
+126
-28
@@ -317,7 +317,7 @@ defmodule File do
|
||||
directories of `path`
|
||||
* `:enospc` - there is no space left on the device
|
||||
* `:enotdir` - a component of `path` is not a directory
|
||||
* `:eperm` - missed required permisions
|
||||
* `:eperm` - missed required permissions
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -694,7 +710,7 @@ defmodule File do
|
||||
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
{:error, :enoent}
|
||||
|
||||
File.touch("/tmp/a.txt", 1544519753)
|
||||
File.touch("/tmp/a.txt", 1_544_519_753)
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@@ -706,7 +722,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_universal_time(path, time),
|
||||
:ok <- write(path, "", [:append]),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
do: :elixir_utils.change_universal_time(path, time)
|
||||
end
|
||||
|
||||
@@ -714,7 +730,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_posix_time(path, time),
|
||||
:ok <- write(path, "", [:append]),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
do: :elixir_utils.change_posix_time(path, time)
|
||||
end
|
||||
|
||||
@@ -733,7 +749,7 @@ defmodule File do
|
||||
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
|
||||
|
||||
File.touch!("/tmp/a.txt", 1544519753)
|
||||
File.touch!("/tmp/a.txt", 1_544_519_753)
|
||||
|
||||
"""
|
||||
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
|
||||
@@ -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
|
||||
@@ -1114,6 +1132,14 @@ defmodule File do
|
||||
explicitly disallow this behavior. If `source` is a `file` and `destination`
|
||||
is a directory, `{:error, :eisdir}` will be returned.
|
||||
|
||||
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.
|
||||
@@ -1127,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"
|
||||
@@ -1144,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}
|
||||
|
||||
@@ -1170,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
|
||||
@@ -1181,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
|
||||
|
||||
@@ -1204,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
|
||||
@@ -1221,15 +1275,28 @@ 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} ->
|
||||
do_cp_file(src, dest, on_conflict, acc)
|
||||
case do_cp_file(src, dest, on_conflict, acc) do
|
||||
# we don't have a way to make a distinction between a non-existing src
|
||||
# or dest being a non-existing dir in the case of :enoent,
|
||||
# but we already know that src exists here.
|
||||
{:error, :enoent, _} -> {:error, :enoent, dest}
|
||||
other -> other
|
||||
end
|
||||
|
||||
{: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)
|
||||
@@ -1243,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}
|
||||
@@ -1256,7 +1349,7 @@ defmodule File do
|
||||
end
|
||||
|
||||
{:ok, _} ->
|
||||
{:error, :eio, src}
|
||||
acc
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, src}
|
||||
@@ -1264,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
|
||||
|
||||
@@ -2159,7 +2252,12 @@ defmodule File do
|
||||
def stream!(path, line_or_bytes, modes)
|
||||
|
||||
def stream!(path, modes, line_or_bytes) when is_list(modes) do
|
||||
# TODO: Deprecate this on Elixir v1.20
|
||||
# TODO: Remove me on Elixir 2.0
|
||||
IO.warn(
|
||||
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
|
||||
"invoke File.stream!(path, line_or_bytes, modes) instead"
|
||||
)
|
||||
|
||||
stream!(path, line_or_bytes, modes)
|
||||
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
|
||||
|
||||
+137
-121
@@ -42,7 +42,7 @@ defmodule Float do
|
||||
|
||||
To learn more about floating-point arithmetic visit:
|
||||
|
||||
* [0.30000000000000004.com](http://0.30000000000000004.com/)
|
||||
* [0.30000000000000004.com](https://0.30000000000000004.com/)
|
||||
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
|
||||
|
||||
"""
|
||||
@@ -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.
|
||||
|
||||
@@ -210,13 +210,14 @@ defmodule GenServer do
|
||||
* [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the
|
||||
child should be restarted, defaults to `:permanent`
|
||||
* [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to
|
||||
shut down the child, either immediately or by giving it time to shut down
|
||||
shut down the child, either immediately or by giving it time to shut down,
|
||||
defaults to `5_000`
|
||||
|
||||
For example:
|
||||
|
||||
use GenServer, restart: :transient, shutdown: 10_000
|
||||
|
||||
See the ["Child specification"](`m:Supervisor#module-child_spec-1-function`) section in the `Supervisor` module for more
|
||||
See the ["Child specification"](`m:Supervisor#module-child-specification`) section in the `Supervisor` module for more
|
||||
detailed information. The `@doc` annotation immediately preceding
|
||||
`use GenServer` will be attached to the generated `child_spec/1` function.
|
||||
|
||||
@@ -231,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`.
|
||||
|
||||
@@ -349,6 +352,41 @@ defmodule GenServer do
|
||||
message arriving, `handle_info/2` is called with `:timeout` as the first
|
||||
argument.
|
||||
|
||||
For example:
|
||||
|
||||
defmodule Counter do
|
||||
use GenServer
|
||||
|
||||
@timeout to_timeout(second: 5)
|
||||
|
||||
@impl true
|
||||
def init(count) do
|
||||
{:ok, count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:increment, _from, count) do
|
||||
new_count = count + 1
|
||||
{:reply, new_count, new_count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:timeout, count) do
|
||||
{:stop, :normal, count}
|
||||
end
|
||||
end
|
||||
|
||||
A `Counter` server will exit with `:normal` if there are no messages in 5 seconds
|
||||
after the initialization or after the last `:increment` call:
|
||||
|
||||
{:ok, counter_pid} = GenServer.start(Counter, 50)
|
||||
GenServer.call(counter_pid, :increment)
|
||||
#=> 51
|
||||
|
||||
# After 5 seconds
|
||||
Process.alive?(counter_pid)
|
||||
#=> false
|
||||
|
||||
## When (not) to use a GenServer
|
||||
|
||||
So far, we have learned that a `GenServer` can be used as a supervised process
|
||||
@@ -488,7 +526,7 @@ defmodule GenServer do
|
||||
* [GenServer - Elixir's Getting Started Guide](genservers.md)
|
||||
* [`:gen_server` module documentation](`:gen_server`)
|
||||
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](https://learnyousomeerlang.com/clients-and-servers)
|
||||
|
||||
"""
|
||||
|
||||
@@ -529,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}
|
||||
@@ -822,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]
|
||||
|
||||
+19
-29
@@ -275,8 +275,6 @@ defprotocol Inspect do
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Atom do
|
||||
require Macro
|
||||
|
||||
def inspect(atom, opts) do
|
||||
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
|
||||
end
|
||||
@@ -569,6 +567,7 @@ defimpl Inspect, for: Regex do
|
||||
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
|
||||
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
|
||||
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
|
||||
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
|
||||
defp translate_options([], acc), do: acc
|
||||
defp translate_options(_t, _acc), do: :error
|
||||
|
||||
@@ -662,35 +661,18 @@ end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
def inspect(%module{} = struct, opts) do
|
||||
try do
|
||||
module.__info__(:struct)
|
||||
rescue
|
||||
_ -> Inspect.Map.inspect_as_map(struct, opts)
|
||||
else
|
||||
info ->
|
||||
if valid_struct?(info, struct) do
|
||||
info =
|
||||
for %{field: field} = map <- info,
|
||||
field != :__exception__,
|
||||
do: map
|
||||
info =
|
||||
for %{field: field} = map <- module.__info__(:struct),
|
||||
field != :__exception__,
|
||||
do: map
|
||||
|
||||
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
|
||||
else
|
||||
Inspect.Map.inspect_as_map(struct, opts)
|
||||
end
|
||||
end
|
||||
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
|
||||
end
|
||||
|
||||
defp valid_struct?(info, struct), do: valid_struct?(info, struct, map_size(struct) - 1)
|
||||
|
||||
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
|
||||
do: valid_struct?(info, struct, count - 1)
|
||||
|
||||
defp valid_struct?([], _struct, 0),
|
||||
do: true
|
||||
|
||||
defp valid_struct?(_fields, _struct, _count),
|
||||
do: false
|
||||
# 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)
|
||||
@@ -719,7 +701,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
|
||||
|
||||
@@ -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,
|
||||
@@ -115,11 +115,28 @@ defmodule Inspect.Opts do
|
||||
width: non_neg_integer | :infinity
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
Options for building an `Inspect.Opts` struct with `new/1`.
|
||||
"""
|
||||
@type new_opt ::
|
||||
{:base, :decimal | :binary | :hex | :octal}
|
||||
| {:binaries, :infer | :as_binaries | :as_strings}
|
||||
| {:charlists, :infer | :as_lists | :as_charlists}
|
||||
| {:custom_options, keyword}
|
||||
| {:inspect_fun, (any, t -> Inspect.Algebra.t())}
|
||||
| {:limit, non_neg_integer | :infinity}
|
||||
| {:pretty, boolean}
|
||||
| {:printable_limit, non_neg_integer | :infinity}
|
||||
| {:safe, boolean}
|
||||
| {:structs, boolean}
|
||||
| {:syntax_colors, [{color_key, IO.ANSI.ansidata()}]}
|
||||
| {:width, non_neg_integer | :infinity}
|
||||
|
||||
@doc """
|
||||
Builds an `Inspect.Opts` struct.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec new(keyword()) :: t
|
||||
@spec new([new_opt()]) :: t
|
||||
def new(opts) do
|
||||
struct(%Inspect.Opts{inspect_fun: default_inspect_fun()}, opts)
|
||||
end
|
||||
@@ -324,6 +341,14 @@ defmodule Inspect.Algebra do
|
||||
quote do: {:doc_color, unquote(doc), unquote(color)}
|
||||
end
|
||||
|
||||
@typedoc """
|
||||
Options for container documents.
|
||||
"""
|
||||
@type container_opts :: [
|
||||
separator: String.t(),
|
||||
break: :strict | :flex | :maybe
|
||||
]
|
||||
|
||||
@docs [
|
||||
:doc_break,
|
||||
:doc_collapse,
|
||||
@@ -371,7 +396,7 @@ defmodule Inspect.Algebra do
|
||||
def to_doc_with_opts(term, opts)
|
||||
|
||||
def to_doc_with_opts(%_{} = struct, %Inspect.Opts{inspect_fun: fun} = opts) do
|
||||
if opts.structs do
|
||||
if opts.structs and valid_struct?(struct) do
|
||||
try do
|
||||
fun.(struct, opts)
|
||||
rescue
|
||||
@@ -428,6 +453,26 @@ defmodule Inspect.Algebra do
|
||||
fun.(arg, opts) |> pack_opts(opts)
|
||||
end
|
||||
|
||||
defp valid_struct?(%module{} = struct) do
|
||||
try do
|
||||
module.__info__(:struct)
|
||||
rescue
|
||||
_ -> false
|
||||
else
|
||||
info ->
|
||||
valid_struct?(info, struct, map_size(struct) - 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
|
||||
do: valid_struct?(info, struct, count - 1)
|
||||
|
||||
defp valid_struct?([], _struct, 0),
|
||||
do: true
|
||||
|
||||
defp valid_struct?(_fields, _struct, _count),
|
||||
do: false
|
||||
|
||||
defp pack_opts({_doc, %Inspect.Opts{}} = doc_opts, _opts), do: doc_opts
|
||||
defp pack_opts(doc, opts), do: {doc, opts}
|
||||
|
||||
@@ -440,7 +485,14 @@ defmodule Inspect.Algebra do
|
||||
updated options from inspection.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
|
||||
@spec container_doc(
|
||||
t,
|
||||
[term],
|
||||
t,
|
||||
Inspect.Opts.t(),
|
||||
(term, Inspect.Opts.t() -> t),
|
||||
container_opts()
|
||||
) ::
|
||||
t
|
||||
def container_doc(left, collection, right, inspect_opts, fun, opts \\ []) do
|
||||
container_doc_with_opts(left, collection, right, inspect_opts, fun, opts) |> elem(0)
|
||||
@@ -496,7 +548,7 @@ defmodule Inspect.Algebra do
|
||||
t,
|
||||
Inspect.Opts.t(),
|
||||
(term, Inspect.Opts.t() -> t),
|
||||
keyword()
|
||||
container_opts()
|
||||
) ::
|
||||
{t, Inspect.Opts.t()}
|
||||
def container_doc_with_opts(left, collection, right, inspect_opts, fun, opts \\ [])
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -172,6 +211,35 @@ defmodule Integer do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a ceiled integer division.
|
||||
|
||||
Raises an `ArithmeticError` exception if one of the arguments is not an
|
||||
integer, or when the `divisor` is `0`.
|
||||
|
||||
This function performs a *ceiled* integer division, which means that
|
||||
the result will always be rounded towards positive infinity.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.ceil_div(5, 2)
|
||||
3
|
||||
iex> Integer.ceil_div(6, -4)
|
||||
-1
|
||||
iex> Integer.ceil_div(-99, 2)
|
||||
-49
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec ceil_div(integer, neg_integer | pos_integer) :: integer
|
||||
def ceil_div(dividend, divisor) do
|
||||
if not :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
|
||||
div(dividend, divisor) + 1
|
||||
else
|
||||
div(dividend, divisor)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the ordered digits for the given `integer`.
|
||||
|
||||
@@ -229,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)
|
||||
@@ -241,7 +310,7 @@ defmodule Integer do
|
||||
An optional `base` to the corresponding integer can be provided.
|
||||
If `base` is not given, 10 will be used.
|
||||
|
||||
If successful, returns a tuple in the form of `{integer, remainder_of_binary}`.
|
||||
If successful, returns a tuple in the form of `{integer, remaining_string}`.
|
||||
Otherwise `:error`.
|
||||
|
||||
Raises an error if `base` is less than 2 or more than 36.
|
||||
@@ -260,6 +329,9 @@ defmodule Integer do
|
||||
iex> Integer.parse("three")
|
||||
:error
|
||||
|
||||
iex> Integer.parse("404 not found")
|
||||
{404, " not found"}
|
||||
|
||||
iex> Integer.parse("34", 10)
|
||||
{34, ""}
|
||||
|
||||
@@ -460,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}
|
||||
|
||||
@@ -469,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)
|
||||
|
||||
+61
-14
@@ -128,6 +128,22 @@ defmodule IO do
|
||||
@type nodata :: {:error, term} | :eof
|
||||
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
|
||||
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
|
||||
|
||||
@typedoc """
|
||||
Stacktrace information as keyword options for `warn/2`.
|
||||
|
||||
At least `:file` is required. Other options are optional and used
|
||||
to provide more precise location information.
|
||||
"""
|
||||
@type warn_stacktrace_opts :: [
|
||||
file: String.t(),
|
||||
line: pos_integer(),
|
||||
column: pos_integer(),
|
||||
module: module(),
|
||||
function: {atom(), arity()}
|
||||
]
|
||||
|
||||
defguardp is_device(term) when is_atom(term) or is_pid(term)
|
||||
defguardp is_iodata(data) when is_list(data) or is_binary(data)
|
||||
|
||||
@@ -346,7 +362,10 @@ defmodule IO do
|
||||
#=> my_app.ex:4: MyApp.main/1
|
||||
|
||||
"""
|
||||
@spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
|
||||
@spec warn(
|
||||
chardata | String.Chars.t(),
|
||||
Exception.stacktrace() | warn_stacktrace_opts() | Macro.Env.t()
|
||||
) ::
|
||||
:ok
|
||||
def warn(message, stacktrace_info)
|
||||
|
||||
@@ -448,13 +467,15 @@ defmodule IO do
|
||||
|
||||
## Examples
|
||||
|
||||
The following code:
|
||||
|
||||
IO.inspect(<<0, 1, 2>>, width: 40)
|
||||
|
||||
Prints:
|
||||
|
||||
<<0, 1, 2>>
|
||||
|
||||
We can use the `:label` option to decorate the output:
|
||||
You can use the `:label` option to decorate the output:
|
||||
|
||||
IO.inspect(1..100, label: "a wonderful range")
|
||||
|
||||
@@ -462,21 +483,23 @@ defmodule IO do
|
||||
|
||||
a wonderful range: 1..100
|
||||
|
||||
The `:label` option is especially useful with pipelines:
|
||||
Inspect truncates large inputs by default. The `:printable_limit` controls
|
||||
the limit for strings and other string-like constructs (such as charlists):
|
||||
|
||||
[1, 2, 3]
|
||||
|> IO.inspect(label: "before")
|
||||
|> Enum.map(&(&1 * 2))
|
||||
|> IO.inspect(label: "after")
|
||||
|> Enum.sum()
|
||||
"abc"
|
||||
|> String.duplicate(9001)
|
||||
|> IO.inspect(printable_limit: :infinity)
|
||||
|
||||
Prints:
|
||||
For containers such as lists, maps, and tuples, the number of entries
|
||||
is managed by the `:limit` option:
|
||||
|
||||
before: [1, 2, 3]
|
||||
after: [2, 4, 6]
|
||||
1..100
|
||||
|> Enum.map(& {&1, &1})
|
||||
|> Enum.into(%{})
|
||||
|> IO.inspect(limit: :infinity)
|
||||
|
||||
"""
|
||||
@spec inspect(item, keyword) :: item when item: var
|
||||
@spec inspect(item, inspect_opts) :: item when item: var
|
||||
def inspect(item, opts \\ []) do
|
||||
inspect(:stdio, item, opts)
|
||||
end
|
||||
@@ -486,9 +509,10 @@ defmodule IO do
|
||||
|
||||
See `inspect/2` for a full list of options.
|
||||
"""
|
||||
@spec inspect(device, item, keyword) :: item when item: var
|
||||
@spec inspect(device, item, inspect_opts) :: item when item: var
|
||||
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
|
||||
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
|
||||
{label, opts} = Keyword.pop(opts, :label)
|
||||
label = if label, do: [to_chardata(label), ": "], else: []
|
||||
opts = Inspect.Opts.new(opts)
|
||||
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
|
||||
chardata = Inspect.Algebra.format(doc, opts.width)
|
||||
@@ -772,6 +796,29 @@ defmodule IO do
|
||||
:erlang.iolist_size(iodata)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if an IO data (the length is zero).
|
||||
|
||||
For more information about IO data, see the ["IO data"](#module-io-data)
|
||||
section in the module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.iodata_empty?([])
|
||||
true
|
||||
iex> IO.iodata_empty?([""])
|
||||
true
|
||||
iex> IO.iodata_empty?([1, 2 | <<3, 4>>])
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec iodata_empty?(iodata) :: boolean
|
||||
def iodata_empty?(""), do: true
|
||||
def iodata_empty?([]), do: true
|
||||
def iodata_empty?([head | tail]), do: iodata_empty?(head) and iodata_empty?(tail)
|
||||
def iodata_empty?(_), do: false
|
||||
|
||||
@doc false
|
||||
def each_stream(device, line_or_codepoints) do
|
||||
case read(device, line_or_codepoints) do
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@type print_opts :: [
|
||||
enabled: boolean(),
|
||||
doc_bold: [IO.ANSI.ansicode()],
|
||||
doc_code: [IO.ANSI.ansicode()],
|
||||
doc_headings: [IO.ANSI.ansicode()],
|
||||
doc_metadata: [IO.ANSI.ansicode()],
|
||||
doc_quote: [IO.ANSI.ansicode()],
|
||||
doc_inline_code: [IO.ANSI.ansicode()],
|
||||
doc_table_heading: [IO.ANSI.ansicode()],
|
||||
doc_title: [IO.ANSI.ansicode()],
|
||||
doc_underline: [IO.ANSI.ansicode()],
|
||||
width: pos_integer()
|
||||
]
|
||||
|
||||
@bullet_text_unicode "• "
|
||||
@bullet_text_ascii "* "
|
||||
@bullets [?*, ?-, ?+]
|
||||
@@ -30,7 +44,7 @@ defmodule IO.ANSI.Docs do
|
||||
Values for the color settings are strings with
|
||||
comma-separated ANSI values.
|
||||
"""
|
||||
@spec default_options() :: keyword
|
||||
@spec default_options() :: print_opts
|
||||
def default_options do
|
||||
[
|
||||
enabled: true,
|
||||
@@ -52,7 +66,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
@spec print_headings([String.t()], keyword) :: :ok
|
||||
@spec print_headings([String.t()], print_opts) :: :ok
|
||||
def print_headings(headings, options \\ []) do
|
||||
# It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
|
||||
# breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
|
||||
@@ -77,7 +91,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
@spec print_metadata(map, keyword) :: :ok
|
||||
@spec print_metadata(map, print_opts) :: :ok
|
||||
def print_metadata(metadata, options \\ []) when is_map(metadata) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
print_each_metadata(metadata, options) && IO.write("\n")
|
||||
@@ -115,7 +129,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
It takes a set of `options` defined in `default_options/0`.
|
||||
"""
|
||||
@spec print(term(), String.t(), keyword) :: :ok
|
||||
@spec print(term(), String.t(), print_opts) :: :ok
|
||||
def print(doc, format, options \\ [])
|
||||
|
||||
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
|
||||
|
||||
+39
-18
@@ -8,24 +8,29 @@ defprotocol JSON.Encoder do
|
||||
If you have a struct, you can derive the implementation of this protocol
|
||||
by specifying which fields should be encoded to JSON:
|
||||
|
||||
@derive {JSON.Encoder, only: [....]}
|
||||
@derive {JSON.Encoder, only: [...]}
|
||||
defstruct ...
|
||||
|
||||
It is also possible to encode all fields or skip some fields via the
|
||||
`:except` option:
|
||||
Additionally, you can exclude specific fields using the `:except` option or
|
||||
encode all fields by omitting both options entirely, but these should be used
|
||||
with caution:
|
||||
|
||||
@derive {JSON.Encoder, except: [...]}
|
||||
defstruct ...
|
||||
|
||||
@derive JSON.Encoder
|
||||
defstruct ...
|
||||
|
||||
> #### Leaking Private Information {: .error}
|
||||
>
|
||||
> The `:except` approach should be used carefully to avoid
|
||||
> accidentally leaking private information when new fields are added.
|
||||
> Prefer using `:only` to avoid accidentally leaking private information when
|
||||
> new fields are added. Other approaches should be used with auction.
|
||||
|
||||
Finally, if you don't own the struct you want to encode to JSON,
|
||||
you may use `Protocol.derive/3` placed outside of any module:
|
||||
You can also use `Protocol.derive/3` if you don't own the struct that you want
|
||||
to encode to JSON:
|
||||
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, except: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct)
|
||||
|
||||
"""
|
||||
@@ -62,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)
|
||||
|
||||
@@ -125,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
|
||||
|
||||
@@ -328,6 +333,22 @@ defmodule JSON do
|
||||
| {:invalid_byte, non_neg_integer(), byte()}
|
||||
| {:unexpected_sequence, non_neg_integer(), binary()}
|
||||
|
||||
@typedoc """
|
||||
Decoders for customizing JSON decoding behavior.
|
||||
"""
|
||||
@type decoders :: [
|
||||
array_start: (term() -> term()),
|
||||
array_push: (term(), term() -> term()),
|
||||
array_finish: (term(), term() -> {term(), term()}),
|
||||
object_start: (term() -> term()),
|
||||
object_push: (term(), term(), term() -> term()),
|
||||
object_finish: (term(), term() -> {term(), term()}),
|
||||
float: (String.t() -> term()),
|
||||
integer: (String.t() -> term()),
|
||||
string: (String.t() -> term()),
|
||||
null: term()
|
||||
]
|
||||
|
||||
@doc ~S"""
|
||||
Decodes the given JSON.
|
||||
|
||||
@@ -381,13 +402,13 @@ defmodule JSON do
|
||||
|
||||
For streaming decoding, see Erlang's [`:json`](`:json`) module.
|
||||
"""
|
||||
@spec decode(binary(), term(), keyword()) ::
|
||||
@spec decode(binary(), term(), decoders()) ::
|
||||
{term(), term(), binary()} | {:error, decode_error_reason()}
|
||||
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) 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)}}
|
||||
@@ -507,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)
|
||||
|
||||
+141
-125
@@ -231,7 +231,7 @@ defmodule Kernel do
|
||||
|
||||
Finally, note there is an overall structural sorting order, called
|
||||
"Term Ordering", defined below. This order is provided for reference
|
||||
purposes, it is not required by Elixir developers to know it by heart.
|
||||
purposes, it is not required for Elixir developers to know it by heart.
|
||||
|
||||
### Term ordering
|
||||
|
||||
@@ -1999,6 +1999,12 @@ defmodule Kernel do
|
||||
{:case, extra ++ meta, args}
|
||||
end
|
||||
|
||||
defp x_is_false_or_nil do
|
||||
quote generated: true do
|
||||
:erlang.orelse(:erlang."=:="(x, false), :erlang."=:="(x, nil))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Strictly boolean "or" operator.
|
||||
|
||||
@@ -2063,15 +2069,20 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp build_boolean_check(operator, check, true_clause, false_clause) do
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
bools =
|
||||
quote do
|
||||
case unquote(check) do
|
||||
false -> unquote(false_clause)
|
||||
true -> unquote(true_clause)
|
||||
other -> :erlang.error({:badbool, unquote(operator), other})
|
||||
end
|
||||
false -> unquote(false_clause)
|
||||
true -> unquote(true_clause)
|
||||
end
|
||||
|
||||
error =
|
||||
quote generated: true do
|
||||
other -> :erlang.error({:badbool, unquote(operator), other})
|
||||
end
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, operator}],
|
||||
{:case, [], [check, [do: bools ++ error]]}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -2098,10 +2109,10 @@ 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 :"Elixir.Kernel".in(x, [false, nil]) -> false
|
||||
x when unquote(x_is_false_or_nil()) -> false
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
@@ -2112,10 +2123,10 @@ 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 :"Elixir.Kernel".in(x, [false, nil]) -> true
|
||||
x when unquote(x_is_false_or_nil()) -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2456,7 +2467,7 @@ defmodule Kernel do
|
||||
See the "Deriving" section of the documentation of the `Inspect`
|
||||
protocol for more information.
|
||||
"""
|
||||
@spec inspect(Inspect.t(), keyword) :: String.t()
|
||||
@spec inspect(Inspect.t(), [Inspect.Opts.new_opt()]) :: String.t()
|
||||
def inspect(term, opts \\ []) when is_list(opts) do
|
||||
opts = Inspect.Opts.new(opts)
|
||||
|
||||
@@ -2741,7 +2752,7 @@ defmodule Kernel do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(term) do
|
||||
%_{__exception__: true} -> true
|
||||
%_{__exception__: _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2753,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
|
||||
@@ -2781,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
|
||||
|
||||
@@ -2799,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
|
||||
@@ -2815,7 +2824,7 @@ defmodule Kernel do
|
||||
This is most commonly used in pipelines, using the `|>/2` operator, allowing you
|
||||
to pipe a value to a function outside of its first argument.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> 1 |> then(fn x -> x * 2 end)
|
||||
2
|
||||
@@ -3524,8 +3533,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
A convenience macro that checks if the right side (an expression) matches the
|
||||
left side (a pattern).
|
||||
A convenience macro that checks if the result of `expression` matches `pattern`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -3603,7 +3611,7 @@ defmodule Kernel do
|
||||
#=> (MatchError) no match of right hand side value: %{x: 1, y: 2}
|
||||
|
||||
"""
|
||||
defmacro match?(pattern, expr) do
|
||||
defmacro match?(pattern, expression) do
|
||||
success =
|
||||
quote do
|
||||
unquote(pattern) -> true
|
||||
@@ -3614,7 +3622,7 @@ defmodule Kernel do
|
||||
_ -> false
|
||||
end
|
||||
|
||||
{:case, [], [expr, [do: success ++ failure]]}
|
||||
{:case, [], [expression, [do: success ++ failure]]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -3814,19 +3822,6 @@ defmodule Kernel do
|
||||
{_, doc} when doc_attr? ->
|
||||
do_at_escape(name, doc)
|
||||
|
||||
%{__struct__: Regex, source: source, opts: opts} = regex ->
|
||||
# TODO: Remove this in Elixir v2.0
|
||||
IO.warn(
|
||||
"storing and reading regexes from module attributes is deprecated, " <>
|
||||
"inline the regex inside the function definition instead",
|
||||
env
|
||||
)
|
||||
|
||||
case :erlang.system_info(:otp_release) < [?2, ?8] do
|
||||
true -> do_at_escape(name, regex)
|
||||
false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
|
||||
end
|
||||
|
||||
value ->
|
||||
do_at_escape(name, value)
|
||||
end
|
||||
@@ -3872,7 +3867,9 @@ defmodule Kernel do
|
||||
|
||||
defp do_at_escape(name, value) do
|
||||
try do
|
||||
:elixir_quote.escape(value, :none, false)
|
||||
# mark module attrs as shallow-generated since the ast for their representation
|
||||
# might contain opaque terms
|
||||
Macro.escape(value, generated: true)
|
||||
rescue
|
||||
ex in [ArgumentError] ->
|
||||
raise ArgumentError,
|
||||
@@ -4055,10 +4052,10 @@ 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 :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
|
||||
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
|
||||
_ -> unquote(do_clause)
|
||||
end
|
||||
end
|
||||
@@ -4106,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
|
||||
@@ -4376,10 +4379,10 @@ 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 :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
x
|
||||
|
||||
_ ->
|
||||
@@ -4419,10 +4422,10 @@ 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 :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
unquote(right)
|
||||
|
||||
x ->
|
||||
@@ -4700,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 ->
|
||||
@@ -4722,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)
|
||||
@@ -4757,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
|
||||
@@ -4770,8 +4763,8 @@ defmodule Kernel do
|
||||
defp in_range(left, first, last, step) do
|
||||
quoted =
|
||||
quote do
|
||||
:erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
|
||||
:erlang.is_integer(unquote(last)) and
|
||||
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and
|
||||
unquote(generated_is_integer(last)) and
|
||||
((:erlang.>(unquote(step), 0) and
|
||||
unquote(increasing_compare(left, first, last))) or
|
||||
(:erlang.<(unquote(step), 0) and
|
||||
@@ -4787,9 +4780,9 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_literal(left, first, last, step) when step > 0 do
|
||||
quoted =
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
:erlang.is_integer(unquote(left)),
|
||||
quote generated: true do
|
||||
Kernel.and(
|
||||
unquote(generated_is_integer(left)),
|
||||
unquote(increasing_compare(left, first, last))
|
||||
)
|
||||
end
|
||||
@@ -4799,9 +4792,9 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_literal(left, first, last, step) when step < 0 do
|
||||
quoted =
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
:erlang.is_integer(unquote(left)),
|
||||
quote generated: true do
|
||||
Kernel.and(
|
||||
unquote(generated_is_integer(left)),
|
||||
unquote(decreasing_compare(left, first, last))
|
||||
)
|
||||
end
|
||||
@@ -4815,36 +4808,28 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_step(quoted, left, first, step) do
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
Kernel.and(
|
||||
unquote(quoted),
|
||||
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
|
||||
)
|
||||
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])
|
||||
:lists.foldl("e(do: :erlang.orelse(unquote(&2), unquote(&1))), 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)))
|
||||
|
||||
[tail_head | tail] ->
|
||||
quote do
|
||||
:erlang.orelse(
|
||||
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
|
||||
:erlang.orelse(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
:lists.member(unquote(left), unquote(tail))
|
||||
unquote(in_list(left, tail_head, tail, expand, right))
|
||||
)
|
||||
end
|
||||
|
||||
@@ -4853,13 +4838,17 @@ 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
|
||||
|
||||
defp generated_is_integer(arg) do
|
||||
quote generated: true, do: :erlang.is_integer(unquote(arg))
|
||||
end
|
||||
|
||||
defp increasing_compare(var, first, last) do
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
Kernel.and(
|
||||
:erlang.>=(unquote(var), unquote(first)),
|
||||
:erlang."=<"(unquote(var), unquote(last))
|
||||
)
|
||||
@@ -4868,7 +4857,7 @@ defmodule Kernel do
|
||||
|
||||
defp decreasing_compare(var, first, last) do
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
Kernel.and(
|
||||
:erlang."=<"(unquote(var), unquote(first)),
|
||||
:erlang.>=(unquote(var), unquote(last))
|
||||
)
|
||||
@@ -5197,7 +5186,7 @@ defmodule Kernel do
|
||||
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
|
||||
|
||||
%{} ->
|
||||
:elixir_quote.escape(block, :none, false)
|
||||
:elixir_quote.escape(block, :escape, false)
|
||||
end
|
||||
|
||||
versioned_vars = env.versioned_vars
|
||||
@@ -5225,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: []
|
||||
|
||||
@@ -5477,7 +5471,7 @@ defmodule Kernel do
|
||||
store =
|
||||
case unquoted_expr or unquoted_call do
|
||||
true ->
|
||||
:elixir_quote.escape({call, expr}, :none, true)
|
||||
:elixir_quote.escape({call, expr}, :escape, true)
|
||||
|
||||
false ->
|
||||
key = :erlang.unique_integer()
|
||||
@@ -5605,8 +5599,8 @@ defmodule Kernel do
|
||||
## Types
|
||||
|
||||
It is recommended to define types for structs. By convention, such a type
|
||||
is called `t`. To define a struct inside a type, the struct literal syntax
|
||||
is used:
|
||||
is called `t`. To define a type for a struct, the struct literal syntax is
|
||||
used:
|
||||
|
||||
defmodule User do
|
||||
defstruct name: "John", age: 25
|
||||
@@ -5877,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)
|
||||
@@ -5889,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()
|
||||
@@ -6328,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
|
||||
|
||||
@@ -6389,7 +6394,7 @@ defmodule Kernel do
|
||||
|
||||
With a timeout:
|
||||
|
||||
iex> to_timeout(5400000)
|
||||
iex> to_timeout(5_400_000)
|
||||
5400000
|
||||
iex> to_timeout(:infinity)
|
||||
:infinity
|
||||
@@ -6417,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
|
||||
|
||||
@@ -6630,11 +6643,13 @@ defmodule Kernel do
|
||||
defmacro sigil_r(term, modifiers)
|
||||
|
||||
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
|
||||
binary = :elixir_interpolation.unescape_string(binary, ®ex_unescape_map/1)
|
||||
compile_regex(binary, options)
|
||||
end
|
||||
|
||||
defmacro sigil_r({:<<>>, meta, pieces}, options) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
|
||||
tuple = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)}
|
||||
compile_regex(tuple, options)
|
||||
end
|
||||
@@ -6653,13 +6668,14 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp compile_regex(binary_or_tuple, options) do
|
||||
# TODO: Remove this when we require Erlang/OTP 28+
|
||||
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
|
||||
bin_opts = :binary.list_to_bin(options)
|
||||
|
||||
case is_binary(binary_or_tuple) do
|
||||
true ->
|
||||
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
|
||||
Macro.escape(Regex.compile!(binary_or_tuple, bin_opts))
|
||||
|
||||
false ->
|
||||
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
|
||||
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(bin_opts)))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -21,6 +21,15 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :references, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate a require
|
||||
must be warned if unused.
|
||||
"""
|
||||
def warn_require(pid, meta, module, alias) do
|
||||
:gen_server.cast(pid, {:warn_require, module, meta, alias})
|
||||
module
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an alias
|
||||
must be warned if unused.
|
||||
@@ -57,6 +66,11 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.cast(pid, {:add_export, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_require(pid, module, meta) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_require, module, meta})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_import(pid, module, fas, meta, warn) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
|
||||
@@ -119,12 +133,18 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :unused_aliases, @timeout)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_requires(pid) do
|
||||
:gen_server.call(pid, :unused_requires, @timeout)
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
def init(:ok) do
|
||||
state = %{
|
||||
aliases: %{},
|
||||
imports: %{},
|
||||
requires: %{},
|
||||
references: %{},
|
||||
exports: %{},
|
||||
cache: %{},
|
||||
@@ -150,6 +170,18 @@ defmodule Kernel.LexicalTracker do
|
||||
{:reply, Enum.sort(imports), state}
|
||||
end
|
||||
|
||||
def handle_call(:unused_requires, _from, state) do
|
||||
%{references: references, aliases: aliases} = state
|
||||
|
||||
unused_requires =
|
||||
for {module, {meta, alias}} <- state.requires,
|
||||
Map.get(references, module) != :compile do
|
||||
{module, meta, alias, Map.get(aliases, alias) == :used}
|
||||
end
|
||||
|
||||
{:reply, Enum.sort(unused_requires), state}
|
||||
end
|
||||
|
||||
def handle_call(:references, _from, state) do
|
||||
{compile, runtime} = partition(Map.to_list(state.references), [], [])
|
||||
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
|
||||
@@ -245,6 +277,10 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, put_in(state.imports[module][@warn_key], true)}
|
||||
end
|
||||
|
||||
def handle_cast({:warn_require, module, meta, alias}, state) do
|
||||
{:noreply, put_in(state.requires[module], {meta, alias})}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
|
||||
@@ -16,11 +16,43 @@ defmodule Kernel.ParallelCompiler do
|
||||
@type warning() :: {file :: Path.t(), Code.position(), message :: String.t()}
|
||||
@type error() :: {file :: Path.t(), Code.position(), message :: String.t()}
|
||||
|
||||
@typedoc """
|
||||
Options for parallel compilation functions.
|
||||
"""
|
||||
@type compile_opts :: [
|
||||
after_compile: (-> term()),
|
||||
each_file: (Path.t() -> term()),
|
||||
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
|
||||
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
|
||||
each_module: (Path.t(), module(), binary() -> term()),
|
||||
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
long_compilation_threshold: pos_integer(),
|
||||
long_verification_threshold: pos_integer(),
|
||||
verification: boolean(),
|
||||
profile: :time,
|
||||
dest: Path.t(),
|
||||
beam_timestamp: term(),
|
||||
return_diagnostics: boolean(),
|
||||
max_concurrency: pos_integer(),
|
||||
purge_compiler_modules: boolean()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for requiring files in parallel.
|
||||
"""
|
||||
@type require_opts :: [
|
||||
each_file: (Path.t() -> term()),
|
||||
each_module: (Path.t(), module(), binary() -> term()),
|
||||
max_concurrency: pos_integer(),
|
||||
return_diagnostics: boolean()
|
||||
]
|
||||
|
||||
@doc """
|
||||
Starts a task for parallel compilation.
|
||||
"""
|
||||
# TODO: Deprecate this on Elixir v1.20.
|
||||
@doc deprecated: "Use `pmap/2` instead"
|
||||
# TODO: Remove me on Elixir 2.0
|
||||
@deprecated "Use `pmap/2` instead"
|
||||
def async(fun) when is_function(fun, 0) do
|
||||
{ref, task} = inner_async(fun)
|
||||
send(task.pid, ref)
|
||||
@@ -114,10 +146,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
the current file stops being compiled until the dependency is
|
||||
resolved.
|
||||
|
||||
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
|
||||
by default but we recommend using `return_diagnostics: true` so it returns
|
||||
diagnostics as maps as well as a map of compilation information.
|
||||
The map has the shape of:
|
||||
It must be invoked with `return_diagnostics: true` as option, so it returns
|
||||
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
|
||||
where `warnings_info` has the shape:
|
||||
|
||||
%{
|
||||
runtime_warnings: [warning],
|
||||
@@ -169,6 +200,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
|
||||
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
|
||||
after compilation (see `Code.purge_compiler_modules/0`)
|
||||
|
||||
* `:dest` - the destination directory for the BEAM files. When using `compile/2`,
|
||||
this information is only used to properly annotate the BEAM files before
|
||||
they are loaded into memory. If you want a file to actually be written to
|
||||
@@ -177,15 +211,16 @@ defmodule Kernel.ParallelCompiler do
|
||||
* `:beam_timestamp` - the modification timestamp to give all BEAM files
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
|
||||
* `:max_concurrency` - the maximum number of files to compile in parallel.
|
||||
Setting this option to 1 will compile files sequentially.
|
||||
Defaults to the number of schedulers online, or at least 2.
|
||||
Defaults to the number of schedulers online, or at least `2`.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec compile([Path.t()], keyword()) ::
|
||||
@spec compile([Path.t()], compile_opts()) ::
|
||||
{:ok, [atom], [warning] | info()}
|
||||
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
|
||||
def compile(files, options \\ []) when is_list(options) do
|
||||
@@ -198,7 +233,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
See `compile/2` for more information.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec compile_to_path([Path.t()], Path.t(), keyword()) ::
|
||||
@spec compile_to_path([Path.t()], Path.t(), compile_opts()) ::
|
||||
{:ok, [atom], [warning] | info()}
|
||||
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
|
||||
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
|
||||
@@ -211,10 +246,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
Opposite to compile, dependencies are not attempted to be
|
||||
automatically solved between files.
|
||||
|
||||
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
|
||||
by default but we recommend using `return_diagnostics: true` so it returns
|
||||
diagnostics as maps as well as a map of compilation information.
|
||||
The map has the shape of:
|
||||
It must be invoked with `return_diagnostics: true` as option, so it returns
|
||||
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
|
||||
where `warnings_info` has the shape:
|
||||
|
||||
%{
|
||||
runtime_warnings: [warning],
|
||||
@@ -231,11 +265,15 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:max_concurrency` - the maximum number of files to compile in parallel.
|
||||
Setting this option to 1 will compile files sequentially.
|
||||
Defaults to the number of schedulers online, or at least 2.
|
||||
Defaults to the number of schedulers online, or at least `2`.
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec require([Path.t()], keyword()) ::
|
||||
@spec require([Path.t()], require_opts()) ::
|
||||
{:ok, [atom], [warning] | info()}
|
||||
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
|
||||
def require(files, options \\ []) when is_list(options) do
|
||||
@@ -286,7 +324,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
if Keyword.get(options, :return_diagnostics, false) do
|
||||
{status, modules_or_errors, info}
|
||||
else
|
||||
IO.warn("you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler")
|
||||
IO.warn(
|
||||
"you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions"
|
||||
)
|
||||
|
||||
to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)
|
||||
|
||||
modules_or_errors =
|
||||
@@ -298,7 +339,14 @@ 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
|
||||
fn -> :elixir_code_server.cast(:purge_compiler_modules) end
|
||||
else
|
||||
fn -> :ok end
|
||||
end
|
||||
|
||||
{outcome, state} =
|
||||
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
|
||||
@@ -315,7 +363,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
long_compilation_threshold: threshold,
|
||||
schedulers: schedulers,
|
||||
checker: checker,
|
||||
verification?: Keyword.get(options, :verification, true)
|
||||
verification?: Keyword.get(options, :verification, true),
|
||||
purge_compiler_modules: purge_compiler_modules
|
||||
})
|
||||
|
||||
Process.cancel_timer(state.timer_ref)
|
||||
@@ -342,29 +391,75 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, {:compile, path}, timestamp) do
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
defp write_module_binaries(result, {:compile, path}, state) when map_size(result) > 0 do
|
||||
profile(state, "writing modules to disk", fn ->
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
timestamp = state.beam_timestamp
|
||||
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result do
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
module
|
||||
end
|
||||
# We fan-out the writes as that improves performance
|
||||
# when writing hundreds of beam files. This is cheap as
|
||||
# we only transfer atoms and binaries across processes.
|
||||
pool_size = min(map_size(result), state.schedulers)
|
||||
|
||||
pool_list =
|
||||
for _ <- 1..pool_size do
|
||||
spawn_link(fn -> write_loop(path, timestamp) end)
|
||||
end
|
||||
|
||||
pool_tuple = List.to_tuple(pool_list)
|
||||
|
||||
{modules, _} =
|
||||
Enum.flat_map_reduce(result, 0, fn
|
||||
{{:module, module}, {binary, _}}, scheduler when is_binary(binary) ->
|
||||
send(elem(pool_tuple, scheduler), {:write, module, binary})
|
||||
{[module], rem(scheduler + 1, pool_size)}
|
||||
|
||||
_, scheduler ->
|
||||
{[], scheduler}
|
||||
end)
|
||||
|
||||
pool_refs =
|
||||
for pid <- pool_list do
|
||||
ref = Process.monitor(pid)
|
||||
send(pid, :done)
|
||||
ref
|
||||
end
|
||||
|
||||
for ref <- pool_refs do
|
||||
receive do
|
||||
{:DOWN, ^ref, _, _, _} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
modules
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _output, _timestamp) do
|
||||
defp write_module_binaries(result, _output, _state) do
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
|
||||
end
|
||||
|
||||
defp write_loop(path, timestamp) do
|
||||
receive do
|
||||
{:write, module, binary} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary, [:raw])
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
write_loop(path, timestamp)
|
||||
|
||||
:done ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
## Verification
|
||||
|
||||
defp verify_modules(result, compile_warnings, dependent_modules, state) do
|
||||
modules = write_module_binaries(result, state.output, state.beam_timestamp)
|
||||
_ = state.after_compile.()
|
||||
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,
|
||||
@@ -375,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}
|
||||
@@ -486,11 +589,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
case cycle_return do
|
||||
{:runtime, dependent_modules, extra_warnings} ->
|
||||
:elixir_code_server.cast(:purge_compiler_modules)
|
||||
state.purge_compiler_modules.()
|
||||
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
|
||||
|
||||
{:compile, [], extra_warnings} ->
|
||||
:elixir_code_server.cast(:purge_compiler_modules)
|
||||
state.purge_compiler_modules.()
|
||||
verify_modules(result, extra_warnings ++ warnings, [], state)
|
||||
|
||||
{:compile, more, extra_warnings} ->
|
||||
@@ -737,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)
|
||||
|
||||
@@ -795,8 +898,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp return_error(warnings, errors, state, fun) do
|
||||
# Also prune compiler modules in case of errors
|
||||
:elixir_code_server.cast(:purge_compiler_modules)
|
||||
state.purge_compiler_modules.()
|
||||
|
||||
errors =
|
||||
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
|
||||
|
||||
@@ -1593,9 +1593,9 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
Let's give it a try on IEx:
|
||||
|
||||
iex> opts = %{width: 10, height: 15}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
iex> opts = %{"width" => 10, "height" => 15}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
...> {:ok, height} <- Map.fetch(opts, "height") do
|
||||
...> {:ok, width * height}
|
||||
...> end
|
||||
{:ok, 150}
|
||||
@@ -1603,21 +1603,13 @@ defmodule Kernel.SpecialForms do
|
||||
If all clauses match, the `do` block is executed, returning its result.
|
||||
Otherwise the chain is aborted and the non-matched value is returned:
|
||||
|
||||
iex> opts = %{width: 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
iex> opts = %{"width" => 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
...> {:ok, height} <- Map.fetch(opts, "height") do
|
||||
...> {:ok, width * height}
|
||||
...> end
|
||||
:error
|
||||
|
||||
Guards can be used in patterns as well:
|
||||
|
||||
iex> users = %{"melany" => "guest", "bob" => :admin}
|
||||
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
|
||||
...> {:ok, to_string(role)}
|
||||
...> end
|
||||
{:ok, "admin"}
|
||||
|
||||
As in `for/1`, variables bound inside `with/1` won't be accessible
|
||||
outside of `with/1`.
|
||||
|
||||
@@ -1661,22 +1653,18 @@ defmodule Kernel.SpecialForms do
|
||||
An `else` option can be given to modify what is being returned from
|
||||
`with` in the case of a failed match:
|
||||
|
||||
iex> opts = %{width: 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
...> {:ok, width * height}
|
||||
...> else
|
||||
...> :error ->
|
||||
...> {:error, :wrong_data}
|
||||
...>
|
||||
...> _other_error ->
|
||||
...> :unexpected_error
|
||||
...> end
|
||||
{:error, :wrong_data}
|
||||
with {:ok, content} <- File.read(path),
|
||||
:ok <- File.write(path, [content, "!"]) do
|
||||
:ok
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.error("could not append ! to \#{path} with reason: \#{reason}")
|
||||
:error
|
||||
end
|
||||
|
||||
The `else` block works like a `case`: it can have multiple clauses,
|
||||
and the first match will be used. Variables bound inside `with` (such as
|
||||
`width` in this example) are not available in the `else` block.
|
||||
and the first match will be used. Variables bound inside `with`
|
||||
(such as `content` in this example) are not available in the `else` block.
|
||||
|
||||
If an `else` block is used and there are no matching clauses, a `WithClauseError`
|
||||
exception is raised.
|
||||
@@ -1987,13 +1975,13 @@ defmodule Kernel.SpecialForms do
|
||||
While it is not possible to match against multiple patterns in a single
|
||||
clause, it's possible to match against multiple values by using guards:
|
||||
|
||||
iex> case :two do
|
||||
...> value when value in [:one, :two] ->
|
||||
iex> case 2 do
|
||||
...> value when value in [1, 2] ->
|
||||
...> "#{value} has been matched"
|
||||
...> :three ->
|
||||
...> "three has been matched"
|
||||
...> 3 ->
|
||||
...> "3 has been matched"
|
||||
...> end
|
||||
"two has been matched"
|
||||
"2 has been matched"
|
||||
"""
|
||||
defmacro case(condition, clauses), do: error!([condition, clauses])
|
||||
|
||||
@@ -2355,7 +2343,7 @@ defmodule Kernel.SpecialForms do
|
||||
defmacro try(args), do: error!([args])
|
||||
|
||||
@doc """
|
||||
Checks if there is a message matching any of the given clauses in the current
|
||||
Consumes the first message matching any of the given clauses in the current
|
||||
process mailbox.
|
||||
|
||||
If there is no matching message, the current process waits until a matching
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -877,7 +887,15 @@ defmodule Kernel.Typespec do
|
||||
|
||||
defp typespec({:fun, meta, args}, vars, caller, state) do
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{{:type, location(meta), :fun, args}, state}
|
||||
|
||||
if args != [] do
|
||||
IO.warn(
|
||||
"fun/#{length(args)} is not valid in typespecs. Either specify fun() or use (... -> return) instead",
|
||||
caller
|
||||
)
|
||||
end
|
||||
|
||||
{{:type, location(meta), :fun, []}, state}
|
||||
end
|
||||
|
||||
defp typespec({:..., _meta, _args}, _vars, caller, _state) do
|
||||
|
||||
@@ -126,10 +126,11 @@ defmodule Kernel.Utils do
|
||||
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
|
||||
|
||||
try do
|
||||
:elixir_quote.escape(val, :none, false)
|
||||
:elixir_quote.escape(val, {:struct, module}, false)
|
||||
rescue
|
||||
e in [ArgumentError] ->
|
||||
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
|
||||
raise ArgumentError,
|
||||
"invalid default value for struct field #{key}, " <> Exception.message(e)
|
||||
else
|
||||
_ -> {key, val}
|
||||
end
|
||||
@@ -171,7 +172,7 @@ defmodule Kernel.Utils do
|
||||
|
||||
:lists.foreach(foreach, enforce_keys)
|
||||
struct = :maps.from_list([__struct__: module] ++ fields)
|
||||
escaped_struct = :elixir_quote.escape(struct, :none, false)
|
||||
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
|
||||
|
||||
body =
|
||||
case bootstrapped? do
|
||||
@@ -217,7 +218,7 @@ defmodule Kernel.Utils do
|
||||
case enforce_keys -- :maps.keys(struct) do
|
||||
[] ->
|
||||
mapper = fn {key, val} ->
|
||||
%{field: key, default: val}
|
||||
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
|
||||
end
|
||||
|
||||
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
|
||||
@@ -265,7 +266,7 @@ defmodule Kernel.Utils do
|
||||
module.exception([])
|
||||
end
|
||||
|
||||
def raise(%_{__exception__: true} = exception) do
|
||||
def raise(%_{__exception__: _} = exception) do
|
||||
exception
|
||||
end
|
||||
|
||||
@@ -338,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
|
||||
@@ -368,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) ->
|
||||
|
||||
+29
-19
@@ -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
|
||||
@@ -437,7 +446,7 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value from `key` and updates it, all in one pass.
|
||||
Gets the value for `key` and updates it in one pass, deleting duplicate keys.
|
||||
|
||||
The `fun` argument receives the value of `key` (or `nil` if `key`
|
||||
is not present) and must return a two-element tuple: the current value
|
||||
@@ -483,7 +492,7 @@ defmodule Keyword do
|
||||
defp get_and_update([{key, current} | t], acc, key, fun) do
|
||||
case fun.(current) do
|
||||
{get, value} ->
|
||||
{get, :lists.reverse(acc, [{key, value} | t])}
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{current, :lists.reverse(acc, t)}
|
||||
@@ -509,7 +518,8 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value under `key` and updates it. Raises if there is no `key`.
|
||||
Gets the value for `key` and updates it in one pass, deleting duplicate keys,
|
||||
raising if `key` can't be found in `keywords`.
|
||||
|
||||
The `fun` argument receives the value under `key` and must return a
|
||||
two-element tuple: the current value (the retrieved value, which can be
|
||||
@@ -545,21 +555,21 @@ defmodule Keyword do
|
||||
get_and_update!(keywords, key, fun, [])
|
||||
end
|
||||
|
||||
defp get_and_update!([{key, value} | keywords], key, fun, acc) do
|
||||
defp get_and_update!([{key, value} | t], key, fun, acc) do
|
||||
case fun.(value) do
|
||||
{get, value} ->
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{value, :lists.reverse(acc, keywords)}
|
||||
{value, :lists.reverse(acc, t)}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
|
||||
get_and_update!(keywords, key, fun, [e | acc])
|
||||
defp get_and_update!([{_, _} = h | t], key, fun, acc) do
|
||||
get_and_update!(t, key, fun, [h | acc])
|
||||
end
|
||||
|
||||
defp get_and_update!([], key, _fun, acc) when is_atom(key) do
|
||||
@@ -955,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])
|
||||
|
||||
+61
-5
@@ -187,9 +187,10 @@ defmodule List do
|
||||
"""
|
||||
@spec duplicate(any, 0) :: []
|
||||
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
|
||||
def duplicate(elem, n) do
|
||||
:lists.duplicate(n, elem)
|
||||
end
|
||||
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, [])
|
||||
|
||||
defp duplicate(0, _elem, acc), do: acc
|
||||
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists.
|
||||
@@ -297,6 +298,33 @@ defmodule List do
|
||||
def first([], default), do: default
|
||||
def first([head | _], _default), do: head
|
||||
|
||||
@doc """
|
||||
Returns the first element in `list`.
|
||||
|
||||
If `list` is empty, an error is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.first!([1])
|
||||
1
|
||||
|
||||
iex> List.first!([1, 2, 3])
|
||||
1
|
||||
|
||||
iex> List.first!([])
|
||||
** (ArgumentError) attempted to get the first element of an empty list
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec first!([elem, ...]) :: elem when elem: var
|
||||
def first!(list)
|
||||
|
||||
def first!([head | _]), do: head
|
||||
|
||||
def first!([]) do
|
||||
raise ArgumentError, "attempted to get the first element of an empty list"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list` or `default` if `list` is empty.
|
||||
|
||||
@@ -325,6 +353,34 @@ defmodule List do
|
||||
def last([head], _default), do: head
|
||||
def last([_ | tail], default), do: last(tail, default)
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list`.
|
||||
|
||||
If `list` is empty, an error is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.last!([1])
|
||||
1
|
||||
|
||||
iex> List.last!([1, 2, 3])
|
||||
3
|
||||
|
||||
iex> List.last!([])
|
||||
** (ArgumentError) attempted to get the last element of an empty list
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec last!([elem, ...]) :: elem when elem: var
|
||||
def last!(list)
|
||||
|
||||
def last!([head]), do: head
|
||||
def last!([_ | tail]), do: last!(tail)
|
||||
|
||||
def last!([]) do
|
||||
raise ArgumentError, "attempted to get the last element of an empty list"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns the first tuple
|
||||
where the element at `position` in the tuple matches the
|
||||
@@ -915,7 +971,7 @@ defmodule List do
|
||||
|
||||
If `prefix` is an empty list, it returns `true`.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> List.starts_with?([1, 2, 3], [1, 2])
|
||||
true
|
||||
@@ -945,7 +1001,7 @@ defmodule List do
|
||||
|
||||
If `suffix` is an empty list, it returns `true`.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> List.ends_with?([1, 2, 3], [2, 3])
|
||||
true
|
||||
|
||||
@@ -19,12 +19,6 @@ defprotocol List.Chars do
|
||||
"""
|
||||
@spec to_charlist(t) :: charlist
|
||||
def to_charlist(term)
|
||||
|
||||
@doc false
|
||||
@deprecated "Use List.Chars.to_charlist/1 instead"
|
||||
Kernel.def to_char_list(term) do
|
||||
__MODULE__.to_charlist(term)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Atom do
|
||||
|
||||
+166
-56
@@ -197,6 +197,16 @@ defmodule Macro do
|
||||
@typedoc "A captured remote function in the format of &Mod.fun/arity"
|
||||
@type captured_remote_function :: fun
|
||||
|
||||
@type escape_opts :: [
|
||||
unquote: boolean(),
|
||||
prune_metadata: boolean(),
|
||||
generated: boolean()
|
||||
]
|
||||
|
||||
@type inspect_atom_opts :: [
|
||||
escape: (binary(), char() -> binary())
|
||||
]
|
||||
|
||||
@doc """
|
||||
Breaks a pipeline expression into a list.
|
||||
|
||||
@@ -498,13 +508,11 @@ defmodule Macro do
|
||||
Generates AST nodes for a given number of required argument
|
||||
variables using `Macro.unique_var/2`.
|
||||
|
||||
The second argument is generally the macro caller's module.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
|
||||
iex> {:arg1, [counter: c1], __MODULE__} = var1
|
||||
iex> {:arg2, [counter: c2], __MODULE__} = var2
|
||||
iex> is_integer(c1) and is_integer(c2)
|
||||
true
|
||||
[var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module)
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@@ -560,11 +568,11 @@ defmodule Macro do
|
||||
generate another variable, with its own unique counter.
|
||||
See `var/2` for an alternative.
|
||||
|
||||
The second argument is generally the macro caller's module.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
|
||||
iex> is_integer(c)
|
||||
true
|
||||
var = Macro.unique_var(:foo, __CALLER__.module)
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@@ -793,12 +801,18 @@ defmodule Macro do
|
||||
* `:unquote` - when `true`, this function leaves `unquote/1` and
|
||||
`unquote_splicing/1` expressions unescaped, effectively unquoting
|
||||
the contents on escape. This option is useful only when escaping
|
||||
ASTs which may have quoted fragments in them. Defaults to `false`.
|
||||
ASTs which may have quoted fragments in them. Note this option
|
||||
will give a special meaning to `quote`/`unquote` nodes, which need
|
||||
to be valid AST before escaping. Defaults to `false`.
|
||||
|
||||
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
|
||||
nodes. Note this option changes the semantics of escaped code and
|
||||
it should only be used when escaping ASTs. Defaults to `false`.
|
||||
|
||||
* `:generated` - (since v1.19.0) Whether the AST should be considered as generated
|
||||
by the compiler or not. This means the compiler and tools like Dialyzer may not
|
||||
emit certain warnings.
|
||||
|
||||
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
|
||||
assertion, so when an assertion fails we can show code snippets to users.
|
||||
Without this option, each time the test module is compiled, we would get a
|
||||
@@ -834,12 +848,82 @@ defmodule Macro do
|
||||
`escape/2` is used to escape *values* (either directly passed or variable
|
||||
bound), while `quote/2` produces syntax trees for
|
||||
expressions.
|
||||
|
||||
## Dealing with references and other runtime values
|
||||
|
||||
Macros work at compile-time and therefore `Macro.escape/1` can only escape values
|
||||
that are valid during compilation, such as numbers, atoms, tuples, maps, binaries,
|
||||
etc.
|
||||
|
||||
However, you may have values at compile-time which cannot be escaped, such as
|
||||
`reference`s and `pid`s, since the process or memory address they point to will
|
||||
no longer exist once compilation completes. Attempting to escape said values will
|
||||
raise an exception. This is a common issue when working with NIFs.
|
||||
|
||||
Luckily, Elixir v1.19 introduces a mechanism that allows those values to be escaped,
|
||||
as long as they are encapsulated by a struct within a module that defines the
|
||||
`__escape__/1` function. This is possible as long as the reference has a natural
|
||||
text or binary representation that can be serialized during compilation.
|
||||
|
||||
Let's imagine we have the following struct:
|
||||
|
||||
defmodule WrapperStruct do
|
||||
defstruct [:ref]
|
||||
|
||||
def new(...), do: %WrapperStruct{ref: ...}
|
||||
|
||||
# efficiently dump to / load from binaries
|
||||
def dump_to_binary(%WrapperStruct{ref: ref}), do: ...
|
||||
def load_from_binary(binary), do: %WrapperStruct{ref: ...}
|
||||
end
|
||||
|
||||
Such a struct could not be used in module attributes or escaped with `Macro.escape/2`:
|
||||
|
||||
defmodule Foo do
|
||||
@my_struct WrapperStruct.new(...)
|
||||
def my_struct, do: @my_struct
|
||||
end
|
||||
|
||||
** (ArgumentError) cannot inject attribute @my_struct into function/macro because cannot escape #Reference<...>
|
||||
|
||||
To address this, structs can re-define how they should be escaped by defining a custom
|
||||
`__escape__/1` function which returns the AST. In our example:
|
||||
|
||||
defmodule WrapperStruct do
|
||||
# ...
|
||||
|
||||
def __escape__(struct) do
|
||||
# dump to a binary representation at compile-time
|
||||
binary = dump_to_binary(struct)
|
||||
quote do
|
||||
# load from the binary representation at runtime
|
||||
WrapperStruct.load_from_binary(unquote(Macro.escape(binary)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Now, our example above will be expanded as:
|
||||
|
||||
def my_struct, do: WrapperStruct.load_from_binary(<<...>>)
|
||||
|
||||
When implementing `__escape__/1`, you must ensure that the quoted expression
|
||||
will evaluate to a struct that represents the one given as argument.
|
||||
"""
|
||||
@spec escape(term, keyword) :: t()
|
||||
@spec escape(term, escape_opts) :: t()
|
||||
def escape(expr, opts \\ []) do
|
||||
unquote = Keyword.get(opts, :unquote, false)
|
||||
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
|
||||
:elixir_quote.escape(expr, kind, unquote)
|
||||
kind = if Keyword.get(opts, :prune_metadata, false), do: :escape_and_prune, else: :escape
|
||||
generated = Keyword.get(opts, :generated, false)
|
||||
|
||||
case :elixir_quote.escape(expr, kind, unquote) do
|
||||
# mark module attrs as shallow-generated since the ast for their representation
|
||||
# might contain opaque terms
|
||||
{caller, meta, args} when generated and is_list(meta) ->
|
||||
{caller, [generated: true] ++ meta, args}
|
||||
|
||||
ast ->
|
||||
ast
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on Elixir v1.22
|
||||
@@ -852,25 +936,50 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts the struct information (equivalent to calling
|
||||
`module.__info__(:struct)`).
|
||||
Extracts the struct information.
|
||||
|
||||
This is useful when a struct needs to be expanded at
|
||||
compilation time and the struct being expanded may or may
|
||||
not have been compiled. This function is also capable of
|
||||
expanding structs defined under the module being compiled.
|
||||
not have been compiled (including structs in the defined
|
||||
under the module being compiled). For compiled modules,
|
||||
it will invoke `module.__info__(:struct)`.
|
||||
|
||||
Calling this function also adds an export dependency on the
|
||||
given struct.
|
||||
|
||||
It will raise `ArgumentError` if the struct is not available.
|
||||
|
||||
## Compatibility considerations
|
||||
|
||||
This function currently returns both `:required` and `:default`
|
||||
entries for each field. While this naming is inconsistent
|
||||
(a required field should not have a default), this is done for
|
||||
backwards compatibility purposes.
|
||||
|
||||
In future releases, Elixir may introduce truly required struct
|
||||
fields, the required field will be removed and default will be
|
||||
present only if the field is optional. Your code should prepare
|
||||
for such scenario accordingly.
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec struct_info!(module(), Macro.Env.t()) ::
|
||||
[%{field: atom(), required: boolean(), default: term()}]
|
||||
[
|
||||
%{
|
||||
required(:field) => atom(),
|
||||
optional(:required) => boolean(),
|
||||
optional(:default) => term()
|
||||
}
|
||||
]
|
||||
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
|
||||
|
||||
@@ -1738,12 +1847,17 @@ defmodule Macro do
|
||||
@doc """
|
||||
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
|
||||
|
||||
This is used when you want to programmatically invoke a macro at
|
||||
compile-time.
|
||||
This is used when you want to dynamically invoke a function at
|
||||
compile-time and force it to be tracked as a compile-time dependency.
|
||||
For example, this is used by `dbg/1` to force the `dbg_callback`
|
||||
configuration to be a compile-time dependency.
|
||||
|
||||
If you want to "invoke" a macro instead, remember macros are by
|
||||
definition compile-time, and you can use `Macro.expand/2`.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
def compile_apply(mod, fun, args, caller) do
|
||||
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
|
||||
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
|
||||
Kernel.apply(mod, fun, args)
|
||||
end
|
||||
|
||||
@@ -2082,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
|
||||
@@ -2399,7 +2513,7 @@ defmodule Macro do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
|
||||
@spec inspect_atom(:literal | :key | :remote_call, atom, inspect_atom_opts) :: binary
|
||||
def inspect_atom(source_format, atom, opts \\ [])
|
||||
|
||||
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
|
||||
@@ -2591,7 +2705,7 @@ defmodule Macro do
|
||||
:ok
|
||||
end
|
||||
|
||||
prelude = quote do: options = unquote(Macro.escape(options))
|
||||
prelude = quote do: options = unquote(options)
|
||||
acc = {prelude, dbg_format_header(env)}
|
||||
|
||||
{acc, nil} =
|
||||
@@ -2612,34 +2726,29 @@ defmodule Macro do
|
||||
# Pipelines.
|
||||
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
|
||||
value_var = unique_var(:value, __MODULE__)
|
||||
values_acc_var = unique_var(:values, __MODULE__)
|
||||
|
||||
[start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
|
||||
rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
|
||||
[start_ast | rest_asts] = for {ast, 0} <- unpipe(pipe_ast), do: ast
|
||||
piped_rest_asts = Enum.map(rest_asts, &{&1, pipe(value_var, &1, 0)})
|
||||
|
||||
initial_acc =
|
||||
first_entry =
|
||||
quote do
|
||||
unquote(value_var) = unquote(start_ast)
|
||||
unquote(values_acc_var) = [unquote(value_var)]
|
||||
{:multi_value, unquote(escape(start_ast)), unquote(value_var)}
|
||||
end
|
||||
|
||||
values_ast =
|
||||
for step_ast <- rest_asts, reduce: initial_acc do
|
||||
ast_acc ->
|
||||
quote do
|
||||
unquote(ast_acc)
|
||||
unquote(value_var) = unquote(step_ast)
|
||||
unquote(values_acc_var) = [unquote(value_var) | unquote(values_acc_var)]
|
||||
end
|
||||
end
|
||||
len = length(piped_rest_asts)
|
||||
|
||||
[
|
||||
quote do
|
||||
unquote(values_ast)
|
||||
pipe_entries =
|
||||
Enum.with_index(piped_rest_asts, fn {original_ast, step_ast}, i ->
|
||||
tag = if i + 1 == len, do: :pipe_end, else: :pipe
|
||||
|
||||
{:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
|
||||
end
|
||||
]
|
||||
quote do
|
||||
unquote(value_var) = unquote(step_ast)
|
||||
{unquote(tag), unquote(escape(original_ast)), unquote(value_var)}
|
||||
end
|
||||
end)
|
||||
|
||||
[first_entry | pipe_entries]
|
||||
end
|
||||
|
||||
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
|
||||
@@ -2859,18 +2968,19 @@ defmodule Macro do
|
||||
result
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
|
||||
result = List.last(values)
|
||||
code_strings = Enum.map(code_asts, &to_string_with_colors(&1, options))
|
||||
[{first_ast, first_value} | asts_with_values] = Enum.zip(code_strings, values)
|
||||
first_formatted = [dbg_format_ast(first_ast), " ", inspect(first_value, options), ?\n]
|
||||
defp dbg_format_ast_to_debug({:pipe, code_ast, value}, options) do
|
||||
formatted = [
|
||||
[:faint, "|> ", :reset],
|
||||
dbg_format_ast_with_value_no_newline(code_ast, value, options)
|
||||
]
|
||||
|
||||
rest_formatted =
|
||||
Enum.map(asts_with_values, fn {code_ast, value} ->
|
||||
[:faint, "|> ", :reset, dbg_format_ast(code_ast), " ", inspect(value, options), ?\n]
|
||||
end)
|
||||
{formatted, value}
|
||||
end
|
||||
|
||||
{[first_formatted | rest_formatted], result}
|
||||
defp dbg_format_ast_to_debug({:pipe_end, code_ast, value}, options) do
|
||||
{formatted, value} = dbg_format_ast_to_debug({:pipe, code_ast, value}, options)
|
||||
|
||||
{[formatted, ?\n], value}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do
|
||||
|
||||
+65
-13
@@ -70,6 +70,42 @@ defmodule Macro.Env do
|
||||
@typep tracers :: [module]
|
||||
@typep versioned_vars :: %{optional(variable) => var_version :: non_neg_integer}
|
||||
|
||||
@type define_import_opts :: [
|
||||
trace: boolean(),
|
||||
emit_warnings: boolean(),
|
||||
info_callback: (atom() -> [{atom(), arity()}]),
|
||||
only: :functions | :macros | [{atom(), arity()}],
|
||||
except: [{atom(), arity()}],
|
||||
warn: boolean()
|
||||
]
|
||||
|
||||
@type define_alias_opts :: [
|
||||
trace: boolean(),
|
||||
as: atom(),
|
||||
warn: boolean()
|
||||
]
|
||||
|
||||
@type define_require_opts :: [
|
||||
trace: boolean(),
|
||||
as: atom(),
|
||||
warn: boolean()
|
||||
]
|
||||
|
||||
@type expand_alias_opts :: [
|
||||
trace: boolean()
|
||||
]
|
||||
|
||||
@type expand_import_opts :: [
|
||||
allow_locals: boolean() | (-> function() | false),
|
||||
check_deprecations: boolean(),
|
||||
trace: boolean()
|
||||
]
|
||||
|
||||
@type expand_require_opts :: [
|
||||
check_deprecations: boolean(),
|
||||
trace: boolean()
|
||||
]
|
||||
|
||||
@type t :: %{
|
||||
__struct__: __MODULE__,
|
||||
aliases: aliases,
|
||||
@@ -264,7 +300,7 @@ defmodule Macro.Env do
|
||||
|
||||
iex> Macro.Env.required?(__ENV__, Integer)
|
||||
false
|
||||
iex> require Integer
|
||||
iex> require Integer, warn: false
|
||||
iex> Macro.Env.required?(__ENV__, Integer)
|
||||
true
|
||||
|
||||
@@ -331,7 +367,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_require(t, Macro.metadata(), module) :: {: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)
|
||||
@@ -391,7 +428,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
|
||||
@spec define_import(t, Macro.metadata(), module, define_import_opts) ::
|
||||
{:ok, t} | {:error, String.t()}
|
||||
def define_import(env, meta, module, opts \\ [])
|
||||
when is_list(meta) and is_atom(module) and is_list(opts) do
|
||||
{trace, opts} = Keyword.pop(opts, :trace, true)
|
||||
@@ -441,7 +479,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
|
||||
@spec define_alias(t, Macro.metadata(), module, define_alias_opts) ::
|
||||
{:ok, t} | {:error, String.t()}
|
||||
def define_alias(env, meta, module, opts \\ [])
|
||||
when is_list(meta) and is_atom(module) and is_list(opts) do
|
||||
{trace, opts} = Keyword.pop(opts, :trace, true)
|
||||
@@ -487,7 +526,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec expand_alias(t, keyword, [atom()], keyword) ::
|
||||
@spec expand_alias(t, keyword, [atom()], expand_alias_opts) ::
|
||||
{:alias, atom()} | :error
|
||||
def expand_alias(env, meta, list, opts \\ [])
|
||||
when is_list(meta) and is_list(list) and is_list(opts) do
|
||||
@@ -517,8 +556,15 @@ defmodule Macro.Env do
|
||||
|
||||
## Options
|
||||
|
||||
* `:allow_locals` - when set to `false`, it does not attempt to capture
|
||||
local macros defined in the current module in `env`
|
||||
* `:allow_locals` - controls how local macros are resolved.
|
||||
Defaults to `true`.
|
||||
|
||||
- When `false`, does not attempt to capture local macros defined in the
|
||||
current module in `env`
|
||||
- When `true`, uses a default resolver that looks for public macros in
|
||||
the current module
|
||||
- When a function, it will be invoked to lazily compute a local function
|
||||
(or return false). It has signature `(-> function() | false)`
|
||||
|
||||
* `:check_deprecations` - when set to `false`, does not check for deprecations
|
||||
when expanding macros
|
||||
@@ -527,7 +573,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
|
||||
@spec expand_import(t, keyword, atom(), arity(), expand_import_opts) ::
|
||||
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
|
||||
| {:function, module(), atom()}
|
||||
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
|
||||
@@ -542,10 +588,16 @@ defmodule Macro.Env do
|
||||
trace = Keyword.get(opts, :trace, true)
|
||||
module = env.module
|
||||
|
||||
# When allow_locals is a callback, we don't need to pass module macros as extra
|
||||
# because the callback will handle local macro resolution
|
||||
extra =
|
||||
case allow_locals and function_exported?(module, :__info__, 1) do
|
||||
true -> [{module, module.__info__(:macros)}]
|
||||
false -> []
|
||||
if is_function(allow_locals, 0) do
|
||||
[]
|
||||
else
|
||||
case allow_locals and function_exported?(module, :__info__, 1) do
|
||||
true -> [{module, module.__info__(:macros)}]
|
||||
false -> []
|
||||
end
|
||||
end
|
||||
|
||||
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
|
||||
@@ -583,7 +635,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
|
||||
@spec expand_require(t, keyword, module(), atom(), arity(), expand_require_opts) ::
|
||||
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
|
||||
| :error
|
||||
def expand_require(env, meta, module, name, arity, opts \\ [])
|
||||
@@ -606,7 +658,7 @@ defmodule Macro.Env do
|
||||
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
|
||||
end
|
||||
|
||||
quoted = expander.(args, env)
|
||||
quoted = expander.(:elixir_dispatch.stop_generated(args), env)
|
||||
next = :elixir_module.next_counter(env.module)
|
||||
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
|
||||
end
|
||||
|
||||
+104
-53
@@ -287,8 +287,13 @@ defmodule Map do
|
||||
@doc """
|
||||
Fetches the value for a specific `key` in the given `map`.
|
||||
|
||||
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
|
||||
If `map` doesn't contain `key`, `:error` is returned.
|
||||
If `map` contains the given `key` then its value is returned
|
||||
in the shape of `{:ok, value}`. If `map` doesn't contain `key`,
|
||||
`:error` is returned.
|
||||
|
||||
If the type system can verify `:error` is always returned
|
||||
(which means key is never available in the map), it will emit
|
||||
an error.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -296,7 +301,7 @@ defmodule Map do
|
||||
|
||||
iex> Map.fetch(%{a: 1}, :a)
|
||||
{:ok, 1}
|
||||
iex> Map.fetch(%{a: 1}, :b)
|
||||
iex> Map.fetch(%{"foo" => "bar"}, "unknown")
|
||||
:error
|
||||
|
||||
"""
|
||||
@@ -307,8 +312,11 @@ defmodule Map do
|
||||
Fetches the value for a specific `key` in the given `map`, erroring out if
|
||||
`map` doesn't contain `key`.
|
||||
|
||||
If `map` contains `key`, the corresponding value is returned. If
|
||||
`map` doesn't contain `key`, a `KeyError` exception is raised.
|
||||
The exclamation mark (`!`) implies this function can raise a `KeyError`
|
||||
exception at runtime if `map` doesn't contain `key`. If the type system
|
||||
can verify this function will always raise (which means the key is never
|
||||
available), then it will emit a warning at compile-time. See the "Type
|
||||
checking" section below.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -317,11 +325,54 @@ defmodule Map do
|
||||
iex> Map.fetch!(%{a: 1}, :a)
|
||||
1
|
||||
|
||||
When the key is missing, an exception is raised:
|
||||
|
||||
Map.fetch!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
## Type checking
|
||||
|
||||
The compiler will emit a warning if it can verify that
|
||||
none of the keys given are available in the map.
|
||||
|
||||
When the key is an atom, because only single key is given,
|
||||
a warning will be emitted in case the type system proves
|
||||
the key is not present.
|
||||
|
||||
However, this behaviour matters when the type of the key
|
||||
represents multiple values. For example:
|
||||
|
||||
key = returns_foo_or_bar() #=> :foo or :bar
|
||||
Map.fetch!(%{foo: 123}, key)
|
||||
|
||||
Although the key can be `:foo` or `:bar`, there is no
|
||||
warning emitted, as `:foo` will succeed. This is by design:
|
||||
the exclamation mark in Elixir denotes precisely that a
|
||||
runtime exception may be raised.
|
||||
|
||||
In case you are looking up multiple keys and you don't know
|
||||
if they may be present, you can use `Map.fetch/2` instead
|
||||
and deal with the error case accordingly:
|
||||
|
||||
case Map.fetch(%{foo: 123}, key) do
|
||||
{:ok, value} -> ...
|
||||
:error -> ...
|
||||
end
|
||||
|
||||
Both `Map.fetch!/2` and `Map.fetch/2` will emit a warning if
|
||||
it proves that both `:foo` or `:bar` are absent in the map.
|
||||
|
||||
Alternatively, if you want to statically prove that all of keys
|
||||
are in the map, you can match on the possible values and access
|
||||
them directly:
|
||||
|
||||
case returns_foo_or_bar() do
|
||||
:foo -> map.foo
|
||||
:bar -> map.bar
|
||||
end
|
||||
"""
|
||||
@spec fetch!(map, key) :: value
|
||||
def fetch!(map, key) do
|
||||
:maps.get(key, map)
|
||||
end
|
||||
def fetch!(map, key), do: :maps.get(key, map)
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key` unless the entry `key`
|
||||
@@ -357,8 +408,8 @@ defmodule Map do
|
||||
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace(%{a: 1}, :b, 2)
|
||||
%{a: 1}
|
||||
iex> Map.replace(%{"a" => 1}, "b", 2)
|
||||
%{"a" => 1}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@@ -379,7 +430,11 @@ defmodule Map do
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `map`.
|
||||
|
||||
If `key` is not present in `map`, a `KeyError` exception is raised.
|
||||
The exclamation mark (`!`) implies this function can raise a `KeyError`
|
||||
exception at runtime if `map` doesn't contain `key`. If the type system
|
||||
can verify this function will always raise (which means the key is never
|
||||
available), then it will emit a warning at compile-time. See the "Type
|
||||
checking" section in `Map.fetch!/2` for more information.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -388,8 +443,8 @@ defmodule Map do
|
||||
iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace!(%{a: 1}, :b, 2)
|
||||
** (KeyError) key :b not found in:
|
||||
iex> Map.replace!(%{"foo" => "bar"}, "unknown", "new_bar")
|
||||
** (KeyError) key "unknown" not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -412,8 +467,8 @@ defmodule Map do
|
||||
iex> Map.replace_lazy(%{a: 1, b: 2}, :a, fn v -> v * 4 end)
|
||||
%{a: 4, b: 2}
|
||||
|
||||
iex> Map.replace_lazy(%{a: 1, b: 2}, :c, fn v -> v * 4 end)
|
||||
%{a: 1, b: 2}
|
||||
iex> Map.replace_lazy(%{"a" => 1, "b" => 2}, "c", fn v -> v * 4 end)
|
||||
%{"a" => 1, "b" => 2}
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@@ -492,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
|
||||
@@ -516,15 +573,13 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.get(%{}, :a)
|
||||
nil
|
||||
iex> Map.get(%{a: 1}, :a)
|
||||
iex> Map.get(%{"a" => 1}, "a")
|
||||
1
|
||||
iex> Map.get(%{a: 1}, :b)
|
||||
iex> Map.get(%{"a" => 1}, "b")
|
||||
nil
|
||||
iex> Map.get(%{a: 1}, :b, 3)
|
||||
iex> Map.get(%{"a" => 1}, "b", 3)
|
||||
3
|
||||
iex> Map.get(%{a: nil}, :a, 1)
|
||||
iex> Map.get(%{"a" => nil}, "a", 1)
|
||||
nil
|
||||
|
||||
"""
|
||||
@@ -553,15 +608,11 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = %{a: 1}
|
||||
iex> fun = fn ->
|
||||
...> # some expensive operation here
|
||||
...> 13
|
||||
...> end
|
||||
iex> Map.get_lazy(map, :a, fun)
|
||||
iex> Map.get_lazy(%{a: 1}, :a, fn -> :expensive_value end)
|
||||
1
|
||||
iex> Map.get_lazy(map, :b, fun)
|
||||
13
|
||||
|
||||
iex> Map.get_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
|
||||
:expensive_value
|
||||
|
||||
"""
|
||||
@spec get_lazy(map, key, (-> value)) :: value
|
||||
@@ -700,10 +751,10 @@ defmodule Map do
|
||||
|
||||
iex> Map.pop(%{a: 1}, :a)
|
||||
{1, %{}}
|
||||
iex> Map.pop(%{a: 1}, :b)
|
||||
{nil, %{a: 1}}
|
||||
iex> Map.pop(%{a: 1}, :b, 3)
|
||||
{3, %{a: 1}}
|
||||
iex> Map.pop(%{"a" => 1}, "b")
|
||||
{nil, %{"a" => 1}}
|
||||
iex> Map.pop(%{"a" => 1}, "b", 3)
|
||||
{3, %{"a" => 1}}
|
||||
|
||||
"""
|
||||
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
|
||||
@@ -726,8 +777,8 @@ defmodule Map do
|
||||
{1, %{}}
|
||||
iex> Map.pop!(%{a: 1, b: 2}, :a)
|
||||
{1, %{b: 2}}
|
||||
iex> Map.pop!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in:
|
||||
iex> Map.pop!(%{"a" => 1}, "b")
|
||||
** (KeyError) key "b" not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -753,15 +804,11 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = %{a: 1}
|
||||
iex> fun = fn ->
|
||||
...> # some expensive operation here
|
||||
...> 13
|
||||
...> end
|
||||
iex> Map.pop_lazy(map, :a, fun)
|
||||
iex> Map.pop_lazy(%{a: 1}, :a, fn -> :expensive_value end)
|
||||
{1, %{}}
|
||||
iex> Map.pop_lazy(map, :b, fun)
|
||||
{13, %{a: 1}}
|
||||
|
||||
iex> Map.pop_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
|
||||
{:expensive_value, %{"a" => 1}}
|
||||
|
||||
"""
|
||||
@spec pop_lazy(map, key, (-> value)) :: {value, map}
|
||||
@@ -913,8 +960,8 @@ defmodule Map do
|
||||
iex> Map.update!(%{a: 1}, :a, &(&1 * 2))
|
||||
%{a: 2}
|
||||
|
||||
iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
|
||||
** (KeyError) key :b not found in:
|
||||
iex> Map.update!(%{"a" => 1}, "b", &(&1 * 2))
|
||||
** (KeyError) key "b" not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -1033,6 +1080,7 @@ defmodule Map do
|
||||
#=> %{name: "john"}
|
||||
|
||||
"""
|
||||
# TODO: implement this using row polymorphism
|
||||
@spec from_struct(atom | struct) :: map
|
||||
def from_struct(struct) when is_atom(struct) do
|
||||
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
|
||||
@@ -1061,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})
|
||||
@@ -1069,11 +1117,7 @@ defmodule Map do
|
||||
|
||||
"""
|
||||
@spec equal?(map, map) :: boolean
|
||||
def equal?(map1, map2)
|
||||
|
||||
def equal?(%{} = map1, %{} = map2), do: map1 === map2
|
||||
def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2])
|
||||
def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other])
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Kernel.map_size/1 instead"
|
||||
@@ -1094,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
|
||||
|
||||
@@ -1128,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)
|
||||
|
||||
@@ -55,7 +55,11 @@ defmodule MapSet do
|
||||
|
||||
@type value :: term
|
||||
|
||||
@opaque internal(value) :: :sets.set(value)
|
||||
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
|
||||
# either via module attributes or by the compiler.
|
||||
# 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)
|
||||
|
||||
|
||||
+84
-61
@@ -402,30 +402,21 @@ defmodule Module do
|
||||
|
||||
Accepts the function name (as an atom) of a function in the current module.
|
||||
The function must have an arity of 0 (no arguments). If the function does
|
||||
not return `:ok`, the loading of the module will be aborted.
|
||||
For example:
|
||||
not return `:ok`, the loading of the module will be aborted. Its primary
|
||||
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
|
||||
|
||||
defmodule MyModule do
|
||||
@on_load :load_check
|
||||
@on_load :load_external_code
|
||||
|
||||
def load_check do
|
||||
if some_condition() do
|
||||
:ok
|
||||
else
|
||||
:abort
|
||||
end
|
||||
end
|
||||
|
||||
def some_condition do
|
||||
false
|
||||
def load_external_code do
|
||||
:erlang.load_nif(~c"path/to/extension.so_or_dll")
|
||||
end
|
||||
end
|
||||
|
||||
The function given to `on_load` should avoid calling functions from
|
||||
other modules. If you must call functions in other modules and those
|
||||
modules are defined within the same project, the called modules must
|
||||
have the `@compile {:autoload, true}` annotation, so they are loaded
|
||||
upfront (and not from within the `@on_load` callback).
|
||||
other modules. This is because, when running a `mix release`,
|
||||
`on_load` runs extremely early, before any application starts running,
|
||||
and therefore even systems like the `Logger` and `IO` are not yet available.
|
||||
|
||||
### `@vsn`
|
||||
|
||||
@@ -560,15 +551,20 @@ defmodule Module do
|
||||
callback is invoked under different scenarios, Elixir provides no guarantees
|
||||
of when in the compilation cycle nor in which process the callback runs.
|
||||
|
||||
Furthermore, after verification callbacks are not expected to raise.
|
||||
Given they run after the code is compiled, artifacts have already been
|
||||
written to disk, and therefore raising does not effectively halt compilation
|
||||
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
|
||||
or other callback. Given modules have already been compiled, functions in
|
||||
this module, such as `get_attribute/2`, which expect modules to not have been
|
||||
yet compiled, do not work on `@after_verify` callback.
|
||||
|
||||
Accepts a module or a `{module, function_name}` tuple. The function
|
||||
must take one argument: the module name. When just a module is provided,
|
||||
the function is assumed to be `__after_verify__/1`.
|
||||
|
||||
Callbacks will run in the order they are registered.
|
||||
|
||||
`Module` functions expecting not yet compiled modules are no longer available
|
||||
at the time `@after_verify` is invoked.
|
||||
|
||||
#### Example
|
||||
|
||||
defmodule MyModule do
|
||||
@@ -660,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]
|
||||
@@ -681,12 +678,21 @@ defmodule Module do
|
||||
This function is generated for all modules. It's similar to `module_info/1` but
|
||||
includes some additional Elixir-specific information, such as struct and macro
|
||||
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 :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
generated: boolean()
|
||||
]
|
||||
|
||||
@type get_definition_opts :: [
|
||||
skip_clauses: boolean()
|
||||
]
|
||||
|
||||
@extra_error_msg_defines? "Use Kernel.function_exported?/3 and Kernel.macro_exported?/3 " <>
|
||||
"to check for public functions and macros instead"
|
||||
|
||||
@@ -711,7 +717,8 @@ defmodule Module do
|
||||
|
||||
* `:module` - the module atom name
|
||||
|
||||
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order
|
||||
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order.
|
||||
See `Macro.struct_info!/2` for more information
|
||||
|
||||
"""
|
||||
@callback __info__(:attributes) :: keyword()
|
||||
@@ -721,7 +728,14 @@ defmodule Module do
|
||||
@callback __info__(:md5) :: binary()
|
||||
@callback __info__(:module) :: module()
|
||||
@callback __info__(:struct) ::
|
||||
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
|
||||
[
|
||||
%{
|
||||
required(:field) => atom(),
|
||||
optional(:required) => boolean(),
|
||||
optional(:default) => term()
|
||||
}
|
||||
]
|
||||
| nil
|
||||
|
||||
@doc """
|
||||
Returns information about module attributes used by Elixir.
|
||||
@@ -918,7 +932,7 @@ defmodule Module do
|
||||
when defining the module, while `Kernel.defmodule/2`
|
||||
automatically uses the environment it is invoked at.
|
||||
"""
|
||||
@spec create(module, Macro.t(), Macro.Env.t() | keyword) :: {:module, module, binary, term}
|
||||
@spec create(module, Macro.t(), Macro.Env.t() | create_opts) :: {:module, module, binary, term}
|
||||
def create(module, quoted, opts)
|
||||
|
||||
def create(module, quoted, %Macro.Env{} = env) when is_atom(module) do
|
||||
@@ -945,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
|
||||
@@ -971,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
|
||||
@@ -1002,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
|
||||
@@ -1026,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
|
||||
@@ -1236,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
|
||||
@@ -1262,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
|
||||
@@ -1280,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
|
||||
|
||||
@@ -1293,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
|
||||
|
||||
@@ -1326,7 +1346,7 @@ defmodule Module do
|
||||
@doc """
|
||||
Returns all overridable definitions in `module`.
|
||||
|
||||
Note a definition is included even if it was was already overridden.
|
||||
Note a definition is included even if it was already overridden.
|
||||
You can use `defines?/2` to see if a definition exists or one is pending.
|
||||
|
||||
This function can only be used on modules that have not yet been compiled.
|
||||
@@ -1345,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)
|
||||
@@ -1394,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 """
|
||||
@@ -1423,21 +1443,22 @@ defmodule Module do
|
||||
only an interest in fetching the kind and the metadata
|
||||
|
||||
"""
|
||||
@spec get_definition(module, definition, keyword) ::
|
||||
@spec get_definition(module, definition, get_definition_opts) ::
|
||||
{:v1, def_kind, meta :: keyword,
|
||||
[{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}
|
||||
|
||||
@@ -1454,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 """
|
||||
@@ -1473,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 ->
|
||||
@@ -1495,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
|
||||
@@ -1553,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 """
|
||||
|
||||
@@ -181,8 +181,16 @@ defmodule Module.Behaviour do
|
||||
behaviour not in behaviours ->
|
||||
{:error, {:behaviour_not_declared, behaviour}}
|
||||
|
||||
not Code.ensure_loaded?(behaviour) ->
|
||||
# Module does not exist, but we have already warned about it.
|
||||
{:ok, []}
|
||||
|
||||
not behaviour_defined?(callbacks, behaviour) ->
|
||||
# Module does not define behaviour, but we have already warned about it.
|
||||
{:ok, []}
|
||||
|
||||
true ->
|
||||
{:error, {:behaviour_not_defined, behaviour, callbacks}}
|
||||
{:error, {:callback_not_defined, behaviour, callbacks}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -215,6 +223,18 @@ defmodule Module.Behaviour do
|
||||
end
|
||||
end
|
||||
|
||||
# Determines whether there is at least one callback defined for the given behaviour.
|
||||
# If not, that means that the behaviour has not been defined.
|
||||
defp behaviour_defined?(callbacks, behaviour) do
|
||||
callbacks
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.any?(fn
|
||||
{_kind, ^behaviour, _optional?} -> true
|
||||
{_kind, _behaviour, _optional?} -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
|
||||
when map_size(callbacks) == 0 do
|
||||
context
|
||||
@@ -390,13 +410,17 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
defp format_warning({:callback_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
behaviour_string = inspect(behaviour)
|
||||
|
||||
[
|
||||
"got \"@impl ",
|
||||
inspect(behaviour),
|
||||
behaviour_string,
|
||||
"\" for ",
|
||||
format_definition(kind, callback),
|
||||
" but this behaviour does not specify such callback",
|
||||
" but ",
|
||||
behaviour_string,
|
||||
" does not specify such callback",
|
||||
known_callbacks(callbacks)
|
||||
]
|
||||
end
|
||||
|
||||
@@ -4,16 +4,29 @@
|
||||
|
||||
defmodule Module.ParallelChecker do
|
||||
@moduledoc false
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
import Kernel, except: [spawn: 3]
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type error() :: term()
|
||||
@type mode() :: :erlang | :elixir | :protocol
|
||||
|
||||
@typedoc """
|
||||
Options for `start_link/1`.
|
||||
"""
|
||||
@type start_link_opts :: [
|
||||
{:max_concurrency, pos_integer()}
|
||||
| {:long_verification_threshold, pos_integer()}
|
||||
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
|
||||
| {atom(), term()}
|
||||
]
|
||||
|
||||
@doc """
|
||||
Initializes the parallel checker process.
|
||||
"""
|
||||
@spec start_link(start_link_opts()) :: {:ok, cache()}
|
||||
def start_link(opts \\ []) do
|
||||
:proc_lib.start_link(__MODULE__, :init, [opts])
|
||||
end
|
||||
@@ -51,14 +64,14 @@ defmodule Module.ParallelChecker do
|
||||
@doc """
|
||||
Spawns a process that runs the parallel checker.
|
||||
"""
|
||||
def spawn({pid, {checker, table}}, module, module_map, beam_location, log?) 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)
|
||||
cache_from_module_map(table, module_map, signatures)
|
||||
end
|
||||
|
||||
inner_spawn(pid, checker, table, module, info, log?)
|
||||
@@ -85,12 +98,14 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
cache_from_module_map(table, module_map)
|
||||
{: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}
|
||||
_ -> {:uncached, nil}
|
||||
end
|
||||
|
||||
is_tuple(info) ->
|
||||
@@ -106,14 +121,14 @@ defmodule Module.ParallelChecker do
|
||||
# Set the compiler info so we can collect warnings
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
warnings =
|
||||
{warnings, errors} =
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, table}, log?)
|
||||
else
|
||||
[]
|
||||
{[], []}
|
||||
end
|
||||
|
||||
send(pid, {__MODULE__, module, warnings})
|
||||
send(pid, {__MODULE__, module, warnings, errors})
|
||||
send(checker, {__MODULE__, :done, module})
|
||||
end
|
||||
|
||||
@@ -166,11 +181,11 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives pairs of module maps and BEAM binaries. In parallel it verifies
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
list of warnings from the verification.
|
||||
Receives pairs of module maps and BEAM binaries.
|
||||
|
||||
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)
|
||||
@@ -180,40 +195,37 @@ defmodule Module.ParallelChecker do
|
||||
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
|
||||
|
||||
@doc """
|
||||
Test cache.
|
||||
"""
|
||||
def test_cache do
|
||||
{:ok, cache} = start_link()
|
||||
cache
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the export kind and deprecation reason for the given MFA from
|
||||
the cache. If the module does not exist return `:badmodule`,
|
||||
@@ -264,18 +276,19 @@ defmodule Module.ParallelChecker do
|
||||
definitions
|
||||
)
|
||||
|
||||
diagnostics =
|
||||
{warnings, errors} =
|
||||
module
|
||||
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|
||||
|> 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
|
||||
@@ -326,16 +339,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)
|
||||
|
||||
@@ -353,7 +366,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)
|
||||
@@ -412,7 +425,7 @@ defmodule Module.ParallelChecker do
|
||||
mode =
|
||||
with {^module, binary, _filename} <- object_code,
|
||||
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(chunk) do
|
||||
# The chunk has more information, so that's our preference
|
||||
cache_chunk(table, module, contents)
|
||||
else
|
||||
@@ -463,12 +476,12 @@ defmodule Module.ParallelChecker do
|
||||
if Keyword.has_key?(attributes, :__protocol__), do: :protocol, else: :elixir
|
||||
end
|
||||
|
||||
defp cache_from_module_map(table, map) do
|
||||
defp cache_from_module_map(table, map, signatures) do
|
||||
exports =
|
||||
behaviour_exports(map) ++
|
||||
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
|
||||
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures)
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), signatures)
|
||||
{elixir_mode(map.attributes), module_map_to_module_tuple(map)}
|
||||
end
|
||||
|
||||
@@ -641,7 +654,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
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)
|
||||
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
|
||||
spawned = Map.put(state.spawned, module, timer)
|
||||
run_checkers(%{state | modules: modules, spawned: spawned})
|
||||
end
|
||||
|
||||
+309
-88
@@ -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
|
||||
@@ -24,20 +24,17 @@ defmodule Module.Types do
|
||||
#
|
||||
# * :infer - Same as :dynamic but skips remote calls.
|
||||
#
|
||||
# * :traversal - Focused mostly on traversing AST, skips most type system
|
||||
# operations. Used by macros and when skipping inference.
|
||||
#
|
||||
# The mode may also control exhaustiveness checks in the future (to be decided).
|
||||
# We may also want for applications with subtyping in dynamic mode to always
|
||||
# intersect with dynamic, but this mode may be too lax (to be decided based on
|
||||
# feedback).
|
||||
@modes [:static, :dynamic, :infer, :traversal]
|
||||
@modes [:static, :dynamic, :infer]
|
||||
|
||||
# These functions are not inferred because they are added/managed by the compiler
|
||||
@no_infer [behaviour_info: 1]
|
||||
|
||||
@doc false
|
||||
def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
|
||||
def infer(module, file, attrs, defs, used_private, env, {_, cache}) do
|
||||
# We don't care about inferring signatures for protocols,
|
||||
# those will be replaced anyway. There is also nothing to
|
||||
# infer if there is no cache system, we only do traversals.
|
||||
@@ -49,8 +46,8 @@ defmodule Module.Types do
|
||||
finder =
|
||||
fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, kind, _, _} = clause ->
|
||||
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
|
||||
{_, kind, _, _} = def ->
|
||||
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
|
||||
|
||||
false ->
|
||||
false
|
||||
@@ -75,23 +72,27 @@ defmodule Module.Types do
|
||||
|
||||
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
|
||||
|
||||
{types, %{local_sigs: reachable_sigs} = context} =
|
||||
for {fun_arity, kind, meta, _clauses} = def <- defs,
|
||||
kind in [:def, :defmacro],
|
||||
reduce: {[], context()} do
|
||||
{types, context} ->
|
||||
# Optimized version of finder, since we already the definition
|
||||
# In case there are loops, the other we traverse matters,
|
||||
# so we sort the definitions for determinism
|
||||
{types, private, %{local_sigs: reachable_sigs} = context} =
|
||||
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs),
|
||||
reduce: {[], [], context()} do
|
||||
{types, private, context} when kind in [:def, :defmacro] ->
|
||||
# Optimized version of finder, since we already have the definition
|
||||
finder = fn _ ->
|
||||
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
|
||||
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
|
||||
end
|
||||
|
||||
{_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], context}
|
||||
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
|
||||
else
|
||||
{types, context}
|
||||
{types, private, context}
|
||||
end
|
||||
|
||||
{types, private, context} ->
|
||||
{types, [def | private], context}
|
||||
end
|
||||
|
||||
# Now traverse all used privates to find any other private that have been used by them.
|
||||
@@ -105,8 +106,8 @@ defmodule Module.Types do
|
||||
|
||||
{unreachable, _context} =
|
||||
Enum.reduce(private, {[], context}, fn
|
||||
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
|
||||
warn_unused_def(info, used_sigs, env)
|
||||
{fun_arity, kind, meta, _clauses}, {unreachable, context} ->
|
||||
warn_unused_def(fun_arity, kind, meta, used_sigs, env)
|
||||
|
||||
# Find anything undefined within unused functions
|
||||
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
|
||||
@@ -125,7 +126,7 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
defp infer_mode(kind, infer_signatures?) do
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traverse
|
||||
end
|
||||
|
||||
defp protocol?(attrs) do
|
||||
@@ -146,12 +147,24 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp default_domain({_, arity} = fun_arity, impl) do
|
||||
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
|
||||
with {for, callbacks} <- impl,
|
||||
true <- fun_arity in callbacks do
|
||||
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
|
||||
args = [
|
||||
Descr.dynamic(Module.Types.Of.impl(for))
|
||||
| List.duplicate(Descr.dynamic(), arity - 1)
|
||||
]
|
||||
|
||||
{_fun_arity, kind, meta, clauses} = def
|
||||
|
||||
clauses =
|
||||
for {meta, args, guards, body} <- clauses do
|
||||
{[type_check: {:impl, for}] ++ meta, args, guards, body}
|
||||
end
|
||||
|
||||
{mode, {fun_arity, kind, meta, clauses}, args}
|
||||
else
|
||||
_ -> List.duplicate(Descr.dynamic(), arity)
|
||||
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -161,29 +174,30 @@ defmodule Module.Types do
|
||||
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
|
||||
end
|
||||
|
||||
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
|
||||
:ok
|
||||
end
|
||||
defp warn_unused_def(fun_arity, kind, meta, used, env) do
|
||||
default = Keyword.get(meta, :defaults, 0)
|
||||
|
||||
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
|
||||
case is_map_key(used, fun_arity) do
|
||||
true -> :ok
|
||||
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
end
|
||||
cond do
|
||||
Keyword.get(meta, :context) != nil or Keyword.get(meta, :from_super) == true ->
|
||||
:ok
|
||||
|
||||
:ok
|
||||
end
|
||||
default == 0 ->
|
||||
case is_map_key(used, fun_arity) do
|
||||
true -> :ok
|
||||
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
end
|
||||
|
||||
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
default > 0 ->
|
||||
{name, arity} = fun_arity
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
case min_reachable_default(max, min, :none, name, used) do
|
||||
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
|
||||
^min -> :ok
|
||||
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
|
||||
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
|
||||
case min_reachable_default(max, min, :none, name, used) do
|
||||
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
^min -> :ok
|
||||
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity})
|
||||
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff})
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
@@ -208,7 +222,7 @@ defmodule Module.Types do
|
||||
|
||||
finder = fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
|
||||
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
@@ -219,7 +233,7 @@ defmodule Module.Types do
|
||||
context =
|
||||
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
|
||||
# Optimized version of finder, since we already the definition
|
||||
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
|
||||
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
|
||||
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
context
|
||||
end)
|
||||
@@ -237,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}
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -279,7 +303,7 @@ defmodule Module.Types do
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
|
||||
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
@@ -292,36 +316,121 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
|
||||
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
|
||||
context =
|
||||
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
|
||||
Module.Types.Traverse.of_expr(body, stack, context)
|
||||
end)
|
||||
|
||||
inferred = {:infer, nil, [{List.duplicate(Descr.term(), arity), Descr.dynamic()}]}
|
||||
{inferred, [{0, 0}], context}
|
||||
end
|
||||
|
||||
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
|
||||
{fun, _arity} = fun_arity
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
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
|
||||
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} ->
|
||||
fresh_context = fresh_context(acc_context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
try do
|
||||
{trees, 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 =
|
||||
if stack.mode == :traversal do
|
||||
expected
|
||||
else
|
||||
Pattern.of_domain(trees, expected, context)
|
||||
end
|
||||
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 ->
|
||||
@@ -331,19 +440,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),
|
||||
@@ -355,6 +514,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}
|
||||
@@ -417,9 +628,8 @@ defmodule Module.Types do
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler,
|
||||
# Control if variable refinement is enabled.
|
||||
# It is disabled only on dynamic dispatches.
|
||||
refine_vars: true
|
||||
# Reverse arrow handling (nil | :cache | :use)
|
||||
reverse_arrow: nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -430,34 +640,45 @@ defmodule Module.Types do
|
||||
warnings: [],
|
||||
# All vars and their types
|
||||
vars: %{},
|
||||
# Variables and arguments from patterns
|
||||
# Stores special metadata used by list heads and domain keys in patterns
|
||||
subpatterns: %{},
|
||||
# Variables that are specific to the current environment/conditional
|
||||
conditional_vars: nil,
|
||||
# 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
|
||||
|
||||
|
||||
+1398
-243
File diff suppressed because it is too large
Load Diff
+4483
-1720
File diff suppressed because it is too large
Load Diff
+693
-358
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ defmodule Module.Types.Helpers do
|
||||
@doc """
|
||||
Returns true if the mode cares about warnings.
|
||||
"""
|
||||
defguard is_warning(stack) when stack.mode not in [:traversal, :infer]
|
||||
defguard is_warning(stack) when stack.mode != :infer
|
||||
|
||||
@doc """
|
||||
Guard function to check if an AST node is a variable.
|
||||
@@ -91,29 +91,6 @@ defmodule Module.Types.Helpers do
|
||||
"var.fun()" (with parentheses) means "var" is an atom()
|
||||
"""
|
||||
|
||||
:interpolation ->
|
||||
"""
|
||||
|
||||
#{hint()} string interpolation uses the String.Chars protocol to \
|
||||
convert a data structure into a string. Either convert the data type into a \
|
||||
string upfront or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:generator ->
|
||||
"""
|
||||
|
||||
#{hint()} for-comprehensions use the Enumerable protocol to traverse \
|
||||
data structures. Either convert the data type into a list (or another Enumerable) \
|
||||
or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:into ->
|
||||
"""
|
||||
|
||||
#{hint()} the :into option in for-comprehensions use the Collectable protocol to \
|
||||
build its result. Either pass a valid data type or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:anonymous_rescue ->
|
||||
"""
|
||||
|
||||
@@ -132,6 +109,20 @@ defmodule Module.Types.Helpers do
|
||||
the union (which may be none)
|
||||
"""
|
||||
|
||||
{:impl, for} ->
|
||||
# Get the type without dynamic for better pretty printing
|
||||
type =
|
||||
for
|
||||
|> Module.Types.Of.impl()
|
||||
|> Module.Types.Descr.dynamic()
|
||||
|> Map.fetch!(:dynamic)
|
||||
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
|
||||
|
||||
"""
|
||||
|
||||
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
|
||||
"""
|
||||
|
||||
:empty_domain ->
|
||||
"""
|
||||
|
||||
@@ -141,7 +132,8 @@ defmodule Module.Types.Helpers do
|
||||
end)
|
||||
end
|
||||
|
||||
defp hint, do: :elixir_errors.prefix(:hint)
|
||||
@doc "The hint prefix"
|
||||
def hint, do: :elixir_errors.prefix(:hint)
|
||||
|
||||
@doc """
|
||||
Collect traces from variables in expression.
|
||||
@@ -151,12 +143,13 @@ defmodule Module.Types.Helpers do
|
||||
"""
|
||||
def collect_traces(expr, %{vars: vars}) do
|
||||
{_, versions} =
|
||||
Macro.prewalk(expr, %{}, fn
|
||||
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
|
||||
Macro.prewalk(expr, %{}, fn node, versions ->
|
||||
with {var_name, meta, var_context} when is_atom(var_name) and is_atom(var_context) <- node,
|
||||
false <- String.starts_with?(Atom.to_string(var_name), "_") do
|
||||
version = meta[:version]
|
||||
|
||||
case vars do
|
||||
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
|
||||
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
|
||||
{:ok,
|
||||
Map.put(versions, version, %{
|
||||
type: :variable,
|
||||
@@ -168,9 +161,9 @@ defmodule Module.Types.Helpers do
|
||||
_ ->
|
||||
{:ok, versions}
|
||||
end
|
||||
|
||||
node, versions ->
|
||||
{node, versions}
|
||||
else
|
||||
_ -> {node, versions}
|
||||
end
|
||||
end)
|
||||
|
||||
versions
|
||||
@@ -269,7 +262,12 @@ defmodule Module.Types.Helpers do
|
||||
Converts the given expression to a string,
|
||||
translating inlined Erlang calls back to Elixir.
|
||||
|
||||
We also undo some macro expressions done by the Kernel module.
|
||||
We also undo some macro expressions done by the Kernel module
|
||||
and collapse complex expressions.
|
||||
|
||||
## Options
|
||||
|
||||
* `:collapse_structs` - when false, show structs full representation
|
||||
"""
|
||||
def expr_to_string(expr, opts \\ []) do
|
||||
string = prewalk_expr_to_string(expr, opts)
|
||||
@@ -331,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]}
|
||||
|
||||
@@ -340,6 +345,13 @@ defmodule Module.Types.Helpers do
|
||||
{{:., _, [mod, fun]}, meta, args} ->
|
||||
erl_to_ex(mod, fun, args, meta)
|
||||
|
||||
{:fn, meta, [{:->, _, [_args, return]}]} = expr ->
|
||||
if meta[:capture] do
|
||||
{:&, meta, [return]}
|
||||
else
|
||||
expr
|
||||
end
|
||||
|
||||
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]} ->
|
||||
{mod, fun} =
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, arity) do
|
||||
@@ -349,40 +361,139 @@ defmodule Module.Types.Helpers do
|
||||
|
||||
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
|
||||
|
||||
{:case, meta, [expr, [do: clauses]]} = case ->
|
||||
if meta[:type_check] == :expr do
|
||||
case clauses do
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
{:case, meta, [expr, [do: clauses]]} ->
|
||||
case meta[:type_check] do
|
||||
{:case, op} ->
|
||||
case clauses do
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
{: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]}
|
||||
]}
|
||||
]}
|
||||
],
|
||||
true
|
||||
]},
|
||||
{:->, _, [[{:_, _, Kernel}], false]}
|
||||
]
|
||||
when op == :! ->
|
||||
{:!, meta, [expr]}
|
||||
|
||||
[
|
||||
{:->, _, [[false], else_block]},
|
||||
{:->, _, [[true], do_block]}
|
||||
] ->
|
||||
{:if, meta, [expr, [do: do_block, else: else_block]]}
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{: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]}
|
||||
|
||||
_ ->
|
||||
case
|
||||
end
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{: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]}
|
||||
]
|
||||
when op == :if ->
|
||||
{: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]]}
|
||||
|
||||
[
|
||||
{:->, _, [[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: _] ++ _]} ->
|
||||
{:try, meta, [[do: {:..., [], []}]]}
|
||||
|
||||
{:cond, meta, [[do: _]]} ->
|
||||
{:cond, meta, [[do: {:..., [], []}]]}
|
||||
|
||||
{:receive, meta, [[do: _] ++ _]} ->
|
||||
{:receive, meta, [[do: {:..., [], []}]]}
|
||||
|
||||
{var, meta, context} = expr when is_atom(var) and is_atom(context) ->
|
||||
if is_integer(meta[:capture]) do
|
||||
{:&, meta, [meta[:capture]]}
|
||||
else
|
||||
case
|
||||
expr
|
||||
end
|
||||
|
||||
other ->
|
||||
|
||||
+447
-204
@@ -1,5 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Module.Types.Of do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
@@ -11,10 +12,10 @@ defmodule Module.Types.Of do
|
||||
@suffix quote(do: ...)
|
||||
|
||||
@integer_or_float union(integer(), float())
|
||||
@integer_or_binary union(integer(), binary())
|
||||
@integer integer()
|
||||
@float float()
|
||||
@binary binary()
|
||||
@bitstring bitstring()
|
||||
|
||||
## Variables
|
||||
|
||||
@@ -29,19 +30,53 @@ defmodule Module.Types.Of do
|
||||
|
||||
@doc """
|
||||
Marks a variable with error.
|
||||
|
||||
This purposedly deletes all traces of the variable,
|
||||
as it is often invoked when the cause for error is elsewhere.
|
||||
"""
|
||||
def error_var(var, context) do
|
||||
def error_var({_, meta, _}, context) do
|
||||
error_var(Keyword.fetch!(meta, :version), context)
|
||||
end
|
||||
|
||||
def error_var(version, context) do
|
||||
update_in(context.vars[version], fn
|
||||
%{errored: true} = data -> data
|
||||
data -> Map.put(%{data | type: error_type(), off_traces: []}, :errored, true)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares a variable.
|
||||
"""
|
||||
def declare_var(var, type \\ term(), context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
|
||||
data = %{
|
||||
type: error_type(),
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: []
|
||||
}
|
||||
case context.vars do
|
||||
%{^version => _} ->
|
||||
context
|
||||
|
||||
put_in(context.vars[version], data)
|
||||
vars ->
|
||||
data = %{
|
||||
type: type,
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: [],
|
||||
paths: [],
|
||||
deps: %{}
|
||||
}
|
||||
|
||||
%{context | vars: Map.put(vars, version, data)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Tracks metadata about variables dependencies and paths.
|
||||
"""
|
||||
def track_var(version, new_deps, new_paths, context) do
|
||||
update_in(context.vars[version], fn %{paths: paths, deps: deps} = data ->
|
||||
%{data | paths: new_paths ++ paths, deps: Enum.reduce(new_deps, deps, &Map.put(&2, &1, []))}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -51,26 +86,70 @@ 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.
|
||||
"""
|
||||
@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) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
if gradual?(old_type) and type not in [term(), dynamic()] do
|
||||
case 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)
|
||||
}
|
||||
context =
|
||||
case context.conditional_vars do
|
||||
%{} = conditional_vars ->
|
||||
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
|
||||
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
nil ->
|
||||
context
|
||||
end
|
||||
else
|
||||
{old_type, context}
|
||||
|
||||
case context do
|
||||
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
|
||||
{old_type, context}
|
||||
|
||||
%{pattern_info: %{guard_context: guard_context}} ->
|
||||
new_type = intersection(old_type, type)
|
||||
|
||||
case empty?(new_type) do
|
||||
true when guard_context == :orelse ->
|
||||
data = %{
|
||||
data
|
||||
| type: none(),
|
||||
off_traces: new_trace(expr, none(), stack, off_traces)
|
||||
}
|
||||
|
||||
{none(), %{context | vars: %{vars | version => data}}}
|
||||
|
||||
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
|
||||
|
||||
@@ -81,11 +160,16 @@ defmodule Module.Types.Of do
|
||||
because we want to refine types. Otherwise we should
|
||||
use compatibility.
|
||||
"""
|
||||
def refine_head_var(var, type, expr, stack, context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
def refine_head_var({_, meta, _}, type, expr, stack, context) do
|
||||
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
def refine_head_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
case context.vars do
|
||||
%{^version => %{errored: true}} ->
|
||||
{:ok, error_type(), context}
|
||||
|
||||
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
|
||||
new_type = intersection(type, old_type)
|
||||
|
||||
@@ -95,26 +179,14 @@ defmodule Module.Types.Of do
|
||||
off_traces: new_trace(expr, type, stack, off_traces)
|
||||
}
|
||||
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
|
||||
# We need to return error otherwise it leads to cascading errors
|
||||
if empty?(new_type) do
|
||||
{:error, error_type(),
|
||||
error({:refine_head_var, old_type, type, var, context}, meta, stack, context)}
|
||||
data = Map.put(%{data | type: error_type()}, :errored, true)
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
{:error, old_type, context}
|
||||
else
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
{:ok, new_type, context}
|
||||
end
|
||||
|
||||
%{} = vars ->
|
||||
data = %{
|
||||
type: type,
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: new_trace(expr, type, stack, [])
|
||||
}
|
||||
|
||||
context = %{context | vars: Map.put(vars, version, data)}
|
||||
{:ok, type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -124,11 +196,71 @@ defmodule Module.Types.Of do
|
||||
defp new_trace(expr, type, stack, traces),
|
||||
do: [{expr, stack.file, type} | traces]
|
||||
|
||||
@doc """
|
||||
Preserves `context` in first argument while
|
||||
resetting it to the vars in the second argument.
|
||||
"""
|
||||
def reset_vars(context, %{
|
||||
subpatterns: subpatterns,
|
||||
vars: vars,
|
||||
conditional_vars: conditional_vars
|
||||
}),
|
||||
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.
|
||||
"""
|
||||
def with_conditional_vars(args, acc, expr, stack, context, fun) do
|
||||
%{vars: vars, conditional_vars: conditional_vars} = context
|
||||
|
||||
{vars_conds, {acc, context}} =
|
||||
Enum.map_reduce(args, {acc, context}, fn arg, {acc, context} ->
|
||||
{acc, context} = fun.(arg, acc, %{context | vars: vars, conditional_vars: %{}})
|
||||
%{vars: vars, conditional_vars: cond_vars} = context
|
||||
{{vars, cond_vars}, {acc, context}}
|
||||
end)
|
||||
|
||||
context = %{context | vars: vars, conditional_vars: conditional_vars}
|
||||
{acc, reduce_conditional_vars(vars_conds, expr, stack, context)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reduces conditional variables collected separately.
|
||||
"""
|
||||
def reduce_conditional_vars([{vars, cond} | vars_conds], expr, stack, context) do
|
||||
%{vars: pre_vars} = context
|
||||
|
||||
Enum.reduce(Map.keys(cond), context, fn version, context ->
|
||||
if is_map_key(pre_vars, version) and
|
||||
Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
|
||||
%{^version => %{type: type}} = vars
|
||||
|
||||
type =
|
||||
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
|
||||
%{^version => %{type: type}} = vars
|
||||
union(acc, type)
|
||||
end)
|
||||
|
||||
{_, context} = refine_body_var(version, type, expr, stack, context)
|
||||
context
|
||||
else
|
||||
context
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
## Implementations
|
||||
|
||||
impls = [
|
||||
{Atom, atom()},
|
||||
{BitString, binary()},
|
||||
{BitString, bitstring()},
|
||||
{Float, float()},
|
||||
{Function, fun()},
|
||||
{Integer, integer()},
|
||||
@@ -141,14 +273,25 @@ defmodule Module.Types.Of do
|
||||
{Any, term()}
|
||||
]
|
||||
|
||||
@doc """
|
||||
Currently, for protocol implementations, we only store
|
||||
the open struct definition. This is because we don't want
|
||||
to reconsolidate whenever the struct changes, but at the
|
||||
moment we can't store references either. Ideally struct
|
||||
types on protocol dispatches would be lazily resolved.
|
||||
"""
|
||||
def impl(for, mode \\ :closed)
|
||||
|
||||
for {for, type} <- impls do
|
||||
def impl(unquote(for)), do: unquote(Macro.escape(type))
|
||||
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
|
||||
end
|
||||
|
||||
def impl(struct) do
|
||||
# Elixir did not strictly require the implementation to be available, so we need a fallback.
|
||||
def impl(struct, mode) do
|
||||
# Elixir did not strictly require the implementation to be available,
|
||||
# so we need to deal with such cases accordingly.
|
||||
# TODO: Assume implementation is available on Elixir v2.0.
|
||||
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
# A warning is emitted since v1.19+.
|
||||
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
struct_type(struct, info)
|
||||
else
|
||||
open_map(__struct__: atom([struct]))
|
||||
@@ -161,7 +304,7 @@ defmodule Module.Types.Of do
|
||||
Handles fetching a map key.
|
||||
"""
|
||||
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
|
||||
case map_fetch(type, field) do
|
||||
case map_fetch_key(type, field) do
|
||||
{_optional?, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
@@ -176,107 +319,134 @@ defmodule Module.Types.Of do
|
||||
def closed_map(pairs, expected, stack, context, of_fun) do
|
||||
{pairs_types, context} = pairs(pairs, expected, stack, context, of_fun)
|
||||
|
||||
map =
|
||||
permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
|
||||
# TODO: Use the fallback type to actually indicate if open or closed.
|
||||
if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
|
||||
{dynamic?, domain, single, multiple} =
|
||||
Enum.reduce(pairs_types, {false, [], [], []}, fn
|
||||
{pos_neg_domain, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
|
||||
dynamic? = dynamic? or dynamic_pair?
|
||||
|
||||
case pos_neg_domain do
|
||||
# If atom is included in domain keys, it unions all previous
|
||||
# single and multiple, except the ones negated:
|
||||
#
|
||||
# %{foo: :bar, term() => :baz}
|
||||
# #=> %{foo: :bar or :baz, term() => :baz}
|
||||
#
|
||||
# %{foo: :bar, not :foo => :baz}
|
||||
# #=> %{foo: :bar, term() => :baz}
|
||||
#
|
||||
# In case the negated term does not appear, we set it to none():
|
||||
#
|
||||
# %{foo: :bar, term() => :baz}
|
||||
# #=> %{term() => :baz, foo: :bar or :baz}
|
||||
#
|
||||
# %{not :foo => :baz}
|
||||
# #=> %{term() => :baz, foo: none()}
|
||||
#
|
||||
# In case we are dealing with multiple keys, we always merge the
|
||||
# domain. A more precise approach would be to postpone doing so
|
||||
# until the cartesian map is distributed but those should be very
|
||||
# uncommon.
|
||||
{[], negs, domain_keys} ->
|
||||
if :atom in domain_keys do
|
||||
{single, multiple} = union_negated(negs, value_type, single, multiple)
|
||||
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
|
||||
else
|
||||
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
|
||||
end
|
||||
|
||||
{pos, [], domain_keys} ->
|
||||
domain =
|
||||
case domain_keys do
|
||||
[] -> domain
|
||||
_ -> [{domain_keys, value_type} | domain]
|
||||
end
|
||||
|
||||
case pos do
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, domain, [{key, value_type} | single], multiple}
|
||||
|
||||
_ ->
|
||||
{dynamic?, domain, single, [{pos, value_type} | multiple]}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
{map, context}
|
||||
non_multiple = Enum.reverse(single, domain)
|
||||
|
||||
map =
|
||||
case Enum.reverse(multiple) do
|
||||
[] ->
|
||||
closed_map(non_multiple)
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
closed_map(non_multiple ++ [{key, type} | t])
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
end
|
||||
|
||||
{if(dynamic?, do: dynamic(map), else: map), context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Computes the types of key-value pairs.
|
||||
"""
|
||||
def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{_key_type, context} = of_fun.(key, term(), stack, context)
|
||||
{value_type, context} = of_fun.(value, term(), stack, context)
|
||||
{{true, :none, value_type}, context}
|
||||
end)
|
||||
defp union_negated([], new_type, single, multiple) do
|
||||
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
|
||||
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
|
||||
{single, multiple}
|
||||
end
|
||||
|
||||
def pairs(pairs, expected, stack, context, of_fun) do
|
||||
defp union_negated(negated, new_type, single, multiple) do
|
||||
{single, matched} =
|
||||
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
|
||||
if key in negated do
|
||||
{{key, old_type}, [key | matched]}
|
||||
else
|
||||
{{key, union(old_type, new_type)}, matched}
|
||||
end
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} ->
|
||||
{keys, union(old_type, new_type)}
|
||||
end)
|
||||
|
||||
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
|
||||
end
|
||||
|
||||
defp pairs(pairs, expected, stack, context, of_fun) do
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
|
||||
{pos_neg_domain, dynamic_key?, context} = map_key_type(key, stack, context, of_fun)
|
||||
|
||||
expected_value_type =
|
||||
with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do
|
||||
with {[key], [], []} <- pos_neg_domain,
|
||||
{_, expected_value_type} <- map_fetch_key(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{value_type, context} = of_fun.(value, expected_value_type, stack, context)
|
||||
{{dynamic_key? or gradual?(value_type), keys, value_type}, context}
|
||||
{{pos_neg_domain, dynamic_key? or gradual?(value_type), value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{false, [key], context}
|
||||
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{{[key], [], []}, false, context}
|
||||
end
|
||||
|
||||
defp finite_key_type(key, stack, context, of_fun) do
|
||||
defp map_key_type(key, stack, context, of_fun) do
|
||||
{key_type, context} = of_fun.(key, term(), stack, context)
|
||||
domain_keys = to_domain_keys(key_type)
|
||||
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {gradual?(key_type), list, context}
|
||||
_ -> {gradual?(key_type), :none, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds permutation of maps according to the given pairs types.
|
||||
"""
|
||||
def permutate_map(_pairs_types, %{mode: :traversal}, _of_map) do
|
||||
dynamic()
|
||||
end
|
||||
|
||||
def permutate_map(pairs_types, _stack, of_map) do
|
||||
{dynamic?, fallback, single, multiple, assert} =
|
||||
Enum.reduce(pairs_types, {false, none(), [], [], []}, fn
|
||||
{dynamic_pair?, keys, value_type}, {dynamic?, fallback, single, multiple, assert} ->
|
||||
dynamic? = dynamic? or dynamic_pair?
|
||||
|
||||
case keys do
|
||||
:none ->
|
||||
fallback = union(fallback, value_type)
|
||||
|
||||
{fallback, assert} =
|
||||
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
|
||||
{union(fallback, type), [key | assert]}
|
||||
end)
|
||||
|
||||
{fallback, assert} =
|
||||
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
|
||||
{union(fallback, type), keys ++ assert}
|
||||
end)
|
||||
|
||||
{dynamic?, fallback, [], [], assert}
|
||||
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, fallback, [{key, value_type} | single], multiple, assert}
|
||||
|
||||
keys ->
|
||||
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert}
|
||||
end
|
||||
end)
|
||||
|
||||
map =
|
||||
case Enum.reverse(multiple) do
|
||||
[] ->
|
||||
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
pos_neg_domain =
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)}
|
||||
{:infinite, list} -> {[], list, domain_keys}
|
||||
:error -> {[], [], domain_keys}
|
||||
end
|
||||
|
||||
if dynamic?, do: dynamic(map), else: map
|
||||
{pos_neg_domain, gradual?(key_type), context}
|
||||
end
|
||||
|
||||
defp cartesian_map(lists) do
|
||||
@@ -291,20 +461,24 @@ 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, %{mode: mode} = stack, context, of_fun)
|
||||
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)
|
||||
|
||||
if is_nil(info) do
|
||||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
|
||||
end
|
||||
|
||||
# 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 =
|
||||
with true <- mode != :traversal,
|
||||
{_, expected_value_type} <- map_fetch(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
case map_fetch_key(expected, key) do
|
||||
{_, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
@@ -318,23 +492,28 @@ defmodule Module.Types.Of do
|
||||
@doc """
|
||||
Returns `__info__(:struct)` information about a struct.
|
||||
"""
|
||||
def struct_info(struct, meta, stack, context) do
|
||||
def struct_info(struct, kind, meta, stack, context) 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}
|
||||
{nil, error(error, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -351,45 +530,74 @@ defmodule Module.Types.Of do
|
||||
closed_map(pairs)
|
||||
end
|
||||
|
||||
## Binary
|
||||
@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 """
|
||||
Handles binaries.
|
||||
Handles bitstrings.
|
||||
|
||||
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
|
||||
based on the position of the expression within the binary.
|
||||
"""
|
||||
def binary([], _kind, _stack, context) do
|
||||
context
|
||||
def bitstring([], _kind, _stack, context) do
|
||||
{binary(), context}
|
||||
end
|
||||
|
||||
def binary([head], kind, stack, context) do
|
||||
binary_segment(head, kind, [head], stack, context)
|
||||
def bitstring([head], kind, stack, context) do
|
||||
{alignment, context} = bitstring_segment(head, kind, [head], stack, context)
|
||||
{alignment_to_type(alignment), context}
|
||||
end
|
||||
|
||||
def binary([head | tail], kind, stack, context) do
|
||||
context = binary_segment(head, kind, [head, @suffix], stack, context)
|
||||
binary_many(tail, kind, stack, context)
|
||||
def bitstring([head | tail], kind, stack, context) do
|
||||
{alignment, context} = bitstring_segment(head, kind, [head, @suffix], stack, context)
|
||||
bitstring_tail(tail, alignment, kind, stack, context)
|
||||
end
|
||||
|
||||
defp binary_many([last], kind, stack, context) do
|
||||
binary_segment(last, kind, [@prefix, last], stack, context)
|
||||
defp bitstring_tail([last], alignment, kind, stack, context) do
|
||||
{seg_alignment, context} = bitstring_segment(last, kind, [@prefix, last], stack, context)
|
||||
{alignment_to_type(alignment(seg_alignment, alignment)), context}
|
||||
end
|
||||
|
||||
defp binary_many([head | tail], kind, stack, context) do
|
||||
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
|
||||
binary_many(tail, kind, stack, context)
|
||||
defp bitstring_tail([head | tail], alignment, kind, stack, context) do
|
||||
{seg_alignment, context} =
|
||||
bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context)
|
||||
|
||||
bitstring_tail(tail, alignment(seg_alignment, alignment), kind, stack, context)
|
||||
end
|
||||
|
||||
defp alignment(left, right) when is_integer(left) and is_integer(right), do: left + right
|
||||
defp alignment(_left, _right), do: :unknown
|
||||
|
||||
defp alignment_to_type(:unknown), do: bitstring()
|
||||
defp alignment_to_type(integer) when rem(integer, 8) == 0, do: binary()
|
||||
defp alignment_to_type(_integer), do: bitstring_no_binary()
|
||||
|
||||
# If the segment is a literal, the compiler has already checked its validity,
|
||||
# so we just skip it.
|
||||
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
|
||||
# so we just check the size.
|
||||
defp bitstring_segment({:"::", _meta, [left, right]}, kind, _args, stack, context)
|
||||
when is_binary(left) or is_number(left) do
|
||||
context
|
||||
{_type, alignment_type} = specifier_type(kind, right)
|
||||
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
|
||||
|
||||
# We don't need to check for bitstrings because the left side
|
||||
# is either a binary (aligned), float (aligned), or integer
|
||||
# (which we check below).
|
||||
if alignment_type == :integer and alignment_value != :default do
|
||||
{alignment_value, context}
|
||||
else
|
||||
{0, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
|
||||
type = specifier_type(kind, right)
|
||||
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
|
||||
{type, alignment_type} = specifier_type(kind, right)
|
||||
expr = {:<<>>, meta, args}
|
||||
|
||||
{actual, context} =
|
||||
@@ -406,10 +614,26 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
|
||||
if compatible?(actual, type) do
|
||||
specifier_size(kind, right, stack, context)
|
||||
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
|
||||
|
||||
case alignment_type do
|
||||
:aligned ->
|
||||
{0, context}
|
||||
|
||||
:integer when alignment_value == :default ->
|
||||
{0, context}
|
||||
|
||||
# There is no size, so the alignment depends on the type.
|
||||
# If the type is exclusively a binary, then it is aligned.
|
||||
:bitstring when alignment_value == :default ->
|
||||
if bitstring_no_binary_type?(actual), do: {:unknown, context}, else: {0, context}
|
||||
|
||||
_ ->
|
||||
{alignment_value, context}
|
||||
end
|
||||
else
|
||||
error = {:badbinary, kind, meta, expr, type, actual, context}
|
||||
error(error, meta, stack, context)
|
||||
{:unknown, error(error, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -425,39 +649,48 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
|
||||
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
|
||||
defp specifier_type(:match, {:utf8, _, _}), do: @integer
|
||||
defp specifier_type(:match, {:utf16, _, _}), do: @integer
|
||||
defp specifier_type(:match, {:utf32, _, _}), do: @integer
|
||||
defp specifier_type(:match, {:float, _, _}), do: @float
|
||||
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
|
||||
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
|
||||
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
|
||||
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
|
||||
defp specifier_type(_kind, {:integer, _, _}), do: @integer
|
||||
defp specifier_type(_kind, {:bits, _, _}), do: @binary
|
||||
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
|
||||
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
|
||||
defp specifier_type(_kind, {:binary, _, _}), do: @binary
|
||||
defp specifier_type(_kind, _specifier), do: @integer
|
||||
defp specifier_type(:match, {:utf8, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:utf16, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:utf32, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:float, _, _}), do: {@float, :aligned}
|
||||
defp specifier_type(_kind, {:float, _, _}), do: {@integer_or_float, :aligned}
|
||||
defp specifier_type(_kind, {:utf8, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:utf16, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:utf32, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:integer, _, _}), do: {@integer, :integer}
|
||||
defp specifier_type(_kind, {:bits, _, _}), do: {@bitstring, :bitstring}
|
||||
defp specifier_type(_kind, {:bitstring, _, _}), do: {@bitstring, :bitstring}
|
||||
defp specifier_type(_kind, {:bytes, _, _}), do: {@binary, :aligned}
|
||||
defp specifier_type(_kind, {:binary, _, _}), do: {@binary, :aligned}
|
||||
defp specifier_type(_kind, _specifier), do: {@integer, :integer}
|
||||
|
||||
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
|
||||
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
|
||||
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do
|
||||
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context))
|
||||
end
|
||||
|
||||
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
defp specifier_size(_, {:size, _, [arg]}, _stack, {unit, context})
|
||||
when is_integer(arg) do
|
||||
size = if unit == :default, do: arg, else: arg * unit
|
||||
{size, context}
|
||||
end
|
||||
|
||||
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
{actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context)
|
||||
compatible_size(actual, expr, stack, context)
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
end
|
||||
|
||||
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
defp specifier_size(_match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
|
||||
compatible_size(actual, expr, stack, context)
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
end
|
||||
|
||||
defp specifier_size(_kind, _specifier, _stack, context) do
|
||||
context
|
||||
# We currently assume the unit always comes before size
|
||||
defp specifier_size(_, {:unit, _, [unit]}, _stack, {:default, context}) do
|
||||
{unit, context}
|
||||
end
|
||||
|
||||
defp specifier_size(_kind, _specifier, _stack, align_context) do
|
||||
align_context
|
||||
end
|
||||
|
||||
defp compatible_size(actual, expr, stack, context) do
|
||||
@@ -478,9 +711,12 @@ defmodule Module.Types.Of do
|
||||
"""
|
||||
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
|
||||
case atom_fetch(type) do
|
||||
{_, mods} ->
|
||||
{:finite, mods} ->
|
||||
{mods, context}
|
||||
|
||||
{:infinite, _} ->
|
||||
{[], context}
|
||||
|
||||
:error ->
|
||||
warning = {:badmodule, expr, type, fun, arity, hints, context}
|
||||
{[], error(warning, meta, stack, context)}
|
||||
@@ -493,23 +729,6 @@ defmodule Module.Types.Of do
|
||||
error(__MODULE__, warning, meta, stack, context)
|
||||
end
|
||||
|
||||
def format_diagnostic({:refine_head_var, old_type, new_type, var, context}) do
|
||||
traces = collect_traces(var, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
incompatible types assigned to #{format_var(var)}:
|
||||
|
||||
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badbinary, kind, meta, expr, expected_type, actual_type, context}) do
|
||||
type = if kind == :match, do: "matching", else: "construction"
|
||||
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
|
||||
@@ -629,6 +848,30 @@ defmodule Module.Types.Of do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct, kind, module}) do
|
||||
message =
|
||||
if Code.ensure_loaded?(module) do
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
|
||||
else
|
||||
"struct #{inspect(module)} is undefined " <>
|
||||
"(module #{inspect(module)} is not available or is yet to be defined)"
|
||||
end
|
||||
|
||||
%{
|
||||
message: message,
|
||||
group: true,
|
||||
severity: if(kind == :pattern, 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
|
||||
|
||||
+1437
-494
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,210 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
defmodule Module.Types.Traverse do
|
||||
@moduledoc false
|
||||
|
||||
# Traverses expressions to find local calls when inference is disabled.
|
||||
|
||||
# Literals
|
||||
def of_expr(literal, _stack, context)
|
||||
when is_atom(literal) or is_integer(literal) or is_float(literal) or is_binary(literal) or
|
||||
is_pid(literal) or literal == [] do
|
||||
context
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_expr(list, stack, context) when is_list(list) do
|
||||
Enum.reduce(list, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_expr({left, right}, stack, context) do
|
||||
context = of_expr(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# <<...>>
|
||||
def of_expr({:<<>>, _meta, args}, stack, context) do
|
||||
Enum.reduce(args, context, fn
|
||||
{:"::", _meta, [left, _right]}, context ->
|
||||
of_expr(left, stack, context)
|
||||
|
||||
expr, context ->
|
||||
of_expr(expr, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# Tuples, maps
|
||||
def of_expr({container, _meta, exprs}, stack, context) when container in [:{}, :%{}] 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
|
||||
context = of_pattern(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# Blocks
|
||||
def of_expr({:__block__, _, args}, stack, context) do
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# cond do ... end
|
||||
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
|
||||
Enum.reduce(clauses, context, fn {:->, _meta, [[head], body]}, context ->
|
||||
context = of_expr(head, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
# case expr do ... end
|
||||
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
|
||||
context = of_expr(case_expr, stack, context)
|
||||
of_expr(clauses, stack, context)
|
||||
end
|
||||
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, _meta, clauses}, stack, context) do
|
||||
of_expr(clauses, stack, context)
|
||||
end
|
||||
|
||||
# try do ... end
|
||||
def of_expr({:try, _meta, [blocks]}, stack, context) do
|
||||
Enum.reduce(blocks, context, fn {_, clauses_or_body}, context ->
|
||||
of_expr(clauses_or_body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# receive do ... end
|
||||
def of_expr({:receive, _meta, [blocks]}, stack, context) do
|
||||
Enum.reduce(blocks, context, fn
|
||||
{:do, clauses_or_empty_body}, context ->
|
||||
of_expr(clauses_or_empty_body, stack, context)
|
||||
|
||||
{:after, [{:->, _meta, [[timeout], body]}]}, context ->
|
||||
context = of_expr(timeout, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# for, with
|
||||
def of_expr({op, _meta, [_ | _] = args}, stack, context) when op in [:for, :with] do
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# fun.(args)
|
||||
def of_expr({{:., _meta, [fun]}, _call_meta, args}, stack, context) do
|
||||
context = of_expr(fun, stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# remote.fun(args)
|
||||
def of_expr({{:., _, [remote, name]}, _meta, args}, stack, context)
|
||||
when is_atom(name) do
|
||||
context = of_expr(remote, stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# &Mod.fun/arity
|
||||
def of_expr({:&, _, [{:/, _, [{{:., _, [_remote, name]}, _, []}, arity]}]}, _stack, context)
|
||||
when is_atom(name) and is_integer(arity) do
|
||||
context
|
||||
end
|
||||
|
||||
# &fun/arity
|
||||
def of_expr({:&, meta, [{:/, _, [{name, _, _ctx}, arity]}]}, stack, context)
|
||||
when is_atom(name) and is_integer(arity) do
|
||||
local_fun(meta, name, arity, stack, context)
|
||||
end
|
||||
|
||||
# super(args)
|
||||
def of_expr({:super, meta, args}, stack, context) when is_list(args) do
|
||||
{_kind, name} = Keyword.fetch!(meta, :super)
|
||||
context = local_fun(meta, name, length(args), stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# local_fun(args)
|
||||
def of_expr({name, meta, args}, stack, context)
|
||||
when is_atom(name) and is_list(args) do
|
||||
context = local_fun(meta, name, length(args), stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# var
|
||||
def of_expr({name, _meta, ctx}, _stack, context)
|
||||
when is_atom(name) and is_atom(ctx) 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
|
||||
{_kind, _info, context} -> context
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -129,6 +129,7 @@ defmodule OptionParser do
|
||||
* `:integer` - parses the value as an integer
|
||||
* `:float` - parses the value as a float
|
||||
* `:string` - parses the value as a string
|
||||
* `:regex` - parses the value as a regular expression with Unicode support
|
||||
|
||||
If a switch can't be parsed according to the given type, it is
|
||||
returned in the invalid options list.
|
||||
@@ -282,11 +283,11 @@ defmodule OptionParser do
|
||||
|
||||
iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
|
||||
** (OptionParser.ParseError) 1 error found!
|
||||
--limit : Expected type integer, got "xyz"
|
||||
--limit : Expected type integer, got "xyz"...
|
||||
|
||||
iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
|
||||
** (OptionParser.ParseError) 1 error found!
|
||||
--unknown : Unknown option
|
||||
--unknown : Unknown option...
|
||||
|
||||
iex> OptionParser.parse!(
|
||||
...> ["-l", "xyz", "-f", "bar"],
|
||||
@@ -295,7 +296,7 @@ defmodule OptionParser do
|
||||
...> )
|
||||
** (OptionParser.ParseError) 2 errors found!
|
||||
-l : Expected type integer, got "xyz"
|
||||
-f : Expected type integer, got "bar"
|
||||
-f : Expected type integer, got "bar"...
|
||||
|
||||
"""
|
||||
@spec parse!(argv, options) :: {parsed, argv}
|
||||
@@ -354,7 +355,7 @@ defmodule OptionParser do
|
||||
...> strict: [number: :integer]
|
||||
...> )
|
||||
** (OptionParser.ParseError) 1 error found!
|
||||
--number : Expected type integer, got "lib"
|
||||
--number : Expected type integer, got "lib"...
|
||||
|
||||
iex> OptionParser.parse_head!(
|
||||
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
|
||||
@@ -362,7 +363,7 @@ defmodule OptionParser do
|
||||
...> )
|
||||
** (OptionParser.ParseError) 2 errors found!
|
||||
--verbose : Missing argument of type integer
|
||||
--source : Expected type integer, got "lib"
|
||||
--source : Expected type integer, got "lib"...
|
||||
|
||||
"""
|
||||
@spec parse_head!(argv, options) :: {parsed, argv}
|
||||
@@ -664,7 +665,7 @@ defmodule OptionParser do
|
||||
end
|
||||
|
||||
defp validate_switch({_name, type_or_type_and_modifiers}) do
|
||||
valid = [:boolean, :count, :integer, :float, :string, :keep]
|
||||
valid = [:boolean, :count, :integer, :float, :string, :regex, :keep]
|
||||
invalid = List.wrap(type_or_type_and_modifiers) -- valid
|
||||
|
||||
if invalid != [] do
|
||||
@@ -704,6 +705,12 @@ defmodule OptionParser do
|
||||
_ -> {true, value}
|
||||
end
|
||||
|
||||
:regex in kinds ->
|
||||
case Regex.compile(value, "u") do
|
||||
{:ok, regex} -> {false, regex}
|
||||
{:error, _} -> {true, value}
|
||||
end
|
||||
|
||||
true ->
|
||||
{false, value}
|
||||
end
|
||||
@@ -863,15 +870,20 @@ defmodule OptionParser do
|
||||
error_count = length(errors)
|
||||
error = if error_count == 1, do: "error", else: "errors"
|
||||
|
||||
"#{error_count} #{error} found!\n" <>
|
||||
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
|
||||
slogan =
|
||||
"#{error_count} #{error} found!\n" <>
|
||||
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
|
||||
|
||||
case format_available_options(opts, types) do
|
||||
"" -> slogan
|
||||
available_options -> slogan <> "\n\n#{available_options}"
|
||||
end
|
||||
end
|
||||
|
||||
defp format_error({option, nil}, opts, types) do
|
||||
if type = get_type(option, opts, types) do
|
||||
if String.contains?(option, "_") do
|
||||
msg = "#{option} : Unknown option"
|
||||
|
||||
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
|
||||
else
|
||||
"#{option} : Missing argument of type #{type}"
|
||||
@@ -891,7 +903,13 @@ defmodule OptionParser do
|
||||
|
||||
defp format_error({option, value}, opts, types) do
|
||||
type = get_type(option, opts, types)
|
||||
"#{option} : Expected type #{type}, got #{inspect(value)}"
|
||||
|
||||
with :regex <- type,
|
||||
{:error, {reason, position}} <- Regex.compile(value, "u") do
|
||||
"#{option} : Invalid regular expression #{inspect(value)}: #{reason} at position #{position}"
|
||||
else
|
||||
_ -> "#{option} : Expected type #{type}, got #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp get_type(option, opts, types) do
|
||||
@@ -917,4 +935,63 @@ defmodule OptionParser do
|
||||
option = String.replace(source, "_", "-")
|
||||
if score < current, do: best, else: {option, score}
|
||||
end
|
||||
|
||||
defp format_available_options(opts, switches) do
|
||||
reverse_aliases =
|
||||
opts
|
||||
|> Keyword.get(:aliases, [])
|
||||
|> Enum.reduce(%{}, fn {alias, target}, acc ->
|
||||
Map.update(acc, target, [alias], &[alias | &1])
|
||||
end)
|
||||
|
||||
formatted_options =
|
||||
switches
|
||||
|> Enum.sort()
|
||||
|> Enum.map(fn {name, types} ->
|
||||
types = List.wrap(types)
|
||||
|
||||
case types |> List.delete(:keep) |> List.first(:string) do
|
||||
:boolean ->
|
||||
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))}"
|
||||
base = add_aliases(base, name, reverse_aliases)
|
||||
|
||||
if :keep in types do
|
||||
base <> " (may be given more than once)"
|
||||
else
|
||||
base
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if formatted_options == [] do
|
||||
""
|
||||
else
|
||||
"Supported options:\n" <> Enum.map_join(formatted_options, "\n", &(" " <> &1))
|
||||
end
|
||||
end
|
||||
|
||||
defp add_aliases(base, name, reverse_aliases) do
|
||||
case Map.get(reverse_aliases, name, []) do
|
||||
[] ->
|
||||
base
|
||||
|
||||
alias_list ->
|
||||
alias_str =
|
||||
alias_list
|
||||
|> Enum.sort()
|
||||
|> Enum.map_join(", ", &("-" <> Atom.to_string(&1)))
|
||||
|
||||
base <> " (alias: #{alias_str})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -450,7 +452,7 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with information about all children.
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
|
||||
This function returns a list of tuples containing:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -546,7 +548,7 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unregister_name(_, _) do
|
||||
def unregister_name(_) do
|
||||
raise "{:via, PartitionSupervisor, _} cannot be given on unregistration"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,8 @@ defmodule Path do
|
||||
"""
|
||||
@type t :: IO.chardata()
|
||||
|
||||
@type relative_to_opts :: [force: boolean()]
|
||||
|
||||
@doc """
|
||||
Converts the given path to an absolute one.
|
||||
|
||||
@@ -401,7 +403,7 @@ defmodule Path do
|
||||
Path.relative_to("../foo", "/usr/local") #=> "../foo"
|
||||
|
||||
"""
|
||||
@spec relative_to(t, t, keyword) :: binary
|
||||
@spec relative_to(t, t, relative_to_opts) :: binary
|
||||
def relative_to(path, cwd, opts \\ []) when is_list(opts) do
|
||||
os_type = major_os_type()
|
||||
split_path = split(path)
|
||||
@@ -479,11 +481,11 @@ defmodule Path do
|
||||
|
||||
Check `relative_to/3` for the supported options.
|
||||
"""
|
||||
@spec relative_to_cwd(t, keyword) :: binary
|
||||
@spec relative_to_cwd(t, relative_to_opts) :: binary
|
||||
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
|
||||
|
||||
@@ -801,7 +803,7 @@ defmodule Path do
|
||||
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
|
||||
|
||||
"""
|
||||
@spec wildcard(t, keyword) :: [binary]
|
||||
@spec wildcard(t, match_dot: boolean()) :: [binary]
|
||||
def wildcard(glob, opts \\ []) when is_list(opts) do
|
||||
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ defmodule Port do
|
||||
|
||||
The port can be opened through four main mechanisms.
|
||||
|
||||
As a short summary, prefer to using the `:spawn` and `:spawn_executable`
|
||||
As a short summary, prefer to use the `:spawn` and `:spawn_executable`
|
||||
options mentioned below. The other two options, `:spawn_driver` and `:fd`
|
||||
are for advanced usage within the VM. Also consider using `System.cmd/3`
|
||||
if all you want is to execute a program and retrieve its return value.
|
||||
|
||||
+43
-27
@@ -202,24 +202,23 @@ defmodule Process do
|
||||
@doc """
|
||||
Sends an exit signal with the given `reason` to `pid`.
|
||||
|
||||
The following behavior applies if `reason` is any term except `:normal`
|
||||
or `:kill`:
|
||||
Exit behavior differs based on the value of `reason`:
|
||||
|
||||
1. If `pid` is not trapping exits, `pid` will exit with the given
|
||||
`reason`.
|
||||
- If `:normal`, `pid` will not exit unless it is the calling process, in
|
||||
which case it will exit with the reason `:normal`. If it is trapping exits,
|
||||
the exit signal is transformed into a message `{:EXIT, from, :normal}` and
|
||||
delivered to its message queue.
|
||||
|
||||
2. If `pid` is trapping exits, the exit signal is transformed into a
|
||||
message `{:EXIT, from, reason}` and delivered to the message queue
|
||||
of `pid`.
|
||||
- If `:kill`, which occurs when `Process.exit(pid, :kill)` is called, an
|
||||
untrappable exit signal is sent to `pid` which will unconditionally exit
|
||||
with reason `:killed`.
|
||||
|
||||
If `reason` is the atom `:normal`, `pid` will not exit (unless `pid` is
|
||||
the calling process, in which case it will exit with the reason `:normal`).
|
||||
If it is trapping exits, the exit signal is transformed into a message
|
||||
`{:EXIT, from, :normal}` and delivered to its message queue.
|
||||
- If any other term and `pid` is not trapping exits, `pid` will exit with
|
||||
the given `reason`.
|
||||
|
||||
If `reason` is the atom `:kill`, that is if `Process.exit(pid, :kill)` is called,
|
||||
an untrappable exit signal is sent to `pid` which will unconditionally exit
|
||||
with reason `:killed`.
|
||||
- If any other term and `pid` is trapping exits, the exit signal is
|
||||
transformed into a message `{:EXIT, from, reason}` and delivered to its
|
||||
message queue.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -388,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
|
||||
@@ -535,7 +532,7 @@ defmodule Process do
|
||||
If the process is already dead when calling `Process.monitor/1`, a
|
||||
`:DOWN` message is delivered immediately.
|
||||
|
||||
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
|
||||
See ["Links and monitors"](genservers.md#links-and-monitors)
|
||||
for an example. See `:erlang.monitor/2` for more information.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -840,7 +837,7 @@ defmodule Process do
|
||||
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:priority, priority_level) :: priority_level
|
||||
@spec flag(:save_calls, 0..10000) :: 0..10000
|
||||
@spec flag(:save_calls, 0..10_000) :: 0..10_000
|
||||
@spec flag(:sensitive, boolean) :: boolean
|
||||
@spec flag(:trap_exit, boolean) :: boolean
|
||||
defdelegate flag(flag, value), to: :erlang, as: :process_flag
|
||||
@@ -859,7 +856,7 @@ defmodule Process do
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec flag(pid, :save_calls, 0..10000) :: 0..10000
|
||||
@spec flag(pid, :save_calls, 0..10_000) :: 0..10_000
|
||||
defdelegate flag(pid, flag, value), to: :erlang, as: :process_flag
|
||||
|
||||
@doc """
|
||||
@@ -980,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}`.
|
||||
|
||||
@@ -998,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
|
||||
|
||||
+199
-146
@@ -21,7 +21,7 @@ defmodule Protocol do
|
||||
the data structure.
|
||||
|
||||
Although Elixir includes specific functions such as `tuple_size`,
|
||||
`binary_size` and `map_size`, sometimes we want to be able to
|
||||
`byte_size` and `map_size`, sometimes we want to be able to
|
||||
retrieve the size of a data structure regardless of its type.
|
||||
In Elixir we can write polymorphic code, i.e. code that works
|
||||
with different shapes/types, by using protocols. A size protocol
|
||||
@@ -267,6 +267,8 @@ defmodule Protocol do
|
||||
|
||||
@optional_callbacks __deriving__: 2
|
||||
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
@doc false
|
||||
defmacro def(signature)
|
||||
|
||||
@@ -285,6 +287,11 @@ defmodule Protocol do
|
||||
call_args = :lists.map(to_var, :lists.seq(2, arity))
|
||||
call_args = [quote(do: term) | call_args]
|
||||
|
||||
# TODO: Raise in Elixir v2.0
|
||||
if :lists.any(&match?({:\\, _, [_, _]}, &1), args) do
|
||||
IO.warn("default arguments in protocol definitions is deprecated", __CALLER__)
|
||||
end
|
||||
|
||||
quote generated: true do
|
||||
name = unquote(name)
|
||||
arity = unquote(arity)
|
||||
@@ -451,7 +458,7 @@ defmodule Protocol do
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_protocols([charlist | String.t()]) :: [atom]
|
||||
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
|
||||
def extract_protocols(paths) do
|
||||
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
|
||||
case attributes[:__protocol__] do
|
||||
@@ -480,7 +487,7 @@ defmodule Protocol do
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_impls(module, [charlist | String.t()]) :: [atom]
|
||||
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
|
||||
def extract_impls(protocol, paths) when is_atom(protocol) do
|
||||
prefix = Atom.to_charlist(protocol) ++ [?.]
|
||||
|
||||
@@ -494,17 +501,25 @@ defmodule Protocol do
|
||||
|
||||
defp extract_matching_by_attribute(paths, prefix, callback) do
|
||||
for path <- paths,
|
||||
# Do not use protocols as they may be consolidating
|
||||
path = if(is_list(path), do: path, else: String.to_charlist(path)),
|
||||
file <- list_dir(path),
|
||||
{path, files} = list_dir(path),
|
||||
file <- files,
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
end
|
||||
|
||||
# Do not use protocols as they may be consolidating
|
||||
defp list_dir({path, files}) when is_list(path) and is_list(files) do
|
||||
{path, files}
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_binary(path) do
|
||||
list_dir(String.to_charlist(path))
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_list(path) do
|
||||
case :file.list_dir(path) do
|
||||
{:ok, files} -> files
|
||||
_ -> []
|
||||
{:ok, files} -> {path, files}
|
||||
_ -> {path, []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -564,31 +579,34 @@ defmodule Protocol do
|
||||
# Ensure the types are sorted so the compiled beam is deterministic
|
||||
types = Enum.sort(types)
|
||||
|
||||
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, signatures} <-
|
||||
consolidate(protocol, any, definitions, signatures, types),
|
||||
do: compile(definitions, signatures, compile_info)
|
||||
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, checker} <-
|
||||
consolidate(protocol, any, definitions, checker, types),
|
||||
do: compile(definitions, checker, compile_info)
|
||||
end
|
||||
|
||||
defp beam_protocol(protocol) do
|
||||
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
|
||||
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
|
||||
opts = [:allow_missing_chunks]
|
||||
|
||||
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
|
||||
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
|
||||
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
|
||||
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
|
||||
%{attributes: attributes, definitions: definitions} = module_map
|
||||
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
signatures = Map.get(module_map, :signatures, %{})
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
|
||||
case attributes[:__protocol__] do
|
||||
[fallback_to_any: any] ->
|
||||
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
|
||||
checker =
|
||||
with true <- is_binary(checker),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
contents
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
{:ok, any, definitions, checker, {module_map, specs, chunks}}
|
||||
|
||||
_ ->
|
||||
{:error, :not_a_protocol}
|
||||
@@ -607,7 +625,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
# Consolidate the protocol for faster implementations and fine-grained type information.
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
|
||||
case List.keytake(definitions, {:__protocol__, 1}, 0) do
|
||||
{protocol_def, definitions} ->
|
||||
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
|
||||
@@ -623,25 +641,37 @@ defmodule Protocol do
|
||||
protocol_def = change_protocol(protocol_def, types)
|
||||
impl_for = change_impl_for(impl_for, protocol, types)
|
||||
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
|
||||
new_signatures = new_signatures(definitions, protocol_funs, protocol, types)
|
||||
|
||||
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
|
||||
signatures = Enum.into(new_signatures, signatures)
|
||||
{:ok, definitions, signatures}
|
||||
|
||||
checker =
|
||||
if checker do
|
||||
update_in(checker.exports, fn exports ->
|
||||
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
|
||||
|
||||
for {fun, info} <- exports do
|
||||
if sig = Map.get(signatures, fun) do
|
||||
{fun, %{info | sig: sig}}
|
||||
else
|
||||
{fun, info}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:ok, definitions, checker}
|
||||
|
||||
nil ->
|
||||
{:error, :not_a_protocol}
|
||||
end
|
||||
end
|
||||
|
||||
defp new_signatures(definitions, protocol_funs, protocol, types) do
|
||||
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
|
||||
alias Module.Types.Descr
|
||||
types_minus_any = List.delete(types, Any)
|
||||
|
||||
clauses =
|
||||
types
|
||||
|> List.delete(Any)
|
||||
|> Enum.map(fn impl ->
|
||||
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
|
||||
Enum.map(types_minus_any, fn impl ->
|
||||
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
|
||||
end)
|
||||
|
||||
{domain, impl_for, impl_for!} =
|
||||
@@ -656,10 +686,16 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
_ ->
|
||||
structs_domain =
|
||||
case structs do
|
||||
[] -> Descr.none()
|
||||
_ -> Descr.open_map(__struct__: Descr.atom(structs))
|
||||
end
|
||||
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {[domain], _} -> domain end)
|
||||
|> Enum.reduce(&Descr.union/2)
|
||||
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
|
||||
Descr.union(Module.Types.Of.impl(impl, :open), acc)
|
||||
end)
|
||||
|
||||
not_domain = Descr.negation(domain)
|
||||
|
||||
@@ -680,10 +716,12 @@ defmodule Protocol do
|
||||
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
|
||||
end
|
||||
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
Map.new(
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
)
|
||||
end
|
||||
|
||||
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
|
||||
@@ -748,15 +786,13 @@ 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.
|
||||
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
module_map = %{module_map | definitions: definitions} |> Map.put(:signatures, signatures)
|
||||
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
|
||||
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
|
||||
module_map = %{module_map | definitions: definitions}
|
||||
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
|
||||
end
|
||||
|
||||
## Definition callbacks
|
||||
@@ -769,16 +805,7 @@ defmodule Protocol do
|
||||
@before_compile Protocol
|
||||
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel,
|
||||
except: [
|
||||
def: 1,
|
||||
def: 2,
|
||||
defdelegate: 2,
|
||||
defguard: 1,
|
||||
defguardp: 1,
|
||||
defstruct: 1,
|
||||
defexception: 1
|
||||
]
|
||||
import Kernel, except: [def: 1, def: 2]
|
||||
|
||||
# Import the new `def` that is used by protocols
|
||||
import Protocol, only: [def: 1]
|
||||
@@ -789,12 +816,11 @@ defmodule Protocol do
|
||||
# Set up a clear slate to store defined functions
|
||||
@__functions__ []
|
||||
@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
|
||||
@@ -841,6 +867,21 @@ defmodule Protocol do
|
||||
)
|
||||
end
|
||||
|
||||
extra =
|
||||
((Module.definitions_in(env.module, :def) ++ Module.definitions_in(env.module, :defmacro)) --
|
||||
functions) --
|
||||
[impl_for: 1, impl_for!: 1, __protocol__: 1, __deriving__: 2, __deriving__: 3]
|
||||
|
||||
# TODO: Make an error on Elixir v2.0
|
||||
if extra != [] do
|
||||
warn(
|
||||
"protocols can only define functions without implementation via def/1, found: " <>
|
||||
Enum.map_join(extra, ", ", fn {name, arity} -> "#{name}/#{arity}" end),
|
||||
env,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
callback_metas = callback_metas(env.module, :callback)
|
||||
callbacks = :maps.keys(callback_metas)
|
||||
|
||||
@@ -885,57 +926,105 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp after_defprotocol do
|
||||
quote bind_quoted: [built_in: built_in()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
__MODULE__.Any
|
||||
else
|
||||
nil
|
||||
prefix =
|
||||
quote bind_quoted: [built_in: built_in()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
Protocol.__concat__(__MODULE__, "Any")
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
# Disable Dialyzer checks - before and after consolidation
|
||||
# the types could be more strict
|
||||
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
|
||||
|
||||
@doc false
|
||||
@spec impl_for(term) :: atom | nil
|
||||
Kernel.def(impl_for(data))
|
||||
|
||||
# Define the implementation for structs.
|
||||
#
|
||||
# It simply delegates to struct_impl_for which is then
|
||||
# optimized during protocol consolidation.
|
||||
Kernel.def impl_for(%struct{}) do
|
||||
struct_impl_for(struct)
|
||||
end
|
||||
|
||||
# Disable Dialyzer checks - before and after consolidation
|
||||
# the types could be more strict
|
||||
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
|
||||
# Define the implementation for built-ins
|
||||
:lists.foreach(
|
||||
fn {mod, guard} ->
|
||||
target = Protocol.__concat__(__MODULE__, mod)
|
||||
|
||||
@doc false
|
||||
@spec impl_for(term) :: atom | nil
|
||||
Kernel.def(impl_for(data))
|
||||
|
||||
# Define the implementation for structs.
|
||||
#
|
||||
# It simply delegates to struct_impl_for which is then
|
||||
# optimized during protocol consolidation.
|
||||
Kernel.def impl_for(%struct{}) do
|
||||
struct_impl_for(struct)
|
||||
end
|
||||
|
||||
# Define the implementation for built-ins
|
||||
:lists.foreach(
|
||||
fn {mod, guard} ->
|
||||
target = Protocol.__concat__(__MODULE__, mod)
|
||||
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
case Code.ensure_compiled(unquote(target)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
case Code.ensure_compiled(unquote(target)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
built_in
|
||||
)
|
||||
end,
|
||||
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)
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
# Inline struct implementation for performance
|
||||
@compile {:inline, struct_impl_for: 1}
|
||||
|
||||
if not Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
@typedoc """
|
||||
All the types that implement this protocol.
|
||||
"""
|
||||
@type t :: term
|
||||
end
|
||||
|
||||
# Store information as an attribute so it
|
||||
# can be read without loading the module.
|
||||
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
|
||||
@__protocol__ [fallback_to_any: !!@fallback_to_any]
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:module) :: __MODULE__
|
||||
@spec __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@spec __protocol__(:consolidated?) :: boolean()
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
|
||||
Kernel.def(__protocol__(:module), do: __MODULE__)
|
||||
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
|
||||
Kernel.def(__protocol__(:consolidated?), do: false)
|
||||
Kernel.def(__protocol__(:impls), do: :not_consolidated)
|
||||
end
|
||||
|
||||
undefined_impl_description =
|
||||
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
|
||||
raise =
|
||||
quote do
|
||||
raise(Protocol.UndefinedError,
|
||||
protocol: __MODULE__,
|
||||
value: data,
|
||||
description: @undefined_impl_description
|
||||
)
|
||||
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
|
||||
@@ -945,47 +1034,9 @@ defmodule Protocol do
|
||||
end
|
||||
else
|
||||
Kernel.def impl_for!(data) do
|
||||
impl_for(data) ||
|
||||
raise(Protocol.UndefinedError,
|
||||
protocol: __MODULE__,
|
||||
value: data,
|
||||
description: unquote(undefined_impl_description)
|
||||
)
|
||||
impl_for(data) || unquote(raise)
|
||||
end
|
||||
end
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
# Inline struct implementation for performance
|
||||
@compile {:inline, struct_impl_for: 1}
|
||||
|
||||
if not Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
@typedoc """
|
||||
All the types that implement this protocol.
|
||||
"""
|
||||
@type t :: term
|
||||
end
|
||||
|
||||
# Store information as an attribute so it
|
||||
# can be read without loading the module.
|
||||
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
|
||||
@__protocol__ [fallback_to_any: !!@fallback_to_any]
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:module) :: __MODULE__
|
||||
@spec __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@spec __protocol__(:consolidated?) :: boolean()
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
|
||||
Kernel.def(__protocol__(:module), do: __MODULE__)
|
||||
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
|
||||
Kernel.def(__protocol__(:consolidated?), do: false)
|
||||
Kernel.def(__protocol__(:impls), do: :not_consolidated)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1172,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user