Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5d35f780 | ||
|
|
36351ae051 | ||
|
|
5034b7fa6b | ||
|
|
360ed75d17 | ||
|
|
7f3b294e19 | ||
|
|
7427c0cf5d | ||
|
|
0208ff88c3 | ||
|
|
a46b95b816 | ||
|
|
7223808989 | ||
|
|
24d5016632 | ||
|
|
1d0094e3f8 | ||
|
|
bb69e59b5c | ||
|
|
269b4a334b | ||
|
|
695401e58b | ||
|
|
2e9ce85e91 | ||
|
|
4a74923ebb | ||
|
|
48ff182db0 | ||
|
|
6d4d45a268 | ||
|
|
82b1360aa4 | ||
|
|
ccab980101 | ||
|
|
6f635d4f12 | ||
|
|
3b5bcaa6d8 | ||
|
|
b0d02d3965 | ||
|
|
96d47e12b2 | ||
|
|
394113faac | ||
|
|
bef6436cda | ||
|
|
fdebc8cba6 | ||
|
|
a5cfa254a3 | ||
|
|
aba75ca1b2 | ||
|
|
d4b2c58979 | ||
|
|
6fde3de13f | ||
|
|
936078255d | ||
|
|
4d242a8c3d | ||
|
|
a229c0e301 | ||
|
|
87af1e34ec | ||
|
|
3a21a6a50a | ||
|
|
1962a66575 | ||
|
|
6a44ab621a | ||
|
|
c6d5bbcd9d | ||
|
|
4fbdbc8acd | ||
|
|
05576dd133 | ||
|
|
9e2f0ff348 | ||
|
|
c86e8d2b54 | ||
|
|
7bd7665a9b | ||
|
|
0ae594d716 | ||
|
|
dbd2fadba1 | ||
|
|
a164205866 | ||
|
|
26f6dbdb2a | ||
|
|
0c8f1feb3a | ||
|
|
1eff1acffd | ||
|
|
dc939db4ad | ||
|
|
b945cee1a9 | ||
|
|
78f22cc8ef | ||
|
|
1d661fb91d | ||
|
|
4147e50ea0 | ||
|
|
5779f5be37 | ||
|
|
3389f84059 | ||
|
|
7d2a3d8a69 | ||
|
|
21a1e47740 | ||
|
|
4bc288d47d | ||
|
|
2b2a99d0cf | ||
|
|
e3d2cea83a | ||
|
|
093d4c8873 | ||
|
|
628afe5b74 | ||
|
|
3d8b254724 | ||
|
|
01ff4a5ec2 | ||
|
|
63a7a054ce | ||
|
|
583150d002 | ||
|
|
6e05add26b | ||
|
|
4f0c820624 | ||
|
|
11b7f08b0b | ||
|
|
30af9521cf | ||
|
|
5d605debe1 | ||
|
|
345507aca9 | ||
|
|
c4b42ddbbe | ||
|
|
15572f54c1 | ||
|
|
fee29461cb | ||
|
|
4616a06f73 | ||
|
|
6cde8680ab | ||
|
|
cb5fb8e2e0 | ||
|
|
11e920f9e4 | ||
|
|
b703302cd1 | ||
|
|
02124013de | ||
|
|
11d2adf0ce | ||
|
|
c62216d7cb | ||
|
|
9417808a56 | ||
|
|
2db772efa6 | ||
|
|
f6e177092e | ||
|
|
82196d2bd9 | ||
|
|
99b6970093 | ||
|
|
f3a86d828b | ||
|
|
5b682995bf | ||
|
|
a6e7663f8f | ||
|
|
cf7e87a637 | ||
|
|
e1738affb4 | ||
|
|
e3327051f9 | ||
|
|
f919b3e22b | ||
|
|
a704e91133 | ||
|
|
a357d19110 | ||
|
|
2096ac050d | ||
|
|
7c4552c156 | ||
|
|
bc7844c26c | ||
|
|
f67cbf14a6 | ||
|
|
a89dcdc865 | ||
|
|
c929e885bb | ||
|
|
9158e33883 | ||
|
|
7bf65189db | ||
|
|
f316902a80 | ||
|
|
b911bb0380 | ||
|
|
37c7da4fbf | ||
|
|
6ef6f78206 | ||
|
|
024beb1bfc | ||
|
|
44085d4347 | ||
|
|
0bd52d4a7e | ||
|
|
48c515f994 | ||
|
|
81fb10eec8 | ||
|
|
8b584147e8 | ||
|
|
5e7cb9a707 | ||
|
|
2971952edc | ||
|
|
309bb8a875 | ||
|
|
e697189e3b | ||
|
|
842faf0ec5 | ||
|
|
63e5a53925 | ||
|
|
79cf2ffa15 | ||
|
|
6c927126bb | ||
|
|
7df894e181 | ||
|
|
7ac3ec1a39 | ||
|
|
d5f4d491c9 | ||
|
|
44405c300b | ||
|
|
a9021b1c8d | ||
|
|
25bba6c57e | ||
|
|
8c49052a41 | ||
|
|
dc1ac5f461 | ||
|
|
03dda70bdd | ||
|
|
1a711f95d3 | ||
|
|
df537c4d44 | ||
|
|
a46b6e3ade | ||
|
|
ca29070564 | ||
|
|
a0b17a355a | ||
|
|
a5e437e2cd | ||
|
|
4434e1f5ea | ||
|
|
793583d057 | ||
|
|
2d60a959c0 | ||
|
|
2d0e5bab83 | ||
|
|
8425a039f2 | ||
|
|
138680489e | ||
|
|
550b01db0d | ||
|
|
e7a03d2feb | ||
|
|
8a1bbb59df | ||
|
|
745ee1d538 | ||
|
|
ce47442332 | ||
|
|
6c413a893b | ||
|
|
6ef392a98d | ||
|
|
2dcba2c0cb | ||
|
|
9f490a187f | ||
|
|
7c836e221a | ||
|
|
a598106b23 | ||
|
|
4b4ac32b73 | ||
|
|
9e78534880 | ||
|
|
b8c1eb7de2 | ||
|
|
4e616904c1 | ||
|
|
44ea7a621e | ||
|
|
a8d9921a89 | ||
|
|
82e17e8bc3 | ||
|
|
17f782e0eb | ||
|
|
6fda428a21 | ||
|
|
e303c78201 | ||
|
|
0f832059da | ||
|
|
3b96b74a51 | ||
|
|
70624eff47 | ||
|
|
ffcea82a1e | ||
|
|
a8f18d8105 | ||
|
|
aaaa921781 | ||
|
|
e20ca7e788 | ||
|
|
e308336c94 | ||
|
|
f08dff3536 | ||
|
|
8f63ee11b3 | ||
|
|
5d2ac0ca69 | ||
|
|
6e57f00e4b | ||
|
|
1570992d04 | ||
|
|
e713dc9c9b | ||
|
|
8eb51e6494 | ||
|
|
e641683a41 | ||
|
|
4270828c93 | ||
|
|
002e42d574 | ||
|
|
e94774926b | ||
|
|
7918013cfd | ||
|
|
fa7db38e6f | ||
|
|
1a63a83e07 | ||
|
|
cff8010e80 | ||
|
|
369f9b8844 | ||
|
|
dbf74a6098 | ||
|
|
450a24c840 | ||
|
|
ba04ada65d | ||
|
|
e25f52019a | ||
|
|
8ea2a5dc17 | ||
|
|
85b5838020 | ||
|
|
f16d8d290f | ||
|
|
6689fc1ff4 | ||
|
|
34abdd4361 | ||
|
|
9a6a7b38a8 | ||
|
|
d7c0671877 | ||
|
|
27438994fd | ||
|
|
92648c7eb2 | ||
|
|
dae2bcb4bd | ||
|
|
7bb87a7d0a | ||
|
|
62e5ebbda9 | ||
|
|
751a518d06 | ||
|
|
895b4064c9 | ||
|
|
0a21418534 | ||
|
|
ca719e08fb | ||
|
|
2e2fdf84c8 | ||
|
|
8564fd506a | ||
|
|
59e257d3ca | ||
|
|
51ac6a5c0f | ||
|
|
0821930417 | ||
|
|
f9a7552ab5 | ||
|
|
f2ad598ca3 | ||
|
|
1bfa86baf3 | ||
|
|
4720b39284 | ||
|
|
4fbe8ee9a7 | ||
|
|
d0b5519700 | ||
|
|
84f3ed66c9 | ||
|
|
7571ff2672 | ||
|
|
58b62fb594 | ||
|
|
0676818543 | ||
|
|
c423e83fb0 | ||
|
|
7e9c89780c | ||
|
|
3cf8682687 | ||
|
|
d1a33f8b63 | ||
|
|
2429df6a6a | ||
|
|
1db0a0c3fd | ||
|
|
92c3b9fddd | ||
|
|
9ed6724440 | ||
|
|
4c42f94aa2 | ||
|
|
025d8ef8ce | ||
|
|
e578a71b79 | ||
|
|
1a4a91a1f3 | ||
|
|
ac8c3dd3ab | ||
|
|
6adff3fcc2 | ||
|
|
259d52cba7 | ||
|
|
32597f08f7 | ||
|
|
a3522e4a25 | ||
|
|
8fcf677082 | ||
|
|
6b61234adb | ||
|
|
717952aacf | ||
|
|
5cad659e6d | ||
|
|
b3e4738a4c | ||
|
|
f54719040c | ||
|
|
f3aab1140b | ||
|
|
5b21c6a168 | ||
|
|
48c124abbe | ||
|
|
b7a82bfcea | ||
|
|
d0fb26a68a | ||
|
|
a324c883ce | ||
|
|
f401038c31 | ||
|
|
6d12dcecf7 | ||
|
|
e746c042bb | ||
|
|
4e840bc094 | ||
|
|
345deb35cc | ||
|
|
b7e22baa45 | ||
|
|
1d9d560fcd | ||
|
|
6957e15210 | ||
|
|
b82c44a109 | ||
|
|
1cb2089367 | ||
|
|
c7586755a6 | ||
|
|
77426cc770 | ||
|
|
ecf55172c7 | ||
|
|
251d3fe5b2 | ||
|
|
16bee46eef | ||
|
|
b639d87c9f | ||
|
|
b3797d0cf5 | ||
|
|
065b8532c0 | ||
|
|
0aa4acef63 | ||
|
|
233f824c16 | ||
|
|
a3bfe9d407 | ||
|
|
35cceec15b | ||
|
|
9c83ece467 | ||
|
|
9e687da4a9 | ||
|
|
681a9ee9cc | ||
|
|
2fbe44ef64 | ||
|
|
ad993f0d80 | ||
|
|
82fac2fd38 | ||
|
|
2bcaaf8a62 | ||
|
|
56e4e28d86 | ||
|
|
bec8d39796 | ||
|
|
6f9f2d1429 | ||
|
|
1c8b15897b | ||
|
|
e0d257251b | ||
|
|
b3c0f259ef | ||
|
|
b680bea383 | ||
|
|
9cb8bb63ce | ||
|
|
66ca149315 | ||
|
|
68b660d8e8 | ||
|
|
a897d59df7 | ||
|
|
bd36b69cec | ||
|
|
eecafef2c0 | ||
|
|
1ad6e735cb | ||
|
|
5fd13212d9 | ||
|
|
46c461e617 | ||
|
|
9a2029245d | ||
|
|
b4bd740eb7 | ||
|
|
940d01c1c4 | ||
|
|
f4bf3fd20e | ||
|
|
1d3a5b85f4 | ||
|
|
34c7f2106c | ||
|
|
42cf846de0 | ||
|
|
94dc133058 | ||
|
|
9347ccd564 | ||
|
|
4215bbe986 | ||
|
|
f70e504ebb | ||
|
|
73cf46d785 | ||
|
|
ec50a88ee4 | ||
|
|
311f6fb57d | ||
|
|
8e27ce49a3 | ||
|
|
afedada0d1 | ||
|
|
1e12c7d6be | ||
|
|
264200a4a6 | ||
|
|
39acd239af | ||
|
|
1ac9007bae | ||
|
|
72e5d84b4e | ||
|
|
0cdaea1a8c | ||
|
|
a9976cc5d9 | ||
|
|
fce9b1c0d4 | ||
|
|
83e9b66e65 | ||
|
|
8efdf4eb10 | ||
|
|
56bc730a2a | ||
|
|
fd1109e859 | ||
|
|
1d599978f7 | ||
|
|
b64b214412 | ||
|
|
c540d1fa94 | ||
|
|
75536d80f1 | ||
|
|
0395301060 | ||
|
|
59434b1590 | ||
|
|
97770a660a | ||
|
|
23d9bfd3e1 | ||
|
|
99cd654460 | ||
|
|
45ef8fd94a | ||
|
|
784db65375 | ||
|
|
2421be6732 | ||
|
|
d5e131623d | ||
|
|
9046a2fe72 | ||
|
|
1796733076 | ||
|
|
c958e540e9 | ||
|
|
36f272cdf0 | ||
|
|
efcdfb8eca | ||
|
|
df9d1d6e52 | ||
|
|
1094302c24 | ||
|
|
03c42d723c | ||
|
|
09e1448710 | ||
|
|
502421c94e | ||
|
|
47b762b4f1 | ||
|
|
b48221d221 | ||
|
|
6f18bf70c0 | ||
|
|
82fa62d0a5 | ||
|
|
83ac4fe29c | ||
|
|
6a14c55681 | ||
|
|
3ba669da3e | ||
|
|
7ad36f1bcc | ||
|
|
8032f1dd19 | ||
|
|
9f24a452f8 | ||
|
|
fcf47f34ab | ||
|
|
600b7c63d3 | ||
|
|
f8ffaf01b5 | ||
|
|
f594d98a15 | ||
|
|
bb8480fc3c | ||
|
|
c64417d72f | ||
|
|
eacf0a9baf | ||
|
|
0cdfc80737 | ||
|
|
b2aad3d9f8 | ||
|
|
eab32c2797 | ||
|
|
1cb62b731c | ||
|
|
d714fb203b | ||
|
|
65e6b08576 | ||
|
|
9ee4bcf564 | ||
|
|
8a8fbcf2b1 | ||
|
|
45fc548139 | ||
|
|
9b65902446 | ||
|
|
b897da7b4c | ||
|
|
d44ad8142a | ||
|
|
7f8a8c57a4 | ||
|
|
7ff272706a | ||
|
|
fa1ff52c39 | ||
|
|
55da97b9a3 | ||
|
|
b5f1864d4e | ||
|
|
8ac03ec3b7 | ||
|
|
8f43d2bd2b | ||
|
|
1f50ad0aa5 | ||
|
|
f781a02a24 | ||
|
|
91e622889d | ||
|
|
db6e80277d | ||
|
|
23a431223b | ||
|
|
85ff5da403 | ||
|
|
571c108503 | ||
|
|
92de556716 | ||
|
|
209b0b31ff | ||
|
|
274dbb6e10 | ||
|
|
e17cb856a0 | ||
|
|
32d5bf3f18 | ||
|
|
c494891344 | ||
|
|
5337ab8716 | ||
|
|
8336f33263 | ||
|
|
b08f6fa3e2 | ||
|
|
97abb54442 | ||
|
|
c4a63d3c11 | ||
|
|
07e34342da | ||
|
|
bb0362f3f6 | ||
|
|
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 |
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
* Describe here the reasons behind the pull request.
|
||||
* Make sure you have read the CONTRIBUTING.md file.
|
||||
* Make sure any relevant documentation and tests have been added/updated.
|
||||
* Do not submit Draft pull requests unless previously asked/agreed.
|
||||
+14
-16
@@ -26,31 +26,27 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: "28.1"
|
||||
- otp_version: "29.0"
|
||||
deterministic: true
|
||||
- otp_version: "28.1"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "28.4"
|
||||
docs: true
|
||||
coverage: true
|
||||
- otp_version: "28.1"
|
||||
- otp_version: "27.3"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "27.0"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "26.0"
|
||||
- otp_version: master
|
||||
development: true
|
||||
- otp_version: maint
|
||||
development: true
|
||||
|
||||
# Earlier Erlang/OTP versions ignored compiler directives
|
||||
# when using warnings_as_errors. So we only set ERLC_OPTS
|
||||
# from Erlang/OTP 27+.
|
||||
env:
|
||||
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
@@ -96,7 +92,7 @@ jobs:
|
||||
|
||||
- name: "Upload Coverage Artifact"
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: TestCoverage
|
||||
path: cover/*
|
||||
@@ -119,17 +115,19 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version:
|
||||
- "29.0"
|
||||
- "28.1"
|
||||
- "27.3"
|
||||
- "26.2"
|
||||
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2026 The Elixir Team
|
||||
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "29 8 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
zizmor:
|
||||
name: Zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
@@ -27,7 +27,9 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
|
||||
@@ -33,7 +33,9 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
|
||||
|
||||
@@ -74,6 +74,6 @@ unless System.get_env("DRYRUN") do
|
||||
"api-username" => "Elixir"
|
||||
}
|
||||
|
||||
resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers)
|
||||
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -88,9 +89,9 @@ runs:
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
|
||||
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0
|
||||
with:
|
||||
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
|
||||
image: ghcr.io/oss-review-toolkit/ort:92.2.0
|
||||
run: >-
|
||||
labels,
|
||||
cache-dependencies,
|
||||
@@ -106,4 +107,6 @@ runs:
|
||||
ort-cli-report-args: >-
|
||||
-O CycloneDX=output.file.formats=json,xml
|
||||
-O SpdxDocument=outputFileFormats=JSON,YAML
|
||||
ort-cli-scan-args: >-
|
||||
--scanners Provenant
|
||||
sw-version: "${{ inputs.version }}"
|
||||
|
||||
@@ -32,7 +32,9 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: |
|
||||
|
||||
@@ -37,30 +37,31 @@ jobs:
|
||||
if: github.ref_type != 'branch'
|
||||
run: |
|
||||
gh release create \
|
||||
--repo ${{ github.repository }} \
|
||||
--title ${{ github.ref_name }} \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$GITHUB_REF_NAME" \
|
||||
--notes '' \
|
||||
--draft \
|
||||
${{ github.ref_name }}
|
||||
"$GITHUB_REF_NAME"
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# zizmor: ignore[artipacked]
|
||||
if: github.ref_type == 'branch'
|
||||
|
||||
- name: Update ${{ github.ref_name }}-latest
|
||||
if: github.ref_type == 'branch'
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}-latest
|
||||
ref_name="${GITHUB_REF_NAME}-latest"
|
||||
|
||||
if ! gh release view $ref_name; then
|
||||
if ! gh release view "$ref_name"; then
|
||||
gh release create \
|
||||
--latest=false \
|
||||
--title $ref_name \
|
||||
--notes "Automated release for latest ${{ github.ref_name }}." \
|
||||
$ref_name
|
||||
--title "$ref_name" \
|
||||
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
|
||||
"$ref_name"
|
||||
fi
|
||||
|
||||
git tag $ref_name --force
|
||||
git push origin $ref_name --force
|
||||
git tag "$ref_name" --force
|
||||
git push origin "$ref_name" --force
|
||||
|
||||
build:
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
|
||||
@@ -70,9 +71,6 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- otp: 26
|
||||
otp_version: "26.0"
|
||||
|
||||
- otp: 27
|
||||
otp_version: "27.0"
|
||||
|
||||
@@ -80,8 +78,13 @@ jobs:
|
||||
otp_version: "28.0"
|
||||
build_docs: build_docs
|
||||
|
||||
- otp: 29
|
||||
otp_version: "29.0"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Build Release"
|
||||
uses: ./.github/workflows/release_pre_built
|
||||
@@ -97,19 +100,19 @@ jobs:
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: build-linux-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.zip
|
||||
|
||||
- name: "Upload Windows release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: build-windows-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.exe
|
||||
|
||||
- name: "Upload doc artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: matrix.build_docs
|
||||
with:
|
||||
name: Docs
|
||||
@@ -122,7 +125,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
otp: [26, 27, 28]
|
||||
otp: [27, 28, 29]
|
||||
flavor: [windows, linux]
|
||||
|
||||
env:
|
||||
@@ -136,20 +139,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Download build"
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
|
||||
- name: Log in to Azure
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: "Sign files with Trusted Signing"
|
||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
|
||||
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
@@ -179,7 +182,7 @@ jobs:
|
||||
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
|
||||
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
path: ${{ env.RELEASE_FILE }}*
|
||||
@@ -202,11 +205,13 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Download Build Artifacts"
|
||||
id: download-build-artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -221,7 +226,7 @@ jobs:
|
||||
|
||||
- name: Attest Distribution Assets with SBoM
|
||||
id: attest-sbom
|
||||
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
|
||||
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
|
||||
@@ -241,15 +246,19 @@ jobs:
|
||||
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
|
||||
done
|
||||
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
|
||||
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
|
||||
env:
|
||||
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
|
||||
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
|
||||
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
|
||||
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
|
||||
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
|
||||
|
||||
- name: "Assemble Release SBoM Artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "SBoM"
|
||||
path: |
|
||||
@@ -259,7 +268,7 @@ jobs:
|
||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
||||
|
||||
- name: "Assemble Distribution Attestations"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: "Attestations"
|
||||
path: "attestations/*.sigstore"
|
||||
@@ -273,7 +282,7 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
|
||||
merge-multiple: true
|
||||
@@ -283,14 +292,14 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" == "branch" ]; then
|
||||
tag=${{ github.ref_name }}-latest
|
||||
if [ "$GITHUB_REF_TYPE" == "branch" ]; then
|
||||
tag="${GITHUB_REF_NAME}-latest"
|
||||
else
|
||||
tag="${{ github.ref_name }}"
|
||||
tag="$GITHUB_REF_NAME"
|
||||
fi
|
||||
|
||||
gh release upload \
|
||||
--repo ${{ github.repository }} \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--clobber \
|
||||
"$tag" \
|
||||
elixir-otp-*.zip \
|
||||
@@ -324,7 +333,7 @@ jobs:
|
||||
echo "Required variables for uploading to hex.pm are not set up, skipping..."
|
||||
exit 1
|
||||
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -335,12 +344,10 @@ jobs:
|
||||
|
||||
- name: Upload Precompiled to S3
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
|
||||
|
||||
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
|
||||
dest=${zip/elixir/${ref_name}}
|
||||
dest=${zip/elixir/${GITHUB_REF_NAME}}
|
||||
surrogate_key=${dest/.zip$/}
|
||||
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
|
||||
@@ -349,16 +356,16 @@ jobs:
|
||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
||||
|
||||
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo builds/elixir/${ref_name} >> purge_keys.txt
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Docs to S3
|
||||
run: |
|
||||
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
|
||||
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
|
||||
|
||||
unzip Docs.zip
|
||||
|
||||
@@ -379,7 +386,7 @@ jobs:
|
||||
- name: Update builds txt
|
||||
run: |
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ref_name=${{ github.ref_name }}
|
||||
ref_name="$GITHUB_REF_NAME"
|
||||
|
||||
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@ jobs:
|
||||
name: Notify
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
|
||||
with:
|
||||
otp-version: "27.3"
|
||||
elixir-version: "1.18.3"
|
||||
@@ -29,4 +31,4 @@ jobs:
|
||||
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
||||
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
|
||||
run: |
|
||||
elixir .github/workflows/notify.exs ${{ github.ref_name }}
|
||||
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
|
||||
|
||||
@@ -25,10 +25,12 @@ runs:
|
||||
|
||||
- name: Build Elixir Release
|
||||
shell: bash
|
||||
run: |
|
||||
run: | # zizmor: ignore[github-env]
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
env:
|
||||
INPUT_OTP: ${{ inputs.otp }}
|
||||
|
||||
- name: Install NSIS
|
||||
shell: bash
|
||||
@@ -39,15 +41,18 @@ runs:
|
||||
- name: Build Elixir Windows Installer
|
||||
shell: bash
|
||||
run: |
|
||||
export OTP_VERSION=${{ inputs.otp_version }}
|
||||
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
|
||||
export OTP_VERSION="$INPUT_OTP_VERSION"
|
||||
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
|
||||
(cd lib/elixir/scripts/windows_installer && ./build.sh)
|
||||
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
|
||||
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
|
||||
env:
|
||||
INPUT_OTP: ${{ inputs.otp }}
|
||||
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
|
||||
- name: Get ExDoc ref
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||
run: | # zizmor: ignore[github-env]
|
||||
if [ "$GITHUB_REF_NAME" = "main" ]; then
|
||||
ref=main
|
||||
else
|
||||
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
@@ -59,6 +64,7 @@ runs:
|
||||
repository: elixir-lang/ex_doc
|
||||
ref: ${{ env.EX_DOC_REF }}
|
||||
path: ex_doc
|
||||
persist-credentials: false
|
||||
- name: Build ex_doc
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"**/*.md"
|
||||
],
|
||||
"ignores": [
|
||||
".git/**"
|
||||
".git/**",
|
||||
".github/**"
|
||||
],
|
||||
"gitignore": true,
|
||||
"config": {
|
||||
|
||||
@@ -37,6 +37,11 @@ curations:
|
||||
comment: "Apply Trademark Policy to VERSION file"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".github/pull_request_template.md"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to GitHub pull request template"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Wrongly Identified
|
||||
- path: ".gitignore"
|
||||
|
||||
+88
-190
@@ -4,210 +4,108 @@
|
||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
-->
|
||||
|
||||
# Changelog for Elixir v1.20
|
||||
# Changelog for Elixir v1.21
|
||||
|
||||
## Type system improvements
|
||||
|
||||
This release includes type inference of all constructs.
|
||||
|
||||
### Type inference of function calls
|
||||
|
||||
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
|
||||
|
||||
```elixir
|
||||
def add_foo_and_bar(data) do
|
||||
data.foo + data.bar
|
||||
end
|
||||
```
|
||||
|
||||
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
|
||||
|
||||
Here is another example:
|
||||
|
||||
```elixir
|
||||
def sum_to_string(a, b) do
|
||||
Integer.to_string(a + b)
|
||||
end
|
||||
```
|
||||
|
||||
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
|
||||
|
||||
### 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).
|
||||
|
||||
### Complete typing of maps keys
|
||||
|
||||
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
|
||||
|
||||
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
|
||||
|
||||
```elixir
|
||||
%{123 => "hello", 456.0 => :ok}
|
||||
```
|
||||
|
||||
will have the type:
|
||||
|
||||
```elixir
|
||||
%{integer() => binary(), float() => :ok}
|
||||
```
|
||||
|
||||
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
|
||||
|
||||
```elixir
|
||||
%{integer() => integer(), root: integer()}
|
||||
```
|
||||
|
||||
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
|
||||
|
||||
### Typing of map operations
|
||||
|
||||
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
|
||||
|
||||
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
|
||||
|
||||
```elixir
|
||||
Map.put(map, :key, 123)
|
||||
#=> returns type %{..., key: integer()}
|
||||
|
||||
Map.delete(map, :key)
|
||||
#=> returns type %{..., key: not_set()}
|
||||
```
|
||||
|
||||
As you can see, we track when keys are set and also when they are removed.
|
||||
|
||||
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
|
||||
|
||||
```elixir
|
||||
Map.replace(map, :key, 123)
|
||||
#=> returns type %{..., key: if_set(integer())}
|
||||
```
|
||||
|
||||
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
|
||||
|
||||
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
def name(map), do: Map.fetch!(map, :name)
|
||||
end
|
||||
|
||||
defmodule CallsUser do
|
||||
def calls_name do
|
||||
User.name(%{})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The code above has a type violation, which is now caught by the type system:
|
||||
|
||||
```text
|
||||
warning: incompatible types given to User.name/1:
|
||||
|
||||
User.name(%{})
|
||||
|
||||
given types:
|
||||
|
||||
%{name: not_set()}
|
||||
|
||||
but expected one of:
|
||||
|
||||
dynamic(%{..., name: term()})
|
||||
|
||||
typing violation found at:
|
||||
│
|
||||
16 │ User.name(%{})
|
||||
│ ~
|
||||
│
|
||||
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
|
||||
```
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
|
||||
|
||||
## v1.20.0-rc.1 (2026-01-13)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Improve the performance of the type system when working with large unions of open maps
|
||||
* [Kernel] Do not crash on map types with struct keys when performing type operations
|
||||
* [Kernel] Mark the outcome of bitstring types as dynamic
|
||||
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary
|
||||
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable
|
||||
|
||||
## v1.20.0-rc.0 (2026-01-09)
|
||||
## v1.21.0-dev
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Support splitting middle expressions across EEx clauses
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
|
||||
* [Enum] Add `Enum.min_max` sorter
|
||||
* [Integer] Add `Integer.ceil_div/2`
|
||||
* [IO] Add `IO.iodata_empty?/1`
|
||||
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
|
||||
* [Kernel] Print intermediate results of `dbg` for pipes
|
||||
* [Kernel] Warn on unused requires
|
||||
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
|
||||
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1`
|
||||
* [Code] Add support for the `:erlc_options` compiler option
|
||||
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
|
||||
* [Kernel] Improve performance of type constructors and complex intersections
|
||||
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
|
||||
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation
|
||||
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
|
||||
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
|
||||
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
|
||||
* [Path] Add `Path.safe_join/2`
|
||||
* [Registry] Optimize exact key matching in lookups
|
||||
* [String] Optimize `String.bag_distance/2`
|
||||
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
|
||||
* [URI] Optimize percent-decoding and `URI.to_string/1`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
* [ExUnit.Assertions] Add `trace/3` helper
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps] Support filtering `mix deps` output
|
||||
* [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 test] Add `mix test --dry-run`
|
||||
|
||||
### 2. Hard deprecations
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
|
||||
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
|
||||
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
|
||||
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y`
|
||||
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3`
|
||||
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision
|
||||
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats
|
||||
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments
|
||||
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator
|
||||
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars
|
||||
* [Date.Range] Fix slicing date ranges with stepped ranges
|
||||
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1`
|
||||
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges
|
||||
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown
|
||||
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
|
||||
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
|
||||
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
|
||||
* [Kernel] Fix inferred types for functions with non-returning clauses
|
||||
* [Kernel] Fix map field type inference in the presence of empty map types
|
||||
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
|
||||
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
|
||||
* [Kernel] Improve the error message for non-atom struct keys
|
||||
* [Kernel] Raise when `|` is used in guards
|
||||
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
|
||||
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
|
||||
* [Macro] Properly escape C1 control characters and Unicode noncharacters
|
||||
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
|
||||
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
|
||||
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
|
||||
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
|
||||
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
|
||||
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
|
||||
|
||||
#### Logger
|
||||
#### ExUnit
|
||||
|
||||
* [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`
|
||||
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas
|
||||
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
|
||||
|
||||
## v1.19
|
||||
#### IEx
|
||||
|
||||
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).
|
||||
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys
|
||||
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
|
||||
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file
|
||||
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Prevent synchronization lock files from being overwritten with empty contents
|
||||
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4`
|
||||
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
|
||||
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
|
||||
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
|
||||
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
|
||||
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
|
||||
* [mix new] Avoid trailing whitespace in generated files
|
||||
|
||||
### 3. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead
|
||||
|
||||
### 4. Soft deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1`
|
||||
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1`
|
||||
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1`
|
||||
|
||||
## v1.20
|
||||
|
||||
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md).
|
||||
|
||||
+86
-47
@@ -55,7 +55,7 @@ To recompile all (including Erlang modules):
|
||||
make compile
|
||||
```
|
||||
|
||||
After your changes are done, please remember to run `make format` to guarantee
|
||||
After your changes are done, run `make format` to guarantee
|
||||
all files are properly formatted, then run the full suite with
|
||||
`make test`.
|
||||
|
||||
@@ -71,53 +71,67 @@ updating an existing checkout, run `make clean compile`. You can check
|
||||
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
|
||||
More tasks can be found by reading the [Makefile](Makefile).
|
||||
|
||||
We encourage contributors to write tests that capture both existing and newly
|
||||
introduced behavior, especially for bug fixes and major changes:
|
||||
## Sending a pull request
|
||||
|
||||
* **Bug Fixes:** If you are fixing a bug, please try to include a test that
|
||||
*fails* before your change and *passes* afterward. This makes it easier to
|
||||
confirm that the fix addresses the underlying issue and helps prevent
|
||||
regressions in the future.
|
||||
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/)
|
||||
and must include tests and other relevant proof of work:
|
||||
|
||||
* **Bug Fixes:** If you are fixing a bug, include a test that *fails* before
|
||||
your change and *passes* afterward. This makes it easier to confirm that the
|
||||
fix addresses the underlying issue and helps prevent regressions in the future.
|
||||
|
||||
* **New Features or Major Changes:** If you are adding a new feature or making
|
||||
major changes to existing functionality, please add tests that cover the
|
||||
major parts of that functionality. Aim to have the best code coverage possible.
|
||||
major changes to existing functionality, please add assocaited tests. Aim to
|
||||
have the best code coverage possible.
|
||||
|
||||
With tests running and passing, you are ready to contribute to Elixir and
|
||||
[send a pull request](https://help.github.com/articles/using-pull-requests/).
|
||||
We have saved some excellent pull requests we have received in the past in
|
||||
case you are looking for some examples:
|
||||
* **Performance improvements:** For performance improvements, please include the
|
||||
benchmark script, with inputs and results, in the pull request description.
|
||||
We recommend using [benchee](https://github.com/bencheeorg/benchee).
|
||||
|
||||
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
|
||||
Here are some pull requests we have received in the past you can use as reference:
|
||||
|
||||
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
* [Implement Enum.member?](https://github.com/elixir-lang/elixir/pull/992)
|
||||
|
||||
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
|
||||
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
|
||||
## Reviewing changes
|
||||
|
||||
Once a pull request is sent, the Elixir team will review your changes.
|
||||
We outline our process below to clarify the roles of everyone involved.
|
||||
|
||||
All pull requests must be 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.
|
||||
|
||||
The Elixir team may optionally assign someone to review a pull request.
|
||||
If someone is assigned, they must explicitly approve the code before
|
||||
another team member can merge it.
|
||||
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.
|
||||
|
||||
When the review finishes, your pull request will be squashed and merged
|
||||
into the repository. If you have carefully organized your commits and
|
||||
believe they should be merged without squashing, please mention it in
|
||||
a comment.
|
||||
|
||||
## Building documentation
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is cloned and compiled 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://elixir.hexdocs.pm/writing-documentation.html).
|
||||
|
||||
## Licensing and Compliance Requirements
|
||||
|
||||
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
|
||||
@@ -181,25 +195,50 @@ affecting **all external contributors**:
|
||||
See <https://developercertificate.org/> for a copy of the Developer Certificate
|
||||
of Origin license.
|
||||
|
||||
## Building documentation
|
||||
## Using AI and coding agents
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir.
|
||||
While we allow the use of AI on contributions and discussions, please be mindful
|
||||
when doing so. Generally speaking, Elixir maintainers already have access to AI
|
||||
(like many other developers). Therefore, if we need the feedback or help of a
|
||||
coding agent, we can request so ourselves. For this reason, we often find
|
||||
the point of view of the human behind the agent more valuable.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
That said, here are examples of how one might (or might not) use AI and coding
|
||||
agents in Elixir spaces:
|
||||
|
||||
```sh
|
||||
elixir_dir=$(pwd)
|
||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
||||
* When it comes to discussions, using AI to help express yourself is welcome,
|
||||
but avoid directly copy and pasting AI generated content. If there is a language
|
||||
barrier, use AI to translate, review, and improve your text, but do not use AI
|
||||
to respond on your behalf.
|
||||
|
||||
# Now we will go back to Elixir's root directory,
|
||||
cd "${elixir_dir}"
|
||||
* Do not use coding agents to tackle existing issues unless they have the
|
||||
"Contributions Welcome" label.
|
||||
|
||||
# and generate HTML and EPUB documents:
|
||||
make docs
|
||||
```
|
||||
* If you request a feature on the mailing list and it is accepted, you may
|
||||
use coding agents to implement it, as long as it follows the AI Contributions
|
||||
guidelines below.
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||
* When automating AI usage on the Elixir codebase for performance improvements,
|
||||
security fixes, or correctness changes to the compiler or type system, pair it
|
||||
with a separate set of agents whose job is to argue against and try to invalidate
|
||||
any proposed change. And treat their approval as advisory: a human must still
|
||||
validate it before opening issues or pull requests.
|
||||
|
||||
If any code is written by AI, then you must follow the guidelines below.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -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
|
||||
@@ -189,7 +189,7 @@ clean_elixir:
|
||||
#==> Documentation tasks
|
||||
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<h1>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
|
||||
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
|
||||
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/public/images/logo/logo.png" width="200">
|
||||
</picture>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
|
||||
[](https://www.bestpractices.dev/projects/10187)
|
||||
|
||||
Elixir is a dynamic, functional language designed for building scalable
|
||||
Elixir is a programming language designed for building scalable
|
||||
and maintainable applications.
|
||||
|
||||
For more about Elixir, installation and documentation,
|
||||
@@ -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
|
||||
|
||||
|
||||
+3
-3
@@ -12,12 +12,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.20 | Development
|
||||
1.19 | Bug fixes and security patches
|
||||
1.21 | Development
|
||||
1.20 | Bug fixes and security patches
|
||||
1.19 | Security patches only
|
||||
1.18 | Security patches only
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.20.0-rc.1
|
||||
ELIXIR_VERSION=1.21.0-dev
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
set ELIXIR_VERSION=1.20.0-rc.1
|
||||
set ELIXIR_VERSION=1.21.0-dev
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+3
-2
@@ -293,7 +293,7 @@ defmodule EEx do
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
|
||||
@spec eval_string(String.t(), Code.binding(), [compile_opt]) :: term()
|
||||
def eval_string(source, bindings \\ [], options \\ [])
|
||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||
compiled = compile_string(source, options)
|
||||
@@ -315,7 +315,7 @@ defmodule EEx do
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
|
||||
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t()
|
||||
def eval_file(filename, bindings \\ [], options \\ [])
|
||||
when is_list(bindings) and is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
@@ -344,6 +344,7 @@ defmodule EEx do
|
||||
|
||||
It returns `{:ok, [token]}` where a token is one of:
|
||||
|
||||
* `{:comment, content, %{column: column, line: line}}`
|
||||
* `{:text, content, %{column: column, line: line}}`
|
||||
* `{:expr, marker, content, %{column: column, line: line}}`
|
||||
* `{:start_expr, marker, content, %{column: column, line: line}}`
|
||||
|
||||
+72
-30
@@ -79,7 +79,7 @@ defmodule EEx.Compiler do
|
||||
{:error, message <> code_snippet(state.source, state.indentation, meta), meta}
|
||||
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
{key, expr} =
|
||||
{key, expr, extra_meta} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, _line, _column, _warnings, rev_tokens, []} ->
|
||||
# We ignore warnings because the code will be tokenized
|
||||
@@ -87,7 +87,7 @@ defmodule EEx.Compiler do
|
||||
token_key(rev_tokens, expr)
|
||||
|
||||
{:error, _, _, _, _} ->
|
||||
{:expr, expr}
|
||||
{:expr, expr, %{}}
|
||||
end
|
||||
|
||||
marker =
|
||||
@@ -102,8 +102,8 @@ defmodule EEx.Compiler do
|
||||
marker
|
||||
end
|
||||
|
||||
token = {key, marker, expr, %{line: line, column: column}}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
|
||||
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -127,6 +127,27 @@ defmodule EEx.Compiler do
|
||||
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
|
||||
end
|
||||
|
||||
# Merge middle expressions separated only by whitespace so the whitespace is
|
||||
# part of the Elixir expression, not a separate EEx body.
|
||||
defp merge_token(
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
|
||||
) do
|
||||
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
|
||||
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
|
||||
else
|
||||
[
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
{:text, text, text_meta},
|
||||
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_token(token, acc) do
|
||||
[token | acc]
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
|
||||
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
|
||||
@@ -177,35 +198,37 @@ defmodule EEx.Compiler do
|
||||
defp token_key(rev_tokens, expr) do
|
||||
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
|
||||
{_, [{:do, _} | _]} ->
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
{:start_expr, maybe_append_space(expr), %{}}
|
||||
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
{_, [{:block_identifier, _, identifier} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}}
|
||||
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
|
||||
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||
|
||||
if end_index > fn_index do
|
||||
{:start_expr, expr}
|
||||
{_, [{:stab_op, _, _} | rev_tokens]} ->
|
||||
if fn_before_end?(rev_tokens) do
|
||||
{:start_expr, expr, %{}}
|
||||
else
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
end
|
||||
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
[{:end, _} | _] -> {:end_expr, expr, %{}}
|
||||
_ -> {:expr, expr, %{}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp fn_before_end?([{:fn, _} | _]), do: true
|
||||
defp fn_before_end?([{:end, _} | _]), do: false
|
||||
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
|
||||
defp fn_before_end?([]), do: false
|
||||
|
||||
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
|
||||
defp drop_eol(rest), do: rest
|
||||
|
||||
@@ -303,7 +326,7 @@ defmodule EEx.Compiler do
|
||||
file: file,
|
||||
source: source,
|
||||
line: line,
|
||||
quoted: [],
|
||||
quoted: %{},
|
||||
parser_options: [indentation: indentation] ++ parser_options,
|
||||
indentation: indentation
|
||||
}
|
||||
@@ -347,7 +370,7 @@ defmodule EEx.Compiler do
|
||||
state.parser_options
|
||||
|
||||
expr = Code.string_to_quoted!(chars, options)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
buffer = handle_expr(buffer, mark, expr, meta, state)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -366,7 +389,7 @@ defmodule EEx.Compiler do
|
||||
rest,
|
||||
state.engine.handle_begin(buffer),
|
||||
[{contents, start_line, start_column} | scope],
|
||||
%{state | quoted: [], line: line}
|
||||
%{state | quoted: %{}, line: line}
|
||||
)
|
||||
|
||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
||||
@@ -376,7 +399,7 @@ defmodule EEx.Compiler do
|
||||
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
|
||||
end
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
buffer = handle_expr(buffer, mark, contents, meta, state)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -410,7 +433,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 +449,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 +466,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
|
||||
@@ -476,11 +499,16 @@ defmodule EEx.Compiler do
|
||||
Enum.all?(chars, &(&1 in @all_spaces))
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(%{block_identifier: identifier}) do
|
||||
identifier in [:else, :rescue, :catch]
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(_meta), do: false
|
||||
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
{^key, value} = List.keyfind(quoted, key, 0)
|
||||
value
|
||||
Map.fetch!(quoted, key)
|
||||
end
|
||||
|
||||
defp insert_quoted({left, line, right}, quoted) do
|
||||
@@ -513,6 +541,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
|
||||
|
||||
@@ -195,7 +195,7 @@ defmodule EEx.Engine do
|
||||
def handle_expr(state, "=", ast) do
|
||||
check_state!(state)
|
||||
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
|
||||
var = Macro.var(:"arg#{vars_count}", __MODULE__)
|
||||
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__)
|
||||
|
||||
ast =
|
||||
quote do
|
||||
|
||||
@@ -270,7 +270,7 @@ defmodule EEx.TokenizerTest do
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
|
||||
{:text, ~c"bar", %{column: 21, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}},
|
||||
{:text, ~c"baz", %{column: 34, line: 1}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
|
||||
{:eof, %{column: 46, line: 1}}
|
||||
@@ -286,7 +286,7 @@ defmodule EEx.TokenizerTest do
|
||||
exprs = [
|
||||
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
|
||||
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}},
|
||||
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
|
||||
{:eof, %{column: 3, line: 7}}
|
||||
|
||||
@@ -262,6 +262,37 @@ defmodule EExTest do
|
||||
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
||||
end
|
||||
|
||||
test "with expression with else clause split across tags" do
|
||||
template = """
|
||||
<%= with {:ok, x} <- @res do %>
|
||||
<p><%= x %></p>
|
||||
<% else %>
|
||||
<% _ -> %>
|
||||
<p>bad</p>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
|
||||
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
end
|
||||
|
||||
test "empty clauses separated by whitespace" do
|
||||
template = """
|
||||
<%= case x do %>
|
||||
<% :foo -> %>
|
||||
<% :bar -> %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n \n", template, x: :foo)
|
||||
assert_eval("\n\n", template, x: :bar)
|
||||
end
|
||||
|
||||
test "with end of token" do
|
||||
assert_eval("foo bar %>", "foo bar %>")
|
||||
end
|
||||
@@ -522,7 +553,7 @@ defmodule EExTest do
|
||||
|
||||
assert message |> Exception.message() |> strip_ansi() =~ """
|
||||
│
|
||||
514 │ true && @some[\s
|
||||
#{line + 2} │ true && @some[\s
|
||||
│ │ └ missing closing delimiter (expected "]")
|
||||
│ └ unclosed delimiter
|
||||
"""
|
||||
@@ -575,6 +606,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)
|
||||
|
||||
+90
-29
@@ -10,8 +10,8 @@ defmodule Access do
|
||||
keys of any type in a data structure via the `data[key]` syntax.
|
||||
|
||||
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
||||
of the box. Keywords supports only atoms keys, keys for maps can
|
||||
be of any type. Both return `nil` if the key does not exist:
|
||||
of the box. Keyword lists support only atom keys, while keys for maps
|
||||
can be of any type. Both return `nil` if the key does not exist:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
@@ -33,7 +33,7 @@ defmodule Access do
|
||||
iex> keywords[:c][:unknown]
|
||||
nil
|
||||
|
||||
This works because accessing anything on a `nil` value, returns
|
||||
This works because accessing anything on a `nil` value returns
|
||||
`nil` itself:
|
||||
|
||||
iex> nil[:a]
|
||||
@@ -226,6 +226,8 @@ defmodule Access do
|
||||
end
|
||||
end
|
||||
|
||||
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
|
||||
|
||||
@doc """
|
||||
Fetches the value for the given key in a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
@@ -485,7 +487,7 @@ defmodule Access do
|
||||
## Accessors
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -514,30 +516,56 @@ defmodule Access do
|
||||
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
||||
{"john", %{user: %{}}}
|
||||
|
||||
An error is raised if the accessed structure is not a map or a struct:
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
|
||||
{"john", [user: []]}
|
||||
|
||||
iex> get_in([], [Access.key(:foo)])
|
||||
** (BadMapError) expected a map, got:
|
||||
...
|
||||
An error is raised if the accessed structure is not a map, struct, or keyword list:
|
||||
|
||||
iex> get_in(123, [Access.key(:foo)])
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
"""
|
||||
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
def key(key, default \\ nil) do
|
||||
fn
|
||||
:get, data, next ->
|
||||
:get, %{} = data, next ->
|
||||
next.(Map.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next ->
|
||||
:get_and_update, %{} = data, next ->
|
||||
value = Map.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Map.put(data, key, update)}
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -546,6 +574,19 @@ defmodule Access do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
{"john", [user: []]}
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
|
||||
** (KeyError) key :unknown not found in:
|
||||
...
|
||||
|
||||
iex> map = %{user: %{name: "john"}}
|
||||
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
@@ -574,13 +615,15 @@ defmodule Access do
|
||||
`Access.key!/1` is useful when the key is not known in advance
|
||||
and must be accessed dynamically.
|
||||
|
||||
An error is raised if the accessed structure is not a map/struct:
|
||||
An error is raised if the accessed structure is not a map/struct/keyword list:
|
||||
|
||||
iex> get_in([], [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
||||
iex> get_in(123, [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
|
||||
"""
|
||||
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
def key!(key) do
|
||||
fn
|
||||
:get, %{} = data, next ->
|
||||
@@ -594,8 +637,19 @@ defmodule Access do
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.fetch!(data, key))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.fetch!(data, key)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
|
||||
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -863,7 +917,7 @@ defmodule Access do
|
||||
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
|
||||
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
|
||||
|
||||
When no match is found, an empty list is returned and the update function is never called
|
||||
When no match is found, an empty list is returned and the update function is never called:
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
|
||||
@@ -880,7 +934,7 @@ defmodule Access do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
|
||||
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list)
|
||||
def filter(func) when is_function(func) do
|
||||
fn op, data, next -> filter(op, data, func, next) end
|
||||
end
|
||||
@@ -981,12 +1035,12 @@ defmodule Access do
|
||||
end
|
||||
|
||||
defp slice(:get_and_update, data, range, next) when is_list(data) do
|
||||
range = normalize_range(range, data)
|
||||
%Range{first: first, last: last, step: step} = normalize_range(range, data)
|
||||
|
||||
if range.first > range.last do
|
||||
if first > last do
|
||||
{[], data}
|
||||
else
|
||||
get_and_update_slice(data, range, next, [], [], 0)
|
||||
get_and_update_slice(data, first, last, step, next, [], [], 0)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1091,16 +1145,23 @@ defmodule Access do
|
||||
|
||||
defp normalize_range(range, _list), do: range
|
||||
|
||||
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
||||
if index in range do
|
||||
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index)
|
||||
when index > last do
|
||||
{:lists.reverse(gets), :lists.reverse(updates, rest)}
|
||||
end
|
||||
|
||||
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do
|
||||
if index >= first and rem(index - first, step) == 0 do
|
||||
case next.(head) do
|
||||
:pop ->
|
||||
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
|
||||
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1)
|
||||
|
||||
{get, update} ->
|
||||
get_and_update_slice(
|
||||
rest,
|
||||
range,
|
||||
first,
|
||||
last,
|
||||
step,
|
||||
next,
|
||||
[update | updates],
|
||||
[get | gets],
|
||||
@@ -1108,11 +1169,11 @@ defmodule Access do
|
||||
)
|
||||
end
|
||||
else
|
||||
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
|
||||
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
|
||||
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do
|
||||
{:lists.reverse(gets), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@@ -1139,7 +1200,7 @@ defmodule Access do
|
||||
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
|
||||
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
|
||||
|
||||
When no match is found, nil is returned and the update function is never called
|
||||
When no match is found, nil is returned and the update function is never called:
|
||||
|
||||
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
||||
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
|
||||
|
||||
@@ -327,7 +327,7 @@ defmodule Agent do
|
||||
passing the agent state. The result of the function invocation is
|
||||
returned from this function.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
@@ -366,7 +366,7 @@ defmodule Agent do
|
||||
elements, the first being the value to return (that is, the "get" value)
|
||||
and the second one being the new state of the agent.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
@@ -407,7 +407,7 @@ defmodule Agent do
|
||||
|
||||
This function always returns `:ok`.
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds are allowed before the agent executes the function and returns
|
||||
the result value, or the atom `:infinity` to wait indefinitely. If no result
|
||||
is received within the specified time, the function call fails and the caller
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -991,7 +996,7 @@ defmodule Application do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the directory for app.
|
||||
Gets the directory for `app`.
|
||||
|
||||
This information is returned based on the code path. Here is an
|
||||
example:
|
||||
@@ -1072,7 +1077,7 @@ defmodule Application do
|
||||
@doc """
|
||||
Formats the error reason returned by `start/2`,
|
||||
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
|
||||
returns a string.
|
||||
and returns a string.
|
||||
"""
|
||||
@spec format_error(any) :: String.t()
|
||||
def format_error(reason) do
|
||||
|
||||
+294
-70
@@ -155,6 +155,255 @@ defmodule Base do
|
||||
for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>>
|
||||
end
|
||||
|
||||
# SWAR (SIMD Within A Register) fast paths for valid16?/2 and valid32?/2
|
||||
# (non-hex). Each chunk of 8 bytes is validated in one guard: 7 bytes via
|
||||
# bitwise arithmetic on a single 56-bit integer, plus a per-byte range
|
||||
# check for the 8th byte. 56 bits is the largest width that fits in a BEAM
|
||||
# small int on 64-bit (fixnum range is 59-bit signed); at 64 bits every
|
||||
# `w + 0x80..` would allocate a bignum on the heap and the optimisation
|
||||
# would collapse. See https://github.com/erlang/otp/pull/10938 for the
|
||||
# corresponding pattern in OTP.
|
||||
@swar_mask80 0x80808080808080
|
||||
|
||||
# Per-range SWAR constants, broadcast across 7 lanes. Naming convention:
|
||||
# @swar_ge_X = 0x80 - X → high bit of `(w + @swar_ge_X)` lane is set
|
||||
# iff that byte is ≥ X
|
||||
# @swar_gt_X = 0x7F - X → high bit of `(w + @swar_gt_X)` lane is set
|
||||
# iff that byte is > X
|
||||
# A byte is in range [lo, hi] iff
|
||||
# `bxor(w + @swar_ge_lo, w + @swar_gt_hi)` has its high bit set.
|
||||
@swar_ge_0 0x50505050505050
|
||||
@swar_gt_9 0x46464646464646
|
||||
@swar_ge_2 0x4E4E4E4E4E4E4E
|
||||
@swar_gt_7 0x48484848484848
|
||||
@swar_ge_A 0x3F3F3F3F3F3F3F
|
||||
@swar_gt_F 0x39393939393939
|
||||
@swar_gt_V 0x29292929292929
|
||||
@swar_gt_Z 0x25252525252525
|
||||
@swar_ge_a 0x1F1F1F1F1F1F1F
|
||||
@swar_gt_f 0x19191919191919
|
||||
@swar_gt_v 0x09090909090909
|
||||
@swar_gt_z 0x05050505050505
|
||||
|
||||
# For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we
|
||||
# extend the digit range to [0x2F, 0x39], which absorbs '/' into one range
|
||||
# check — saves one singleton term. Trick lifted from
|
||||
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
|
||||
@swar_ge_slash 0x51515151515151
|
||||
|
||||
# base64's '+', '-' and '_' border no other range, so each is checked as the
|
||||
# degenerate range [c, c]. Mycroft zero-byte detection is one operation
|
||||
# cheaper per singleton but not exact per lane: subtracting 0x01..01 makes a
|
||||
# zero lane borrow from the lane above it, and a lane holding 1 turns that
|
||||
# borrow into 0xFF, so bytes equal to `c bxor 0x01` read as matches too.
|
||||
@swar_ge_plus 0x55555555555555
|
||||
@swar_gt_plus 0x54545454545454
|
||||
@swar_ge_dash 0x53535353535353
|
||||
@swar_gt_dash 0x52525252525252
|
||||
@swar_ge_underscore 0x21212121212121
|
||||
@swar_gt_underscore 0x20202020202020
|
||||
|
||||
# Per-byte validity checks (used in both the SWAR clauses for the 8th byte
|
||||
# of each stride and in the body of the sub-8-byte tail clauses).
|
||||
@compile {:inline,
|
||||
valid_char16upper?: 1,
|
||||
valid_char16lower?: 1,
|
||||
valid_char16mixed?: 1,
|
||||
valid_char32upper?: 1,
|
||||
valid_char32lower?: 1,
|
||||
valid_char32mixed?: 1,
|
||||
valid_char32hexupper?: 1,
|
||||
valid_char32hexlower?: 1,
|
||||
valid_char32hexmixed?: 1,
|
||||
valid_char64base?: 1,
|
||||
valid_char64url?: 1,
|
||||
valid_word16upper?: 1,
|
||||
valid_word16lower?: 1,
|
||||
valid_word16mixed?: 1,
|
||||
valid_word32upper?: 1,
|
||||
valid_word32lower?: 1,
|
||||
valid_word32mixed?: 1,
|
||||
valid_word32hexupper?: 1,
|
||||
valid_word32hexlower?: 1,
|
||||
valid_word32hexmixed?: 1,
|
||||
valid_word64base?: 1,
|
||||
valid_word64url?: 1}
|
||||
|
||||
defp valid_char16upper?(c), do: c in ?0..?9 or c in ?A..?F
|
||||
defp valid_char16lower?(c), do: c in ?0..?9 or c in ?a..?f
|
||||
defp valid_char16mixed?(c), do: c in ?0..?9 or c in ?A..?F or c in ?a..?f
|
||||
|
||||
defp valid_char32upper?(c), do: c in ?A..?Z or c in ?2..?7
|
||||
defp valid_char32lower?(c), do: c in ?a..?z or c in ?2..?7
|
||||
defp valid_char32mixed?(c), do: c in ?A..?Z or c in ?a..?z or c in ?2..?7
|
||||
|
||||
# Most common range first — letters dominate (22/32) over digits (10/32)
|
||||
# in hex base32, so letters go first in the OR short-circuit.
|
||||
defp valid_char32hexupper?(c), do: c in ?A..?V or c in ?0..?9
|
||||
defp valid_char32hexlower?(c), do: c in ?a..?v or c in ?0..?9
|
||||
defp valid_char32hexmixed?(c), do: c in ?A..?V or c in ?a..?v or c in ?0..?9
|
||||
|
||||
defp valid_char64base?(c),
|
||||
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?+ or c == ?/
|
||||
|
||||
defp valid_char64url?(c),
|
||||
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?- or c == ?_
|
||||
|
||||
# SWAR 7-byte word validity. Structure for each function:
|
||||
# 1. ASCII gate `band(w, MASK80) == 0` — every byte < 0x80 so the
|
||||
# additions below cannot carry across lanes.
|
||||
# 2. "Each byte is in range A OR range B (OR range C)" gate — OR per-
|
||||
# range XOR masks (high bit set in lane iff byte in that range), AND
|
||||
# with MASK80, demand all 7 high bits set.
|
||||
defp valid_word16upper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_F)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word16lower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_f)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word16mixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_F)
|
||||
),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_f)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32upper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32lower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32mixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_2, w + @swar_gt_7)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexupper?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_V)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexlower?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_v)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word32hexmixed?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9),
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_V)
|
||||
),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_v)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
# base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons.
|
||||
# For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as
|
||||
# part of one range (Lemire merge), leaving only '+' as a singleton. For url,
|
||||
# '-' and '_' are two more singletons.
|
||||
defp valid_word64base?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_slash, w + @swar_gt_9)
|
||||
),
|
||||
bxor(w + @swar_ge_plus, w + @swar_gt_plus)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
defp valid_word64url?(w) do
|
||||
band(w, @swar_mask80) == 0 and
|
||||
band(
|
||||
bor(
|
||||
bor(
|
||||
bor(
|
||||
bxor(w + @swar_ge_A, w + @swar_gt_Z),
|
||||
bxor(w + @swar_ge_a, w + @swar_gt_z)
|
||||
),
|
||||
bxor(w + @swar_ge_0, w + @swar_gt_9)
|
||||
),
|
||||
bor(
|
||||
bxor(w + @swar_ge_dash, w + @swar_gt_dash),
|
||||
bxor(w + @swar_ge_underscore, w + @swar_gt_underscore)
|
||||
)
|
||||
),
|
||||
@swar_mask80
|
||||
) == @swar_mask80
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes a binary string into a base 16 encoded string.
|
||||
|
||||
@@ -187,7 +436,7 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [upper: b16_alphabet, lower: to_lower_enc.(b16_alphabet)] do
|
||||
name = :"encode16#{base}"
|
||||
name = String.to_unsafe_atom("encode16#{base}")
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -287,7 +536,7 @@ defmodule Base do
|
||||
* `:lower` - only allows lower case characters
|
||||
* `:mixed` - allows mixed case characters
|
||||
|
||||
An `ArgumentError` exception is raised if the padding is incorrect or
|
||||
An `ArgumentError` exception is raised if the string has an odd length or
|
||||
a non-alphabet character is present in the string.
|
||||
|
||||
## Examples
|
||||
@@ -368,28 +617,24 @@ defmodule Base do
|
||||
upper = Enum.with_index(b16_alphabet)
|
||||
|
||||
for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do
|
||||
decode_name = :"decode16#{base}!"
|
||||
validate_name = :"validate16#{base}?"
|
||||
valid_char_name = :"valid_char16#{base}?"
|
||||
decode_name = String.to_unsafe_atom("decode16#{base}!")
|
||||
validate_name = String.to_unsafe_atom("validate16#{base}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char16#{base}?")
|
||||
valid_word_name = String.to_unsafe_atom("valid_word16#{base}?")
|
||||
|
||||
{min, decoded} = to_decode_list.(alphabet)
|
||||
|
||||
# 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)
|
||||
@@ -490,7 +735,7 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
|
||||
name = :"encode64#{base}"
|
||||
name = String.to_unsafe_atom("encode64#{base}")
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -756,28 +1001,24 @@ defmodule Base do
|
||||
end
|
||||
|
||||
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
|
||||
decode_name = :"decode64#{base}!"
|
||||
decode_name = String.to_unsafe_atom("decode64#{base}!")
|
||||
|
||||
validate_name = :"validate64#{base}?"
|
||||
validate_main_name = :"validate_main64#{validate_name}?"
|
||||
valid_char_name = :"valid_char64#{base}?"
|
||||
validate_name = String.to_unsafe_atom("validate64#{base}?")
|
||||
validate_main_name = String.to_unsafe_atom("validate_main64#{validate_name}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char64#{base}?")
|
||||
valid_word_name = String.to_unsafe_atom("valid_word64#{base}?")
|
||||
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?`
|
||||
# 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)
|
||||
|
||||
@@ -1080,7 +1314,7 @@ defmodule Base do
|
||||
hexupper: b32hex_alphabet,
|
||||
hexlower: to_lower_enc.(b32hex_alphabet)
|
||||
] do
|
||||
name = :"encode32#{base}"
|
||||
name = String.to_unsafe_atom("encode32#{base}")
|
||||
encoded = to_encode_list.(alphabet)
|
||||
|
||||
@compile {:inline, [{name, 1}]}
|
||||
@@ -1154,7 +1388,7 @@ defmodule Base do
|
||||
|
||||
The values for `:case` can be:
|
||||
|
||||
* `:upper` - only allows upper case characters (default)
|
||||
* `:upper` - only allows upper case characters (default)
|
||||
* `:lower` - only allows lower case characters
|
||||
* `:mixed` - allows mixed case characters
|
||||
|
||||
@@ -1419,27 +1653,24 @@ defmodule Base do
|
||||
hexlower: to_lower_dec.(hexupper),
|
||||
hexmixed: to_mixed_dec.(hexupper)
|
||||
] do
|
||||
decode_name = :"decode32#{base}!"
|
||||
validate_name = :"validate32#{base}?"
|
||||
validate_main_name = :"validate_main32#{validate_name}?"
|
||||
valid_char_name = :"valid_char32#{base}?"
|
||||
decode_name = String.to_unsafe_atom("decode32#{base}!")
|
||||
validate_name = String.to_unsafe_atom("validate32#{base}?")
|
||||
validate_main_name = String.to_unsafe_atom("validate_main32#{validate_name}?")
|
||||
valid_char_name = String.to_unsafe_atom("valid_char32#{base}?")
|
||||
{min, decoded} = to_decode_list.(alphabet)
|
||||
|
||||
# SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?`
|
||||
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
|
||||
# 8-byte-multiple `main`) recurses through the single-byte clause.
|
||||
valid_word_name = String.to_unsafe_atom("valid_word32#{base}?")
|
||||
|
||||
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
|
||||
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
+56
-29
@@ -3,6 +3,8 @@
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Calendar do
|
||||
@strftime_max_width 1024
|
||||
|
||||
@moduledoc """
|
||||
This module defines the responsibilities for working with
|
||||
calendars, dates, times and datetimes in Elixir.
|
||||
@@ -104,6 +106,7 @@ defmodule Calendar do
|
||||
@typedoc "Any map or struct that contains the time fields."
|
||||
@type time :: %{
|
||||
optional(any) => any,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
@@ -293,7 +296,7 @@ defmodule Calendar do
|
||||
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Define the rollover moment for the calendar.
|
||||
Defines the rollover moment for the calendar.
|
||||
|
||||
This is the moment, in your calendar, when the current day ends
|
||||
and the next day starts.
|
||||
@@ -379,13 +382,13 @@ defmodule Calendar do
|
||||
@callback iso_days_to_end_of_day(iso_days) :: iso_days
|
||||
|
||||
@doc """
|
||||
Shifts date by given duration according to its calendar.
|
||||
Shifts date by the given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
|
||||
|
||||
@doc """
|
||||
Shifts naive datetime by given duration according to its calendar.
|
||||
Shifts naive datetime by the given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_naive_datetime(
|
||||
@@ -400,7 +403,7 @@ defmodule Calendar do
|
||||
) :: {year, month, day, hour, minute, second, microsecond}
|
||||
|
||||
@doc """
|
||||
Shifts time by given duration according to its calendar.
|
||||
Shifts time by the given duration according to its calendar.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
|
||||
@@ -529,6 +532,7 @@ defmodule Calendar do
|
||||
* `%`: indicates the start of a formatted section
|
||||
* `<padding>`: set the padding (see below)
|
||||
* `<width>`: a number indicating the minimum size of the formatted section
|
||||
(maximum #{@strftime_max_width})
|
||||
* `<format>`: the format itself (see below)
|
||||
|
||||
### Accepted padding options
|
||||
@@ -559,11 +563,11 @@ defmodule Calendar do
|
||||
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
||||
q | Quarter | 1, 2, 3, 4
|
||||
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
|
||||
S | Second | 00, 59, 60
|
||||
S | Second | 00, 59
|
||||
u | Day of the week | 1 (Monday), 7 (Sunday)
|
||||
x | Preferred date (without time) representation | 2018-10-17
|
||||
X | Preferred time (without date) representation | 12:34:56
|
||||
y | Year as 2-digits | 01, 01, 86, 18
|
||||
y | Year as 2-digits | -01, 01, 86, 18
|
||||
Y | Year | -0001, 0001, 1986
|
||||
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
||||
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
||||
@@ -620,7 +624,7 @@ defmodule Calendar do
|
||||
...>)
|
||||
"серпень"
|
||||
|
||||
Microsecond formatting:
|
||||
Microsecond formatting:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
|
||||
"19-08-26 13:52:06.0"
|
||||
@@ -667,9 +671,13 @@ defmodule Calendar do
|
||||
end
|
||||
|
||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||
new_width = (width || 0) * 10 + (digit - ?0)
|
||||
width = (width || 0) * 10 + (digit - ?0)
|
||||
|
||||
parse_modifiers(rest, new_width, pad, parser_data)
|
||||
if width > @strftime_max_width do
|
||||
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
|
||||
end
|
||||
|
||||
parse_modifiers(rest, width, pad, parser_data)
|
||||
end
|
||||
|
||||
# set default padding if none was specified
|
||||
@@ -704,7 +712,7 @@ defmodule Calendar do
|
||||
|
||||
# Literally just %
|
||||
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
|
||||
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc])
|
||||
end
|
||||
|
||||
# Abbreviated name of day
|
||||
@@ -773,7 +781,7 @@ defmodule Calendar do
|
||||
|
||||
# Day of the month
|
||||
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -792,31 +800,35 @@ defmodule Calendar do
|
||||
|
||||
# Hour using a 24-hour clock
|
||||
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 12-hour clock
|
||||
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the year
|
||||
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Month
|
||||
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Minute
|
||||
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -844,19 +856,23 @@ defmodule Calendar do
|
||||
|
||||
# Quarter
|
||||
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Second
|
||||
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the week
|
||||
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -906,20 +922,25 @@ defmodule Calendar do
|
||||
|
||||
# Year as 2-digits
|
||||
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
result =
|
||||
if datetime.year < 0 do
|
||||
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
||||
else
|
||||
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
end
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year
|
||||
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
{sign, year} =
|
||||
result =
|
||||
if datetime.year < 0 do
|
||||
{?-, -datetime.year}
|
||||
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)]
|
||||
else
|
||||
{[], datetime.year}
|
||||
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)
|
||||
end
|
||||
|
||||
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -966,7 +987,7 @@ defmodule Calendar do
|
||||
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
||||
|
||||
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
||||
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
|
||||
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}"
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
@@ -985,13 +1006,19 @@ defmodule Calendar do
|
||||
raise ArgumentError, "invalid strftime format: %#{next}"
|
||||
end
|
||||
|
||||
defp pad_preferred(result, width, pad) when length(result) < width do
|
||||
pad_preferred([pad | result], width, pad)
|
||||
defp pad_preferred(result, width, pad) do
|
||||
result
|
||||
|> IO.iodata_to_binary()
|
||||
|> pad_leading(width, pad)
|
||||
end
|
||||
|
||||
defp pad_preferred(result, _width, _pad), do: result
|
||||
|
||||
defp pad_leading(string, count, padding) do
|
||||
to_pad = count - String.length(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
|
||||
defp pad_leading_ascii(string, count, padding) do
|
||||
to_pad = count - byte_size(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
@@ -81,7 +81,7 @@ defmodule Date do
|
||||
|
||||
Ranges of dates can be increasing (`first <= last`) and are
|
||||
always inclusive. For a decreasing range, use `range/3` with
|
||||
a step of -1 as first argument.
|
||||
a step of -1 as third argument.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -160,7 +160,7 @@ defmodule Date do
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"both dates must have matching calendar and the step must be a " <>
|
||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
|
||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}"
|
||||
end
|
||||
|
||||
defp range(first, first_days, last, last_days, calendar, step) do
|
||||
@@ -422,7 +422,7 @@ defmodule Date do
|
||||
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
|
||||
date
|
||||
|> convert!(Calendar.ISO)
|
||||
|> to_iso8601()
|
||||
|> to_iso8601(format)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -556,14 +556,18 @@ defmodule Date do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
|
||||
def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
|
||||
%{year: year1, month: month1, day: day1} = date1
|
||||
%{year: year2, month: month2, day: day2} = date2
|
||||
|
||||
case {{year1, month1, day1}, {year2, month2, day2}} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
def compare(
|
||||
%{year: year1, month: month1, day: day1, calendar: calendar},
|
||||
%{year: year2, month: month2, day: day2, calendar: calendar}
|
||||
) do
|
||||
cond do
|
||||
year1 > year2 -> :gt
|
||||
year1 < year2 -> :lt
|
||||
month1 > month2 -> :gt
|
||||
month1 < month2 -> :lt
|
||||
day1 > day2 -> :gt
|
||||
day1 < day2 -> :lt
|
||||
true -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1047,7 +1051,7 @@ defmodule Date do
|
||||
@doc """
|
||||
Calculates the quarter of the year of a given `date`.
|
||||
|
||||
Returns the day of the year as an integer. For the ISO 8601
|
||||
Returns the quarter of the year as an integer. For the ISO 8601
|
||||
calendar (the default), it is an integer from 1 to 4.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -37,21 +37,19 @@ defmodule Date.Range do
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
} = range,
|
||||
},
|
||||
%Date{calendar: calendar} = date
|
||||
) do
|
||||
{days, _} = Date.to_iso_days(date)
|
||||
|
||||
cond do
|
||||
empty?(range) ->
|
||||
{:ok, false}
|
||||
in_range? =
|
||||
if step > 0 do
|
||||
first_days <= days and days <= last_days and rem(days - first_days, step) == 0
|
||||
else
|
||||
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
|
||||
end
|
||||
|
||||
first_days <= last_days ->
|
||||
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
|
||||
|
||||
true ->
|
||||
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
|
||||
end
|
||||
{:ok, in_range?}
|
||||
end
|
||||
|
||||
def member?(%Date.Range{step: _}, _) do
|
||||
@@ -59,11 +57,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
|
||||
@@ -79,7 +86,7 @@ defmodule Date.Range do
|
||||
step: step
|
||||
} = range
|
||||
) do
|
||||
{:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
|
||||
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
@@ -176,7 +183,7 @@ defmodule Date.Range do
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
}),
|
||||
do: abs(div(last_days - first_days, step)) + 1
|
||||
do: div(last_days - first_days, step) + 1
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp size(
|
||||
@@ -186,43 +193,16 @@ defmodule Date.Range do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
size(Map.put(date_range, :step, step))
|
||||
end
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step > 0 and first_days > last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step < 0 and first_days < last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{step: _}), do: false
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
defp empty?(
|
||||
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
|
||||
date_range
|
||||
) do
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
empty?(Map.put(date_range, :step, step))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
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
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ defmodule DateTime do
|
||||
since v1.15.0.
|
||||
|
||||
The default unit if none gets passed is `:native`,
|
||||
which results on a default resolution of microseconds.
|
||||
which results in a default resolution of microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -720,6 +720,9 @@ defmodule DateTime do
|
||||
Other time zone databases can be passed as argument or set globally.
|
||||
See the "Time zone database" section in the module docs.
|
||||
|
||||
Shifting to the `"Etc/UTC"` time zone always succeeds without
|
||||
consulting the `time_zone_database`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
|
||||
@@ -753,6 +756,28 @@ defmodule DateTime do
|
||||
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
|
||||
end
|
||||
|
||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
|
||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
||||
calendar.naive_datetime_from_iso_days(iso_days_utc)
|
||||
|
||||
datetime = %DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision},
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
{:ok, datetime}
|
||||
end
|
||||
|
||||
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
|
||||
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
|
||||
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
|
||||
@@ -1177,7 +1202,7 @@ defmodule DateTime do
|
||||
datetime
|
||||
|> to_iso_days()
|
||||
# Subtract total original offset in order to get UTC and add the new offset
|
||||
|> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|
||||
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second)
|
||||
|> calendar.naive_datetime_from_iso_days()
|
||||
end
|
||||
|
||||
@@ -1296,9 +1321,9 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian seconds to a `DateTime` struct.
|
||||
Converts a number of Gregorian seconds to a `DateTime` struct.
|
||||
|
||||
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
|
||||
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use
|
||||
`DateTime.shift_zone/3`.
|
||||
|
||||
## Examples
|
||||
@@ -1341,7 +1366,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
|
||||
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1576,15 +1601,15 @@ defmodule DateTime do
|
||||
def diff(datetime1, datetime2, unit \\ :second)
|
||||
|
||||
def diff(datetime1, datetime2, :day) do
|
||||
diff(datetime1, datetime2, :second) |> div(86400)
|
||||
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000)
|
||||
end
|
||||
|
||||
def diff(datetime1, datetime2, :hour) do
|
||||
diff(datetime1, datetime2, :second) |> div(3600)
|
||||
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000)
|
||||
end
|
||||
|
||||
def diff(datetime1, datetime2, :minute) do
|
||||
diff(datetime1, datetime2, :second) |> div(60)
|
||||
diff(datetime1, datetime2, :microsecond) |> div(60_000_000)
|
||||
end
|
||||
|
||||
def diff(
|
||||
@@ -1626,6 +1651,9 @@ defmodule DateTime do
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
||||
always succeeds without consulting the `time_zone_database`.
|
||||
|
||||
This function relies on a contiguous representation of time,
|
||||
ignoring timezone changes. For example, if you add one day when there
|
||||
are summer time/daylight saving time changes, it will also change the
|
||||
@@ -1744,6 +1772,9 @@ defmodule DateTime do
|
||||
|
||||
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
|
||||
|
||||
If the datetime is in the `"Etc/UTC"` time zone, this function
|
||||
always succeeds without consulting the `time_zone_database`.
|
||||
|
||||
This operation is equivalent to shifting the datetime wall clock
|
||||
(in other words, the value as someone in that timezone would see
|
||||
on their watch), then applying the time zone offset to convert it
|
||||
@@ -1812,44 +1843,6 @@ defmodule DateTime do
|
||||
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
|
||||
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
|
||||
|
||||
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
} = datetime
|
||||
|
||||
{year, month, day, hour, minute, second, microsecond} =
|
||||
calendar.shift_naive_datetime(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
__duration__!(duration)
|
||||
)
|
||||
|
||||
%DateTime{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
time_zone: "Etc/UTC",
|
||||
zone_abbr: "UTC",
|
||||
std_offset: 0,
|
||||
utc_offset: 0
|
||||
}
|
||||
end
|
||||
|
||||
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
|
||||
%{
|
||||
year: year,
|
||||
@@ -2060,11 +2053,12 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
defp apply_tz_offset(iso_days, offset) do
|
||||
Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
|
||||
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second)
|
||||
end
|
||||
|
||||
defp from_map(%{} = datetime_map) do
|
||||
%DateTime{
|
||||
calendar: datetime_map.calendar,
|
||||
year: datetime_map.year,
|
||||
month: datetime_map.month,
|
||||
day: datetime_map.day,
|
||||
|
||||
@@ -72,7 +72,7 @@ defmodule Duration do
|
||||
|
||||
However, once again, it is important to remember that shifting a duration is not
|
||||
arithmetic, so you may want to use the functions in this module depending on what
|
||||
you to achieve. Compare the results of both examples below:
|
||||
you want to achieve. Compare the results of both examples below:
|
||||
|
||||
# Adding one month after the other
|
||||
iex> date = ~D[2016-01-31]
|
||||
@@ -129,6 +129,16 @@ defmodule Duration do
|
||||
second: 0,
|
||||
microsecond: {0, 0}
|
||||
|
||||
@typedoc """
|
||||
The microsecond component of a duration.
|
||||
|
||||
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
|
||||
durations may represent negative amounts of time. The precision is
|
||||
an integer from 0 to 6 holding the number of significant digits,
|
||||
as in the calendar types.
|
||||
"""
|
||||
@type microsecond :: {value :: integer, precision :: 0..6}
|
||||
|
||||
@typedoc """
|
||||
The duration struct type.
|
||||
"""
|
||||
@@ -140,7 +150,7 @@ defmodule Duration do
|
||||
hour: integer,
|
||||
minute: integer,
|
||||
second: integer,
|
||||
microsecond: Calendar.microsecond()
|
||||
microsecond: microsecond()
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
@@ -154,7 +164,7 @@ defmodule Duration do
|
||||
| {:hour, integer}
|
||||
| {:minute, integer}
|
||||
| {:second, integer}
|
||||
| {:microsecond, Calendar.microsecond()}
|
||||
| {:microsecond, microsecond()}
|
||||
|
||||
@typedoc """
|
||||
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
|
||||
@@ -232,7 +242,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
|
||||
|
||||
@@ -243,26 +253,26 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec add(t, t) :: t
|
||||
def add(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
|
||||
%Duration{
|
||||
year: d1.year + d2.year,
|
||||
month: d1.month + d2.month,
|
||||
week: d1.week + d2.week,
|
||||
day: d1.day + d2.day,
|
||||
hour: d1.hour + d2.hour,
|
||||
minute: d1.minute + d2.minute,
|
||||
second: d1.second + d2.second,
|
||||
microsecond: {m1 + m2, max(p1, p2)}
|
||||
year: y1 + y2,
|
||||
month: mo1 + mo2,
|
||||
week: w1 + w2,
|
||||
day: day1 + day2,
|
||||
hour: h1 + h2,
|
||||
minute: mi1 + mi2,
|
||||
second: s1 + s2,
|
||||
microsecond: {ms1 + ms2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@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
|
||||
|
||||
@@ -273,19 +283,19 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec subtract(t, t) :: t
|
||||
def subtract(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
|
||||
%Duration{
|
||||
year: d1.year - d2.year,
|
||||
month: d1.month - d2.month,
|
||||
week: d1.week - d2.week,
|
||||
day: d1.day - d2.day,
|
||||
hour: d1.hour - d2.hour,
|
||||
minute: d1.minute - d2.minute,
|
||||
second: d1.second - d2.second,
|
||||
microsecond: {m1 - m2, max(p1, p2)}
|
||||
year: y1 - y2,
|
||||
month: mo1 - mo2,
|
||||
week: w1 - w2,
|
||||
day: day1 - day2,
|
||||
hour: h1 - h2,
|
||||
minute: mi1 - mi2,
|
||||
second: s1 - s2,
|
||||
microsecond: {ms1 - ms2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -302,14 +312,16 @@ defmodule Duration do
|
||||
"""
|
||||
@spec multiply(t, integer) :: t
|
||||
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: duration.year * integer,
|
||||
month: duration.month * integer,
|
||||
week: duration.week * integer,
|
||||
day: duration.day * integer,
|
||||
hour: duration.hour * integer,
|
||||
minute: duration.minute * integer,
|
||||
second: duration.second * integer,
|
||||
year: y * integer,
|
||||
month: mo * integer,
|
||||
week: w * integer,
|
||||
day: d * integer,
|
||||
hour: h * integer,
|
||||
minute: mi * integer,
|
||||
second: s * integer,
|
||||
microsecond: {ms * integer, p}
|
||||
}
|
||||
end
|
||||
@@ -327,14 +339,16 @@ defmodule Duration do
|
||||
"""
|
||||
@spec negate(t) :: t
|
||||
def negate(%Duration{microsecond: {ms, p}} = duration) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: -duration.year,
|
||||
month: -duration.month,
|
||||
week: -duration.week,
|
||||
day: -duration.day,
|
||||
hour: -duration.hour,
|
||||
minute: -duration.minute,
|
||||
second: -duration.second,
|
||||
year: -y,
|
||||
month: -mo,
|
||||
week: -w,
|
||||
day: -d,
|
||||
hour: -h,
|
||||
minute: -mi,
|
||||
second: -s,
|
||||
microsecond: {-ms, p}
|
||||
}
|
||||
end
|
||||
|
||||
+141
-102
@@ -33,8 +33,9 @@ defmodule Calendar.ISO do
|
||||
you to format datetimes however else you desire.
|
||||
|
||||
Elixir does not support reduced accuracy formats (for example, a date without
|
||||
the day component) nor decimal precisions in the lowest component (such as
|
||||
`10:01:25,5`).
|
||||
the day component) nor decimal precisions in components other than seconds
|
||||
(such as `10:01,5`). Fractional seconds are supported and truncated to
|
||||
microsecond precision.
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -81,6 +82,11 @@ defmodule Calendar.ISO do
|
||||
iex> Calendar.ISO.parse_time("23")
|
||||
{:error, :invalid_format}
|
||||
|
||||
A decimal fraction is accepted on seconds, but not on any other component:
|
||||
|
||||
iex> Calendar.ISO.parse_time("10:01:25,5")
|
||||
{:ok, {10, 1, 25, {500000, 1}}}
|
||||
|
||||
### Extensions
|
||||
|
||||
The parser and formatter adopt one ISO 8601 extension: extended year notation.
|
||||
@@ -417,16 +423,13 @@ defmodule Calendar.ISO do
|
||||
@spec parse_date(String.t(), format) ::
|
||||
{:ok, {year, month, day}}
|
||||
| {:error, atom}
|
||||
def parse_date(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_date_guarded(string, format)
|
||||
|
||||
defp parse_date_guarded("-" <> string, format),
|
||||
def parse_date("-" <> string, format) when is_format(format),
|
||||
do: do_parse_date(string, -1, format)
|
||||
|
||||
defp parse_date_guarded("+" <> string, format),
|
||||
def parse_date("+" <> string, format) when is_format(format),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
defp parse_date_guarded(string, format),
|
||||
def parse_date(string, format) when is_binary(string) and is_format(format),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do
|
||||
@@ -502,16 +505,13 @@ defmodule Calendar.ISO do
|
||||
@spec parse_naive_datetime(String.t(), format) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}}
|
||||
| {:error, atom}
|
||||
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_naive_datetime_guarded(string, format)
|
||||
|
||||
defp parse_naive_datetime_guarded("-" <> string, format),
|
||||
def parse_naive_datetime("-" <> string, format) when is_format(format),
|
||||
do: do_parse_naive_datetime(string, -1, format)
|
||||
|
||||
defp parse_naive_datetime_guarded("+" <> string, format),
|
||||
def parse_naive_datetime("+" <> string, format) when is_format(format),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
defp parse_naive_datetime_guarded(string, format),
|
||||
def parse_naive_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_naive_datetime(
|
||||
@@ -606,16 +606,13 @@ defmodule Calendar.ISO do
|
||||
@spec parse_utc_datetime(String.t(), format) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
|
||||
| {:error, atom}
|
||||
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: parse_utc_datetime_guarded(string, format)
|
||||
|
||||
defp parse_utc_datetime_guarded("-" <> string, format),
|
||||
def parse_utc_datetime("-" <> string, format) when is_format(format),
|
||||
do: do_parse_utc_datetime(string, -1, format)
|
||||
|
||||
defp parse_utc_datetime_guarded("+" <> string, format),
|
||||
def parse_utc_datetime("+" <> string, format) when is_format(format),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
defp parse_utc_datetime_guarded(string, format),
|
||||
def parse_utc_datetime(string, format) when is_binary(string) and is_format(format),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_utc_datetime(
|
||||
@@ -666,12 +663,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, 86_400) do
|
||||
case add_time_unit_to_iso_days({0, day_fraction}, -offset, :second) do
|
||||
{0, day_fraction} ->
|
||||
{{year, month, day}, time_from_day_fraction(day_fraction)}
|
||||
|
||||
{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
|
||||
|
||||
@@ -683,75 +680,73 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
|
||||
Parses an ISO 8601 formatted duration string to a list of `Duration` compatible unit pairs.
|
||||
|
||||
See `Duration.from_iso8601/1`.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
|
||||
def parse_duration("P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
parse_duration_date(string, 1, [], 0)
|
||||
end
|
||||
|
||||
def parse_duration("+P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
parse_duration_date(string, 1, [], 0)
|
||||
end
|
||||
|
||||
def parse_duration("-P" <> string) when byte_size(string) > 0 do
|
||||
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
|
||||
{:ok,
|
||||
Enum.map(fields, fn
|
||||
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
|
||||
{unit, value} -> {unit, -value}
|
||||
end)}
|
||||
end
|
||||
parse_duration_date(string, -1, [], 0)
|
||||
end
|
||||
|
||||
def parse_duration(_) do
|
||||
{:error, :invalid_duration}
|
||||
end
|
||||
|
||||
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
|
||||
defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
|
||||
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
|
||||
defp parse_duration_date("T" <> string, sign, acc, _min_position)
|
||||
when byte_size(string) > 0 do
|
||||
parse_duration_time(string, sign, acc, 0)
|
||||
end
|
||||
|
||||
defp parse_duration_date(string, acc, allowed) do
|
||||
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
|
||||
{key, allowed} <- find_unit(allowed, next) do
|
||||
parse_duration_date(rest, [{key, integer} | acc], allowed)
|
||||
defp parse_duration_date(string, sign, acc, min_position) do
|
||||
with {integer, <<unit, rest::binary>>} <- Integer.parse(string),
|
||||
{key, next_min_position} <- find_date_unit(min_position, unit) do
|
||||
parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position)
|
||||
else
|
||||
_ -> {:error, :invalid_date_component}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
|
||||
defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_time(string, acc, allowed) do
|
||||
defp parse_duration_time(string, sign, acc, min_position) do
|
||||
case Integer.parse(string) do
|
||||
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
|
||||
case parse_microsecond(rest) do
|
||||
{{ms, precision}, "S"} ->
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
with {:second, _next_min_position} <- find_time_unit(min_position, ?S),
|
||||
{{ms, precision}, "S"} <- parse_microsecond(rest) do
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
|
||||
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
|
||||
{:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc}
|
||||
else
|
||||
_ ->
|
||||
{:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
{integer, <<next, rest::binary>>} ->
|
||||
case find_unit(allowed, next) do
|
||||
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
|
||||
false -> {:error, :invalid_time_component}
|
||||
{integer, <<unit, rest::binary>>} ->
|
||||
case find_time_unit(min_position, unit) do
|
||||
{key, next_min_position} ->
|
||||
parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position)
|
||||
|
||||
false ->
|
||||
{:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -759,9 +754,17 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
|
||||
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
|
||||
defp find_unit([], _unit), do: false
|
||||
# The minimum position is the earliest unit still allowed, or one past the end.
|
||||
defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1}
|
||||
defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2}
|
||||
defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3}
|
||||
defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4}
|
||||
defp find_date_unit(_min_position, _unit), do: false
|
||||
|
||||
defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1}
|
||||
defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2}
|
||||
defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3}
|
||||
defp find_time_unit(_min_position, _unit), do: false
|
||||
|
||||
@doc """
|
||||
Returns the `t:Calendar.iso_days/0` format of the specified date.
|
||||
@@ -892,12 +895,15 @@ defmodule Calendar.ISO do
|
||||
|
||||
# Converts year, month, day to count of days since 0000-01-01.
|
||||
@doc false
|
||||
def date_to_iso_days(0, 1, 1), do: 0
|
||||
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
|
||||
|
||||
def date_to_iso_days(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
valid_date_to_iso_days(year, month, day)
|
||||
end
|
||||
|
||||
defp valid_date_to_iso_days(0, 1, 1), do: 0
|
||||
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
|
||||
|
||||
defp valid_date_to_iso_days(year, month, day) do
|
||||
y = if month <= 2, do: year - 1, else: year
|
||||
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
|
||||
year_of_era = y - era * @years_per_era
|
||||
@@ -1131,6 +1137,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
It is an integer from 1 to 4.
|
||||
|
||||
In the ISO calendar, the quarter is determined solely by the month, so the `year`
|
||||
and `day` arguments are ignored. Combination of `year`, `month`, and `day` is not
|
||||
validated as a valid date, unlike in `day_of_year/3`. Use `valid_date?/3` when
|
||||
full date validation is required.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.quarter_of_year(2016, 1, 31)
|
||||
@@ -1173,7 +1184,7 @@ defmodule Calendar.ISO do
|
||||
@doc since: "1.8.0"
|
||||
@spec year_of_era(year) :: {1..10_000, era}
|
||||
def year_of_era(year) when is_year_CE(year), do: {year, 1}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {1 - year, 0}
|
||||
|
||||
@doc """
|
||||
Calendar callback to compute the year and era from the
|
||||
@@ -1226,7 +1237,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
def day_of_era(year, month, day) when is_year_BCE(year) do
|
||||
day = abs(date_to_iso_days(year, month, day) - @iso_epoch)
|
||||
day = @iso_epoch - date_to_iso_days(year, month, day)
|
||||
{day, 0}
|
||||
end
|
||||
|
||||
@@ -1453,7 +1464,7 @@ defmodule Calendar.ISO do
|
||||
@doc """
|
||||
Converts the given naive_datetime into a iodata.
|
||||
|
||||
See `naive_datetime_to_iodata/8` for more information.
|
||||
See `naive_datetime_to_string/8` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1578,7 +1589,7 @@ defmodule Calendar.ISO do
|
||||
@doc """
|
||||
Converts the given datetime into a iodata.
|
||||
|
||||
See `datetime_to_iodata/12` for more information.
|
||||
See `datetime_to_string/12` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1682,7 +1693,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines if the date given is valid according to the proleptic Gregorian calendar.
|
||||
Determines if the time given is valid.
|
||||
|
||||
Leap seconds are not supported by the built-in Calendar.ISO.
|
||||
|
||||
@@ -1702,7 +1713,7 @@ defmodule Calendar.ISO do
|
||||
boolean
|
||||
def valid_time?(hour, minute, second, {ms_value, ms_precision} = _microsecond)
|
||||
when is_integer(hour) and is_integer(minute) and is_integer(second) and is_integer(ms_value) and
|
||||
is_integer(ms_value) do
|
||||
is_integer(ms_precision) do
|
||||
is_hour(hour) and is_minute(minute) and is_second(second) and
|
||||
is_microsecond(ms_value, ms_precision)
|
||||
end
|
||||
@@ -1835,6 +1846,9 @@ defmodule Calendar.ISO do
|
||||
shift_options = shift_datetime_options(duration)
|
||||
|
||||
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
|
||||
{:microsecond, {0, _}}, naive_datetime ->
|
||||
naive_datetime
|
||||
|
||||
{_, 0}, naive_datetime ->
|
||||
naive_datetime
|
||||
|
||||
@@ -1864,6 +1878,9 @@ defmodule Calendar.ISO do
|
||||
shift_options = shift_time_options(duration)
|
||||
|
||||
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
|
||||
{:microsecond, {0, _}}, time ->
|
||||
time
|
||||
|
||||
{_, 0}, time ->
|
||||
time
|
||||
|
||||
@@ -1875,7 +1892,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()
|
||||
|
||||
@@ -1926,8 +1943,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
|
||||
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
|
||||
ppd = System.convert_time_unit(86_400, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, value, ppd)
|
||||
add_time_unit_to_iso_days(iso_days, value, unit)
|
||||
end
|
||||
|
||||
defp shift_time_unit_values({0, _}, {_, original_precision}) do
|
||||
@@ -2008,8 +2024,8 @@ defmodule Calendar.ISO do
|
||||
total = System.convert_time_unit(integer, unit, :microsecond)
|
||||
|
||||
if total in @unix_range_microseconds do
|
||||
microseconds = Integer.mod(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
|
||||
{seconds, microseconds} = div_rem(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + seconds
|
||||
precision = precision_for_unit(unit)
|
||||
{date, time} = iso_seconds_to_datetime(seconds)
|
||||
{:ok, date, time, {microseconds, precision}}
|
||||
@@ -2030,38 +2046,27 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_microsecond("." <> rest) do
|
||||
case parse_microsecond(rest, 0, []) do
|
||||
{[], 0, _} ->
|
||||
:error
|
||||
defp parse_microsecond("." <> rest), do: parse_microsecond(rest, rest, 0)
|
||||
defp parse_microsecond("," <> rest), do: parse_microsecond(rest, rest, 0)
|
||||
defp parse_microsecond(rest), do: {{0, 0}, rest}
|
||||
|
||||
{microsecond, precision, rest} ->
|
||||
scale = scale_factor(precision)
|
||||
{{:erlang.list_to_integer(microsecond) * scale, precision}, rest}
|
||||
end
|
||||
end
|
||||
# Digits past the sixth are consumed but do not contribute to the value.
|
||||
defp parse_microsecond(<<head, tail::binary>>, digits, 6) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, digits, 6)
|
||||
|
||||
defp parse_microsecond("," <> rest) do
|
||||
parse_microsecond("." <> rest)
|
||||
end
|
||||
defp parse_microsecond(<<head, tail::binary>>, digits, precision) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, digits, precision + 1)
|
||||
|
||||
defp parse_microsecond(rest) do
|
||||
{{0, 0}, rest}
|
||||
end
|
||||
defp parse_microsecond(_rest, _digits, 0), do: :error
|
||||
|
||||
defp parse_microsecond(<<head, tail::binary>>, 6, acc) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, 6, acc)
|
||||
|
||||
defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
|
||||
do: parse_microsecond(tail, precision + 1, [head | acc])
|
||||
|
||||
defp parse_microsecond(rest, precision, acc) do
|
||||
{:lists.reverse(acc), precision, rest}
|
||||
defp parse_microsecond(rest, digits, precision) do
|
||||
scale = scale_factor(precision)
|
||||
microsecond = :erlang.binary_to_integer(:binary.part(digits, 0, precision)) * scale
|
||||
{{microsecond, precision}, rest}
|
||||
end
|
||||
|
||||
defp parse_offset(""), do: {nil, ""}
|
||||
defp parse_offset("Z"), do: {0, ""}
|
||||
defp parse_offset("-00:00"), do: :error
|
||||
|
||||
defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>),
|
||||
do: parse_offset(1, h1, h2, m1, m2, rest)
|
||||
@@ -2084,7 +2089,8 @@ defmodule Calendar.ISO do
|
||||
true <- m1 in ?0..?5 and m2 in ?0..?9,
|
||||
hour = (h1 - ?0) * 10 + h2 - ?0,
|
||||
min = (m1 - ?0) * 10 + m2 - ?0,
|
||||
true <- hour < 24 do
|
||||
true <- hour < 24,
|
||||
true <- sign == 1 or hour != 0 or min != 0 do
|
||||
{(hour * 60 + min) * 60 * sign, rest}
|
||||
else
|
||||
_ -> :error
|
||||
@@ -2100,10 +2106,43 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def iso_days_to_unit({days, {parts, ppd}}, unit) do
|
||||
day_microseconds = days * @parts_per_day
|
||||
microseconds = divide_by_parts_per_day(parts, ppd)
|
||||
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
|
||||
def iso_days_to_unit(iso_days, :second) do
|
||||
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), @microseconds_per_second)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, :millisecond) do
|
||||
floor_div_positive_divisor(iso_days_to_microseconds(iso_days), 1_000)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, :microsecond) do
|
||||
iso_days_to_microseconds(iso_days)
|
||||
end
|
||||
|
||||
def iso_days_to_unit(iso_days, unit) do
|
||||
System.convert_time_unit(iso_days_to_microseconds(iso_days), :microsecond, unit)
|
||||
end
|
||||
|
||||
defp iso_days_to_microseconds({days, {parts, ppd}}) do
|
||||
days * @parts_per_day + divide_by_parts_per_day(parts, ppd)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_time_unit_to_iso_days(iso_days, add, :second) do
|
||||
add_day_fraction_to_iso_days(iso_days, add * @microseconds_per_second, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, :millisecond) do
|
||||
add_day_fraction_to_iso_days(iso_days, add * 1_000, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, :microsecond) do
|
||||
add_day_fraction_to_iso_days(iso_days, add, @parts_per_day)
|
||||
end
|
||||
|
||||
def add_time_unit_to_iso_days(iso_days, add, unit)
|
||||
when unit == :nanosecond or is_integer(unit) do
|
||||
ppd = System.convert_time_unit(@seconds_per_day, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, add, ppd)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -164,7 +164,7 @@ defmodule NaiveDateTime do
|
||||
Returns the "local time" for the machine the Elixir program is running on.
|
||||
|
||||
WARNING: This function can cause insidious bugs. It depends on the time zone
|
||||
configuration at run time. This can changed and be set to a time zone that has
|
||||
configuration at run time. This can change and be set to a time zone that has
|
||||
daylight saving jumps (spring forward or fall back).
|
||||
|
||||
This function can be used to display what the time is right now for the time
|
||||
@@ -541,15 +541,15 @@ defmodule NaiveDateTime do
|
||||
def diff(naive_datetime1, naive_datetime2, unit \\ :second)
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :day) do
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000)
|
||||
end
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :hour) do
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000)
|
||||
end
|
||||
|
||||
def diff(naive_datetime1, naive_datetime2, :minute) do
|
||||
diff(naive_datetime1, naive_datetime2, :second) |> div(60)
|
||||
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000)
|
||||
end
|
||||
|
||||
def diff(
|
||||
@@ -570,9 +570,11 @@ defmodule NaiveDateTime do
|
||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
|
||||
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units1 - units2
|
||||
diff_microsecond =
|
||||
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) -
|
||||
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond))
|
||||
|
||||
System.convert_time_unit(diff_microsecond, :microsecond, unit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -672,7 +674,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec truncate(t(), :microsecond | :millisecond | :second) :: t()
|
||||
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t()
|
||||
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
|
||||
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
|
||||
end
|
||||
@@ -715,16 +717,18 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@spec to_date(Calendar.naive_datetime()) :: Date.t()
|
||||
def to_date(%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
calendar: calendar,
|
||||
hour: _,
|
||||
minute: _,
|
||||
second: _,
|
||||
microsecond: _
|
||||
}) do
|
||||
def to_date(
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
calendar: calendar,
|
||||
hour: _,
|
||||
minute: _,
|
||||
second: _,
|
||||
microsecond: _
|
||||
} = _naive_datetime
|
||||
) do
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
@@ -741,16 +745,18 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@spec to_time(Calendar.naive_datetime()) :: Time.t()
|
||||
def to_time(%{
|
||||
year: _,
|
||||
month: _,
|
||||
day: _,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}) do
|
||||
def to_time(
|
||||
%{
|
||||
year: _,
|
||||
month: _,
|
||||
day: _,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
} = _naive_datetime
|
||||
) do
|
||||
%Time{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
@@ -1146,16 +1152,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,
|
||||
|
||||
@@ -62,7 +62,7 @@ defmodule Time do
|
||||
|
||||
You can pass a time unit to automatically truncate the resulting time.
|
||||
|
||||
The default unit if none gets passed is `:native` which results on a default resolution of microseconds.
|
||||
The default unit if none gets passed is `:native` which results in a default resolution of microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -146,8 +146,9 @@ defmodule Time do
|
||||
iex> Time.new(23, 59, 59, 1_000_000)
|
||||
{:error, :invalid_time}
|
||||
|
||||
# Invalid precision
|
||||
Time.new(23, 59, 59, {999_999, 10})
|
||||
Invalid precision:
|
||||
|
||||
iex> Time.new(23, 59, 59, {999_999, 10})
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@@ -466,8 +467,12 @@ defmodule Time do
|
||||
Calendar.microsecond(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
when is_integer(seconds) do
|
||||
def from_seconds_after_midnight(
|
||||
seconds,
|
||||
{microsecond, precision} \\ {0, 0},
|
||||
calendar \\ Calendar.ISO
|
||||
)
|
||||
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
|
||||
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
||||
|
||||
{hour, minute, second, {_, _}} =
|
||||
@@ -478,7 +483,7 @@ defmodule Time do
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
microsecond: {microsecond, precision}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -496,7 +501,7 @@ defmodule Time do
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()}
|
||||
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
||||
iso_days = {0, to_day_fraction(time)}
|
||||
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
||||
@@ -689,7 +694,7 @@ defmodule Time do
|
||||
@doc """
|
||||
Compares two time structs.
|
||||
|
||||
Returns `:gt` if first time is later than the second
|
||||
Returns `:gt` if the first time is later than the second
|
||||
and `:lt` for vice versa. If the two times are equal
|
||||
`:eq` is returned.
|
||||
|
||||
@@ -715,14 +720,32 @@ defmodule Time do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
|
||||
def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
|
||||
%{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
|
||||
%{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
|
||||
|
||||
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
_ -> :eq
|
||||
def compare(
|
||||
%{
|
||||
hour: hour1,
|
||||
minute: minute1,
|
||||
second: second1,
|
||||
microsecond: {microsecond1, _},
|
||||
calendar: calendar
|
||||
},
|
||||
%{
|
||||
hour: hour2,
|
||||
minute: minute2,
|
||||
second: second2,
|
||||
microsecond: {microsecond2, _},
|
||||
calendar: calendar
|
||||
}
|
||||
) do
|
||||
cond do
|
||||
hour1 > hour2 -> :gt
|
||||
hour1 < hour2 -> :lt
|
||||
minute1 > minute2 -> :gt
|
||||
minute1 < minute2 -> :lt
|
||||
second1 > second2 -> :gt
|
||||
second1 < second2 -> :lt
|
||||
microsecond1 > microsecond2 -> :gt
|
||||
microsecond1 < microsecond2 -> :lt
|
||||
true -> :eq
|
||||
end
|
||||
end
|
||||
|
||||
@@ -901,11 +924,11 @@ defmodule Time do
|
||||
def diff(time1, time2, unit \\ :second)
|
||||
|
||||
def diff(time1, time2, :hour) do
|
||||
diff(time1, time2, :second) |> div(3600)
|
||||
diff(time1, time2, :microsecond) |> div(3_600_000_000)
|
||||
end
|
||||
|
||||
def diff(time1, time2, :minute) do
|
||||
diff(time1, time2, :second) |> div(60)
|
||||
diff(time1, time2, :microsecond) |> div(60_000_000)
|
||||
end
|
||||
|
||||
def diff(
|
||||
|
||||
+160
-89
@@ -50,7 +50,7 @@ defmodule Code do
|
||||
|
||||
You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and
|
||||
`ensure_loaded!/1`) to check if a module is loaded before using it and
|
||||
act.
|
||||
act accordingly.
|
||||
|
||||
## `ensure_compiled/1` and `ensure_compiled!/1`
|
||||
|
||||
@@ -258,6 +258,7 @@ defmodule Code do
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:force_do_end_blocks, boolean()}
|
||||
| {:migrate, boolean()}
|
||||
| {:migrate_atom_interpolations, boolean()}
|
||||
| {:migrate_bitstring_modifiers, boolean()}
|
||||
| {:migrate_call_parens_on_pipe, boolean()}
|
||||
| {:migrate_charlists_as_sigils, boolean()}
|
||||
@@ -272,6 +273,7 @@ defmodule Code do
|
||||
| {:escape, boolean()}
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:comments, [term()]}
|
||||
| {:syntax_colors, [{Inspect.Opts.color_key(), IO.ANSI.ansidata()}]}
|
||||
|
||||
@typedoc """
|
||||
Options for parsing functions that convert strings to quoted expressions.
|
||||
@@ -285,20 +287,26 @@ defmodule Code do
|
||||
unescape: boolean(),
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
static_atoms_encoder: (atom() -> term()),
|
||||
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
|
||||
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
|
||||
emit_warnings: boolean()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
|
||||
Options for evaluation environment, accepted by `env_for_eval/1`.
|
||||
"""
|
||||
@type env_eval_opts :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
module: module(),
|
||||
prune_binding: boolean()
|
||||
]
|
||||
@type env_eval_opt ::
|
||||
{:file, binary()}
|
||||
| {:line, pos_integer()}
|
||||
| {:module, module()}
|
||||
|
||||
@typedoc """
|
||||
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
|
||||
and `eval_quoted_with_env/4`.
|
||||
"""
|
||||
@type eval_opt ::
|
||||
{:prune_binding, boolean()}
|
||||
| {:dbg_callback, {module(), atom(), list()}}
|
||||
|
||||
@boolean_compiler_options [
|
||||
:docs,
|
||||
@@ -308,11 +316,16 @@ defmodule Code do
|
||||
:relative_paths
|
||||
]
|
||||
|
||||
@list_compiler_options [:tracers, :parser_options]
|
||||
@list_compiler_options [:tracers, :parser_options, :erlc_options]
|
||||
|
||||
@available_compiler_options @boolean_compiler_options ++
|
||||
@list_compiler_options ++
|
||||
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
|
||||
[
|
||||
:on_undefined_variable,
|
||||
:infer_signatures,
|
||||
:no_warn_undefined,
|
||||
:module_definition
|
||||
]
|
||||
|
||||
@doc """
|
||||
Lists all required files.
|
||||
@@ -402,7 +415,7 @@ defmodule Code do
|
||||
operations.
|
||||
|
||||
"""
|
||||
@spec append_path(Path.t(), cache: boolean()) :: true | false
|
||||
@spec append_path(Path.t(), cache: boolean()) :: boolean()
|
||||
def append_path(path, opts \\ []) do
|
||||
apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true
|
||||
end
|
||||
@@ -541,8 +554,7 @@ defmodule Code do
|
||||
This is the list of directories the Erlang VM uses for finding
|
||||
module code. The list of files is managed per Erlang VM node.
|
||||
|
||||
The path is expanded with `Path.expand/1` before being deleted. If the
|
||||
path does not exist, this function returns `false`.
|
||||
All paths are expanded with `Path.expand/1` before being deleted.
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec delete_paths([Path.t()]) :: :ok
|
||||
@@ -568,9 +580,11 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
It accepts the same options as `env_for_eval/1`. Additionally, you may
|
||||
also pass an environment as second argument, so the evaluation happens
|
||||
within that environment.
|
||||
It accepts the same options as both `env_for_eval/1` and
|
||||
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
|
||||
as third argument, so the evaluation happens within that environment.
|
||||
|
||||
## Return
|
||||
|
||||
Returns a tuple of the form `{value, binding}`, where `value` is the value
|
||||
returned from evaluating `string`. If an error occurs while evaluating
|
||||
@@ -600,7 +614,7 @@ defmodule Code do
|
||||
iex> Enum.sort(binding)
|
||||
[a: 3, b: 2]
|
||||
|
||||
For convenience, you can pass `__ENV__/0` as the `opts` argument and
|
||||
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
|
||||
all imports, requires and aliases defined in the current environment
|
||||
will be automatically carried over:
|
||||
|
||||
@@ -612,21 +626,28 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_string(string, binding \\ [], opts \\ [])
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
|
||||
{term, binding}
|
||||
def eval_string(string, binding \\ [], opts_or_env \\ [])
|
||||
|
||||
def eval_string(string, binding, %Macro.Env{} = env) do
|
||||
validated_eval_string(string, binding, env)
|
||||
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
|
||||
end
|
||||
|
||||
def eval_string(string, binding, opts) when is_list(opts) do
|
||||
validated_eval_string(string, binding, opts)
|
||||
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
|
||||
end
|
||||
|
||||
defp validated_eval_string(string, binding, opts_or_env) do
|
||||
%{line: line, file: file} = env = env_for_eval(opts_or_env)
|
||||
defp validate_binding(binding) when is_list(binding), do: binding
|
||||
|
||||
defp validate_binding(binding) do
|
||||
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
|
||||
end
|
||||
|
||||
defp validated_eval_string(string, binding, env, opts) do
|
||||
%{line: line, file: file} = env
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
|
||||
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
|
||||
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -747,6 +768,12 @@ defmodule Code do
|
||||
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
|
||||
to `true` by default. Defaults to `false`.
|
||||
|
||||
* `:migrate_atom_interpolations` (since v1.21.0) - when `true`, rewrites
|
||||
deprecated atom interpolations to explicit calls to `String.to_unsafe_atom/1`.
|
||||
For example, `:"foo_#{bar}"` becomes `String.to_unsafe_atom("foo_#{bar}")`.
|
||||
Interpolated keywords like `["foo_#{bar}": 1]` are **not** migrated.
|
||||
Defaults to the value of the `:migrate` option. This option changes the AST.
|
||||
|
||||
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
|
||||
removes unnecessary parentheses in known bitstring
|
||||
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
|
||||
@@ -1129,7 +1156,8 @@ defmodule Code do
|
||||
returned quoted expressions (instead of evaluated).
|
||||
|
||||
See `eval_string/3` for a description of arguments and return types.
|
||||
The options are described under `env_for_eval/1`.
|
||||
It accepts the same options as both `env_for_eval/1` and
|
||||
`eval_quoted_with_env/4`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1151,11 +1179,20 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
|
||||
{value, binding, _env} =
|
||||
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
|
||||
{term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])
|
||||
|
||||
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
|
||||
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
|
||||
end
|
||||
|
||||
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
||||
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
|
||||
end
|
||||
|
||||
defp eval_quoted(quoted, binding, env, opts) do
|
||||
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -1183,14 +1220,9 @@ defmodule Code do
|
||||
|
||||
* `:module` - the module to run the environment on
|
||||
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
|
||||
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
|
||||
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
|
||||
|
||||
@doc """
|
||||
@@ -1204,11 +1236,19 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
It accepts the same options as `env_for_eval/1`.
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
|
||||
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
|
||||
used in the evaluated code. It must be a `{module, function, args}`
|
||||
tuple, see `dbg/2` for more details.
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
|
||||
{term, binding, Macro.Env.t()}
|
||||
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
|
||||
when is_list(binding) do
|
||||
@@ -1258,9 +1298,9 @@ defmodule Code do
|
||||
* `:literal_encoder` (since v1.10.0) - how to encode literals in the AST.
|
||||
It must be a function that receives two arguments, the literal and its
|
||||
metadata, and it must return `{:ok, ast :: Macro.t}` or
|
||||
`{:error, reason :: binary}`. If you return anything than the literal
|
||||
`{:error, reason :: binary}`. If you return anything other than the literal
|
||||
itself as the `term`, then the AST is no longer valid. This option
|
||||
may still useful for textual analysis of the source code.
|
||||
may still be useful for textual analysis of the source code.
|
||||
|
||||
* `:static_atoms_encoder` - the static atom encoder function, see
|
||||
"The `:static_atoms_encoder` function" section below. Note this
|
||||
@@ -1286,7 +1326,7 @@ defmodule Code do
|
||||
and keyword lists.
|
||||
|
||||
The encoder function will receive the atom name (as a binary) and a
|
||||
keyword list with the current file, line and column. It must return
|
||||
keyword list with the current line and column. It must return
|
||||
`{:ok, token :: term} | {:error, reason :: binary}`.
|
||||
|
||||
The encoder function is supposed to create an atom from the given
|
||||
@@ -1325,14 +1365,7 @@ defmodule Code do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
|
||||
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
|
||||
{:ok, tokens} ->
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
{:error, _error_msg} = error ->
|
||||
error
|
||||
end
|
||||
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1361,12 +1394,15 @@ defmodule Code do
|
||||
while preserving information like comments and literals position.
|
||||
|
||||
Returns `{:ok, quoted_form, comments}` if it succeeds,
|
||||
`{:error, {line, error, token}}` otherwise.
|
||||
`{:error, {location, error, token}}` otherwise, where `location`
|
||||
is keyword metadata containing the line and column of the error.
|
||||
|
||||
Comments are maps with the following fields:
|
||||
|
||||
* `:line` - The line number of the source code
|
||||
|
||||
* `:column` - The column number of the source code
|
||||
|
||||
* `:text` - The full text of the comment, including the leading `#`
|
||||
|
||||
* `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment
|
||||
@@ -1408,8 +1444,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
|
||||
@@ -1422,7 +1457,9 @@ defmodule Code do
|
||||
|
||||
Returns the AST and a list of comments if it succeeds, raises an exception
|
||||
otherwise. The exception is a `TokenMissingError` in case a token is missing
|
||||
(usually because the expression is incomplete), `SyntaxError` otherwise.
|
||||
(usually because the expression is incomplete), `MismatchedDelimiterError`
|
||||
(in case of mismatched opening and closing delimiters) and `SyntaxError`
|
||||
otherwise.
|
||||
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
@@ -1525,9 +1562,13 @@ defmodule Code do
|
||||
`string_to_quoted/2`, setting this option to `false` will prevent it from
|
||||
escaping the sequences twice. Defaults to `true`.
|
||||
|
||||
* `:syntax_colors` - a keyword list of colors the output is colorized.
|
||||
See `Inspect.Opts` for more information.
|
||||
|
||||
See `format_string!/2` for the full list of formatting options including
|
||||
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
|
||||
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
|
||||
`:file`, `:line`, `:locals_without_parens`, `:force_do_end_blocks`, and all
|
||||
migration options like `:migrate_charlists_as_sigils`. Note `:line_length`
|
||||
does not apply here.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
|
||||
@@ -1609,13 +1650,19 @@ defmodule Code do
|
||||
nil
|
||||
|
||||
:proceed ->
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
try do
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
catch
|
||||
kind, reason ->
|
||||
:elixir_code_server.call({:release, file})
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1641,7 +1688,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores all given compilation options.
|
||||
|
||||
Changing the compilation options affect all processes
|
||||
Changing the compilation options affects all processes
|
||||
running in a given Erlang VM node. To store individual
|
||||
options and for a description of all options, see
|
||||
`put_compiler_option/2`.
|
||||
@@ -1705,14 +1752,11 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores a compilation option.
|
||||
|
||||
Changing the compilation options affect all processes running in a
|
||||
Changing the compilation options affects all processes running in a
|
||||
given Erlang VM node.
|
||||
|
||||
Available options are:
|
||||
|
||||
* `: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`.
|
||||
@@ -1724,10 +1768,17 @@ defmodule Code do
|
||||
remove the `:debug_info` while deploying, tools like `mix release`
|
||||
already do such by default.
|
||||
|
||||
Other environments, such as `mix test`, automatically disables this
|
||||
Other environments, such as `mix test`, automatically disable this
|
||||
via the `:test_elixirc_options` project configuration, as there is
|
||||
typically no need to store debug chunks for test files.
|
||||
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
|
||||
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
|
||||
debug metadata for BEAM debuggers. Defaults to `[]`.
|
||||
|
||||
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
|
||||
when a protocol has already been consolidated and a new implementation is added.
|
||||
Defaults to `false`.
|
||||
@@ -1735,28 +1786,41 @@ defmodule Code do
|
||||
* `:ignore_module_conflict` - when `true`, does not warn when a module has
|
||||
already been defined. Defaults to `false`.
|
||||
|
||||
* `:infer_signatures` (since v1.18.0) - a list of applications of which modules
|
||||
should be using during type inference. When `false`, it disables module-local
|
||||
* `:infer_signatures` (since v1.18.0) - a list of applications whose modules
|
||||
should be used during type inference. When `false`, it disables module-local
|
||||
signature inference used when type checking remote calls to the compiled
|
||||
module. Type checking will be executed regardless of the value of this option.
|
||||
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
|
||||
signatures for test files). Outside of Mix projects, it defaults to `[:elixir]`.
|
||||
|
||||
When setting this option, we recommend running `mix clean` so the modules can be
|
||||
recompiled with the new behaviour. `mix test` automatically disables this option
|
||||
via the `:test_elixirc_options` project configuration, as there is typically no
|
||||
need to infer signatures for test files.
|
||||
* `:module_definition` (since v1.20.0) - stores if the module definition should
|
||||
be `:compiled` (the default) or `:interpreted`. Note this does not affect the
|
||||
`.beam` file written to disk, only how the contents inside `defmodule` are
|
||||
executed. Using the `:interpreted` mode may offer better compilation times for
|
||||
large projects, especially on machines with high core count, however, it comes
|
||||
with some downsides:
|
||||
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
* Errors during compilation may have less precise stacktraces
|
||||
|
||||
* Anonymous functions within `defmodule` can have only up to 20 arguments.
|
||||
If this is an issue, you can use maps or tuples to group the data.
|
||||
Note the functions themselves inside `defmodule`, such as the ones defined
|
||||
inside `def` and friends, can still have up to 255 arguments
|
||||
|
||||
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
|
||||
tuples that will not emit warnings that the module or function does not exist
|
||||
at compilation time. Pass atom `:all` to skip warning for all undefined
|
||||
functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
|
||||
|
||||
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
|
||||
compilation. See the module docs for more information. Defaults to `[]`.
|
||||
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
|
||||
When `:raise` (the default), undefined variables will trigger a compilation
|
||||
error. You may set it to `:warn` if you want undefined variables to
|
||||
emit a warning and expand as to a local call to the zero-arity function
|
||||
of the same name (for example, `node` would be expanded as `node()`).
|
||||
This `:warn` behavior only exists for compatibility reasons when working
|
||||
with old dependencies, its usage is discouraged and it will be removed
|
||||
in future releases.
|
||||
|
||||
* `:parser_options` (since v1.10.0) - a keyword list of options to be given
|
||||
to the parser when compiling files. It accepts the same options as
|
||||
@@ -1767,14 +1831,12 @@ defmodule Code do
|
||||
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
|
||||
latter is used for other purposes beyond compilation.
|
||||
|
||||
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
|
||||
When `:raise` (the default), undefined variables will trigger a compilation
|
||||
error. You may be set it to `:warn` if you want undefined variables to
|
||||
emit a warning and expand as to a local call to the zero-arity function
|
||||
of the same name (for example, `node` would be expanded as `node()`).
|
||||
This `:warn` behavior only exists for compatibility reasons when working
|
||||
with old dependencies, its usage is discouraged and it will be removed
|
||||
in future releases.
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
|
||||
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
|
||||
compilation. See the module docs for more information. Defaults to `[]`.
|
||||
|
||||
It always returns `:ok`. Raises an error for invalid options.
|
||||
|
||||
@@ -1814,6 +1876,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
|
||||
@@ -2079,7 +2150,7 @@ defmodule Code do
|
||||
If the module being checked is currently in a compiler deadlock,
|
||||
this function returns `{:error, :unavailable}`. Unavailable doesn't
|
||||
necessarily mean the module doesn't exist, just that it is not currently
|
||||
available, but it (or may not) become available in the future.
|
||||
available, but it may (or may not) become available in the future.
|
||||
|
||||
Therefore, if you can only continue if the module is available, use
|
||||
`ensure_compiled!/1` instead. In particular, do not do this:
|
||||
|
||||
@@ -195,6 +195,7 @@ defmodule Code.Formatter do
|
||||
file = Keyword.get(opts, :file, nil)
|
||||
sigils = Keyword.get(opts, :sigils, [])
|
||||
migrate = Keyword.get(opts, :migrate, false)
|
||||
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate)
|
||||
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
|
||||
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
|
||||
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
|
||||
@@ -223,6 +224,7 @@ defmodule Code.Formatter do
|
||||
comments: comments,
|
||||
sigils: sigils,
|
||||
file: file,
|
||||
migrate_atom_interpolations: migrate_atom_interpolations,
|
||||
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
|
||||
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
|
||||
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
|
||||
@@ -334,14 +336,20 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra(
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} =
|
||||
quoted,
|
||||
context,
|
||||
state
|
||||
) do
|
||||
if interpolated?(entries) do
|
||||
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
||||
else
|
||||
remote_to_algebra(quoted, context, state)
|
||||
cond do
|
||||
not interpolated?(entries) ->
|
||||
remote_to_algebra(quoted, context, state)
|
||||
|
||||
state.migrate_atom_interpolations ->
|
||||
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
|
||||
|
||||
true ->
|
||||
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1457,7 +1465,7 @@ defmodule Code.Formatter do
|
||||
metadata = [
|
||||
file: state.file,
|
||||
line: meta[:line],
|
||||
sigil: String.to_atom(name),
|
||||
sigil: String.to_unsafe_atom(name),
|
||||
modifiers: modifiers,
|
||||
opening_delimiter: opening_delimiter
|
||||
]
|
||||
@@ -1696,7 +1704,7 @@ defmodule Code.Formatter do
|
||||
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
|
||||
end
|
||||
|
||||
defp integer_to_algebra(text, inspect_otps) do
|
||||
defp integer_to_algebra(text, inspect_opts) do
|
||||
case text do
|
||||
<<?0, ?x, rest::binary>> ->
|
||||
"0x" <> String.upcase(rest)
|
||||
@@ -1710,15 +1718,15 @@ defmodule Code.Formatter do
|
||||
decimal ->
|
||||
insert_underscores(decimal)
|
||||
end
|
||||
|> color_doc(:number, inspect_otps)
|
||||
|> color_doc(:number, inspect_opts)
|
||||
end
|
||||
|
||||
defp float_to_algebra(text, inspect_otps) do
|
||||
defp float_to_algebra(text, inspect_opts) do
|
||||
[int_part, decimal_part] = :binary.split(text, ".")
|
||||
decimal_part = String.downcase(decimal_part)
|
||||
|
||||
string = insert_underscores(int_part) <> "." <> decimal_part
|
||||
color_doc(string, :number, inspect_otps)
|
||||
color_doc(string, :number, inspect_opts)
|
||||
end
|
||||
|
||||
defp insert_underscores("-" <> digits) do
|
||||
|
||||
@@ -27,7 +27,8 @@ defmodule Code.Fragment do
|
||||
column: pos_integer(),
|
||||
columns: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
|
||||
preserve_sigils: boolean(),
|
||||
trailing_fragment: String.t()
|
||||
]
|
||||
|
||||
@@ -35,7 +36,7 @@ defmodule Code.Fragment do
|
||||
Returns the list of lines in the given string, preserving their line endings.
|
||||
|
||||
Only the line endings recognized by the Elixir compiler are
|
||||
considered, namely `\r\n` and `\n`. If you would like the retrieve
|
||||
considered, namely `\r\n` and `\n`. If you would like to retrieve
|
||||
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
|
||||
|
||||
## Examples
|
||||
@@ -51,6 +52,7 @@ defmodule Code.Fragment do
|
||||
|
||||
"""
|
||||
@doc since: "1.19.0"
|
||||
@spec lines(String.t()) :: [String.t()]
|
||||
def lines(string) do
|
||||
lines(string, <<>>)
|
||||
end
|
||||
@@ -140,6 +142,9 @@ defmodule Code.Fragment do
|
||||
* `{:anonymous_call, inside_caller}` - the context is an anonymous
|
||||
call, such as `fun.(` and `@fun.(`.
|
||||
|
||||
* `{:capture_arg, charlist}` - the context is a capture argument,
|
||||
such as `&1`
|
||||
|
||||
* `{:module_attribute, charlist}` - the context is a module attribute,
|
||||
such as `@hello_wor`
|
||||
|
||||
@@ -157,8 +162,8 @@ defmodule Code.Fragment do
|
||||
* `:none` - no context possible
|
||||
|
||||
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
|
||||
of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
|
||||
`~>` and `~>>`
|
||||
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as
|
||||
`~>` and `~>>`, are returned as :operator contexts
|
||||
|
||||
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
|
||||
`inside_struct` can either be a `charlist` in case of a static alias or an
|
||||
@@ -204,6 +209,7 @@ defmodule Code.Fragment do
|
||||
| {:local_arity, charlist}
|
||||
| {:local_call, charlist}
|
||||
| {:anonymous_call, inside_caller}
|
||||
| {:capture_arg, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| {:operator, charlist}
|
||||
| {:operator_arity, charlist}
|
||||
@@ -656,7 +662,7 @@ defmodule Code.Fragment do
|
||||
iex> Code.Fragment.surround_context("foo", {1, 1})
|
||||
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
|
||||
|
||||
## Differences to `cursor_context/2`
|
||||
## Differences from `cursor_context/2`
|
||||
|
||||
Because `surround_context/3` attempts to capture complex expressions,
|
||||
it has some differences to `cursor_context/2`:
|
||||
@@ -670,7 +676,7 @@ defmodule Code.Fragment do
|
||||
be a local or variable
|
||||
|
||||
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
|
||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
|
||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`)
|
||||
|
||||
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
|
||||
`{:struct, ~c""}` as context
|
||||
@@ -1234,14 +1240,43 @@ defmodule Code.Fragment do
|
||||
the cursor. This is necessary to correctly complete anonymous functions
|
||||
and the left-hand side of `->`
|
||||
|
||||
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
|
||||
(see "Tracking sigils" section below)
|
||||
|
||||
## Tracking sigils
|
||||
|
||||
The `:preserve_sigils` option can be used to track cursor positions inside
|
||||
a sigil.
|
||||
|
||||
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
|
||||
as the second argument:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
|
||||
{:ok,
|
||||
{:sigil_r, [delimiter: "/", line: 1],
|
||||
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
|
||||
|
||||
In case the sigil is completed and has zero or more modifiers, the cursor will
|
||||
be nested in the list, with all previous delimiters specified:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
|
||||
{:ok,
|
||||
{:sigil_r, [delimiter: "/", line: 1],
|
||||
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
|
||||
|
||||
If the cursor is after the sigil, then it is discarded as everything else:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
|
||||
{:ok, {:__cursor__, [line: 1], []}}
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
||||
def container_cursor_to_quoted(fragment, opts \\ []) do
|
||||
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
|
||||
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
|
||||
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
|
||||
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
|
||||
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
|
||||
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
@@ -1261,7 +1296,10 @@ defmodule Code.Fragment do
|
||||
end
|
||||
|
||||
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
{:ok, forms}
|
||||
end
|
||||
|
||||
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
|
||||
tokens =
|
||||
@@ -1269,7 +1307,7 @@ defmodule Code.Fragment do
|
||||
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
|
||||
true <- maybe_missing_stab?(rev_tokens, true),
|
||||
opts =
|
||||
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
|
||||
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
|
||||
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
|
||||
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
|
||||
trailing_tokens =
|
||||
@@ -1288,10 +1326,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
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule Code.Identifier do
|
||||
with "-" <> rest <- Atom.to_string(atom),
|
||||
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
|
||||
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
@@ -150,20 +150,20 @@ defmodule Code.Identifier do
|
||||
<<acc::binary, char::utf8>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x100 do
|
||||
defp escape_char(char, acc) when char < 0x80 do
|
||||
<<a::4, b::4>> = <<char::8>>
|
||||
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x10000 do
|
||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
|
||||
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc) when char < 0x1000000 do
|
||||
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
||||
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
||||
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
||||
?}>>
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
defmodule Code.Normalizer do
|
||||
@moduledoc false
|
||||
|
||||
@do_end_keywords [:rescue, :catch, :else, :after]
|
||||
|
||||
defguard is_literal(x)
|
||||
when is_integer(x) or
|
||||
is_float(x) or
|
||||
@@ -68,7 +70,7 @@ defmodule Code.Normalizer do
|
||||
|
||||
# Bit containers
|
||||
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
|
||||
normalize_bitstring(quoted, state)
|
||||
normalize_bitstring(quoted, state, state.escape)
|
||||
end
|
||||
|
||||
# Atoms with interpolations
|
||||
@@ -89,13 +91,7 @@ defmodule Code.Normalizer do
|
||||
normalize_literal(:utf8, [], state)
|
||||
end
|
||||
|
||||
string =
|
||||
if state.escape do
|
||||
normalize_bitstring(string, state, true)
|
||||
else
|
||||
normalize_bitstring(string, state)
|
||||
end
|
||||
|
||||
string = normalize_bitstring(string, state, state.escape)
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
||||
end
|
||||
|
||||
@@ -118,6 +114,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)
|
||||
@@ -187,7 +184,8 @@ defmodule Code.Normalizer do
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
string = normalize_bitstring(string, %{state | parent_meta: meta}, false)
|
||||
{sigil, meta, [string, modifiers]}
|
||||
else
|
||||
_ ->
|
||||
normalize_call(quoted, state)
|
||||
@@ -269,7 +267,7 @@ defmodule Code.Normalizer do
|
||||
"Elixir." <> segments ->
|
||||
segments
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
|> Enum.map(&String.to_unsafe_atom/1)
|
||||
end
|
||||
|
||||
{:__aliases__, meta, segments}
|
||||
@@ -351,18 +349,20 @@ defmodule Code.Normalizer do
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
|
||||
Keyword.has_key?(meta, :do) ->
|
||||
Keyword.has_key?(meta, :do) and kw_blocks?(last) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
|
||||
match?([{:do, _} | _], last) and kw_blocks?(last) ->
|
||||
# Non normalized kw blocks
|
||||
line = state.parent_meta[:line] || meta[:line]
|
||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
true ->
|
||||
# The formatter renders do-end blocks from the meta alone
|
||||
meta = Keyword.drop(meta, [:do, :end])
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
||||
|
||||
@@ -401,11 +401,22 @@ defmodule Code.Normalizer do
|
||||
defp block_keyword?([]), do: true
|
||||
defp block_keyword?(_), do: false
|
||||
|
||||
# Anything after the do block that is not a block keyword makes it a keyword list
|
||||
defp kw_blocks?([{:do, _} | rest] = kw) do
|
||||
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
|
||||
end
|
||||
|
||||
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
|
||||
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
|
||||
end
|
||||
|
||||
defp kw_blocks?(_), do: false
|
||||
|
||||
defp allow_keyword?(:when, 2), do: true
|
||||
defp allow_keyword?(:{}, _), do: false
|
||||
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
|
||||
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
|
||||
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
parts =
|
||||
@@ -424,9 +435,21 @@ defmodule Code.Normalizer do
|
||||
end)
|
||||
end
|
||||
|
||||
parts = maybe_add_trailing_newline(meta, parts, state)
|
||||
{:<<>>, 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,
|
||||
@@ -552,7 +575,7 @@ defmodule Code.Normalizer do
|
||||
atom
|
||||
|> Atom.to_string()
|
||||
|> maybe_escape_literal(state)
|
||||
|> String.to_atom()
|
||||
|> String.to_unsafe_atom()
|
||||
end
|
||||
|
||||
defp maybe_escape_literal(term, _) do
|
||||
|
||||
@@ -80,7 +80,8 @@ defmodule Code.Typespec do
|
||||
Returns all types available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is the type (`:typep`, `:type` and `:opaque`).
|
||||
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+,
|
||||
`:nominal`).
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
@@ -95,9 +96,10 @@ defmodule Code.Typespec do
|
||||
|
||||
types =
|
||||
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
|
||||
kind in [:opaque, :type] do
|
||||
kind in [:opaque, :type, :nominal] do
|
||||
cond do
|
||||
kind == :opaque -> {:opaque, type}
|
||||
kind == :nominal -> {:nominal, type}
|
||||
{name, length(args)} in exported_types -> {:type, type}
|
||||
true -> {:typep, type}
|
||||
end
|
||||
@@ -117,7 +119,7 @@ defmodule Code.Typespec do
|
||||
element is spec name and arity and the second is the spec.
|
||||
|
||||
The module must have a corresponding BEAM file which can be
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
located by the runtime system. The specs will be in the Erlang
|
||||
Abstract Format.
|
||||
"""
|
||||
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
|
||||
@@ -135,10 +137,10 @@ defmodule Code.Typespec do
|
||||
Returns all callbacks available from the module's BEAM code.
|
||||
|
||||
The result is returned as a list of tuples where the first
|
||||
element is spec name and arity and the second is the spec.
|
||||
element is the callback name and arity and the second is the callback.
|
||||
|
||||
The module must have a corresponding BEAM file
|
||||
which can be located by the runtime system. The types will be
|
||||
which can be located by the runtime system. The callbacks will be
|
||||
in the Erlang Abstract Format.
|
||||
"""
|
||||
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
|
||||
@@ -191,8 +193,8 @@ defmodule Code.Typespec do
|
||||
|
||||
## To AST conversion
|
||||
|
||||
defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
|
||||
[]
|
||||
defp collect_vars({:ann_type, _anno, [_var, type]}) do
|
||||
collect_vars(type)
|
||||
end
|
||||
|
||||
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
|
||||
@@ -399,10 +401,10 @@ defmodule Code.Typespec do
|
||||
defp erl_to_ex_var(var) do
|
||||
case Atom.to_string(var) do
|
||||
<<"_", c::utf8, rest::binary>> ->
|
||||
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
|
||||
<<c::utf8, rest::binary>> ->
|
||||
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-10
@@ -152,6 +152,7 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec config(atom(), keyword()) :: keyword()
|
||||
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
|
||||
if not Keyword.keyword?(opts) do
|
||||
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
|
||||
@@ -198,6 +199,7 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec config(atom(), atom(), term()) :: keyword()
|
||||
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
|
||||
get_config!()
|
||||
|> __merge__([{root_key, [{key, opts}]}])
|
||||
@@ -225,6 +227,7 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec read_config(atom()) :: keyword() | nil
|
||||
def read_config(root_key) when is_atom(root_key) do
|
||||
get_config!()[root_key]
|
||||
end
|
||||
@@ -233,7 +236,7 @@ defmodule Config do
|
||||
Returns the environment this configuration file is executed on.
|
||||
|
||||
In Mix projects this function returns the environment this configuration
|
||||
file is executed on.
|
||||
file is executed on.
|
||||
In releases, returns the `MIX_ENV` specified when running `mix release`.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
@@ -284,8 +287,8 @@ defmodule Config do
|
||||
|
||||
In case the file doesn't exist, an error is raised.
|
||||
|
||||
If file is a relative, it will be expanded relatively to the
|
||||
directory the current configuration file is in.
|
||||
If the file is relative, it will be expanded relative to the
|
||||
directory of the current configuration file.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -377,21 +380,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
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule Config.Provider do
|
||||
For example, imagine you want to list some basic configuration
|
||||
on Mix's built-in `config/runtime.exs` file, but you also want
|
||||
to support additional configuration files. To do so, you can add
|
||||
this inside the `def project` portion of your `mix.exs`:
|
||||
this inside the `def project` portion of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule Config.Reader do
|
||||
|
||||
For example, if you expect the target system to have a config file
|
||||
in an absolute path, you can add this inside the `def project` portion
|
||||
of your `mix.exs`:
|
||||
of your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
|
||||
+109
-57
@@ -618,7 +618,7 @@ defmodule Enum do
|
||||
acc,
|
||||
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
|
||||
(acc -> {:cont, chunk, acc} | {:cont, acc})
|
||||
) :: Enumerable.t()
|
||||
) :: [chunk]
|
||||
when chunk: any
|
||||
def chunk_while(enumerable, acc, chunk_fun, after_fun) do
|
||||
{_, {res, acc}} =
|
||||
@@ -666,7 +666,7 @@ defmodule Enum do
|
||||
[1, [2], 3, 4, 5, 6]
|
||||
|
||||
"""
|
||||
@spec concat(t) :: t
|
||||
@spec concat(Enumerable.t(Enumerable.t(elem))) :: [elem] when elem: term
|
||||
def concat(enumerables)
|
||||
|
||||
def concat(list) when is_list(list) do
|
||||
@@ -681,8 +681,8 @@ defmodule Enum do
|
||||
Concatenates the enumerable on the `right` with the enumerable on the
|
||||
`left`.
|
||||
|
||||
This function produces the same result as the `++/2` operator
|
||||
for lists.
|
||||
This function behaves similarly to the `++/2` operator with proper
|
||||
lists, but applied to enumerables.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -693,7 +693,7 @@ defmodule Enum do
|
||||
[1, 2, 3, 4, 5, 6]
|
||||
|
||||
"""
|
||||
@spec concat(t, t) :: t
|
||||
@spec concat(Enumerable.t(elem), Enumerable.t(elem)) :: [elem] when elem: term
|
||||
def concat(left, right) when is_list(left) and is_list(right) do
|
||||
left ++ right
|
||||
end
|
||||
@@ -829,7 +829,7 @@ defmodule Enum do
|
||||
"""
|
||||
@spec dedup(t) :: list
|
||||
def dedup(enumerable) when is_list(enumerable) do
|
||||
dedup_list(enumerable, []) |> :lists.reverse()
|
||||
dedup_list(enumerable)
|
||||
end
|
||||
|
||||
def dedup(enumerable) do
|
||||
@@ -859,6 +859,10 @@ defmodule Enum do
|
||||
|
||||
"""
|
||||
@spec dedup_by(t, (element -> term)) :: list
|
||||
def dedup_by([head | tail], fun) do
|
||||
dedup_by_list(tail, fun, fun.(head), [head])
|
||||
end
|
||||
|
||||
def dedup_by(enumerable, fun) do
|
||||
{list, _} = reduce(enumerable, {[], []}, R.dedup(fun))
|
||||
:lists.reverse(list)
|
||||
@@ -903,7 +907,7 @@ defmodule Enum do
|
||||
|
||||
def drop(enumerable, amount) when is_integer(amount) and amount < 0 do
|
||||
{count, fun} = slice_count_and_fun(enumerable, 1)
|
||||
amount = Kernel.min(amount + count, count)
|
||||
amount = amount + count
|
||||
|
||||
if amount > 0 do
|
||||
fun.(0, amount, 1)
|
||||
@@ -1253,7 +1257,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 ->
|
||||
@@ -1442,7 +1446,7 @@ defmodule Enum do
|
||||
)
|
||||
|
||||
# Avoid warnings about Dict
|
||||
dict_module = String.to_atom("Dict")
|
||||
dict_module = String.to_unsafe_atom("Dict")
|
||||
|
||||
reduce(reverse(enumerable), dict, fn entry, categories ->
|
||||
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
|
||||
@@ -2023,11 +2027,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
|
||||
@@ -2224,14 +2233,19 @@ defmodule Enum do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module()) :: {min :: element, max :: element}
|
||||
@spec min_max(t, (-> empty_result)) :: {min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
|
||||
{element, element} | empty_result
|
||||
{min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
|
||||
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
|
||||
def min_max(list = [_ | _], empty_fallback) when is_function(empty_fallback, 0) do
|
||||
min_max_list(list)
|
||||
end
|
||||
|
||||
def min_max(first..last//step = range, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
case Range.size(range) do
|
||||
@@ -2338,14 +2352,14 @@ defmodule Enum do
|
||||
|
||||
"""
|
||||
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
|
||||
{element, element} | empty_result
|
||||
{min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
@spec min_max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module(),
|
||||
(-> empty_result)
|
||||
) :: {element, element} | empty_result
|
||||
) :: {min :: element, max :: element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max_by(
|
||||
enumerable,
|
||||
@@ -2389,6 +2403,18 @@ defmodule Enum do
|
||||
|
||||
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
|
||||
defp min_max_list([h | t]), do: min_max_list(t, h, h)
|
||||
|
||||
defp min_max_list([h | t], min, max) do
|
||||
cond do
|
||||
h < min -> min_max_list(t, h, max)
|
||||
max < h -> min_max_list(t, min, h)
|
||||
true -> min_max_list(t, min, max)
|
||||
end
|
||||
end
|
||||
|
||||
defp min_max_list([], min, max), do: {min, max}
|
||||
|
||||
@doc """
|
||||
Splits the `enumerable` in two lists according to the given function `fun`.
|
||||
|
||||
@@ -2666,7 +2692,7 @@ defmodule Enum do
|
||||
5050
|
||||
|
||||
"""
|
||||
@spec reduce_while(t, any, (element, any -> {:cont, any} | {:halt, any})) :: any
|
||||
@spec reduce_while(t, acc, (element, acc -> {:cont, acc} | {:halt, acc})) :: acc
|
||||
def reduce_while(enumerable, acc, fun) do
|
||||
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
|
||||
end
|
||||
@@ -2909,8 +2935,7 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
defp slide_list_middle(list, 0, last, start_to_middle) do
|
||||
{slid_range, tail} = slide_list_last(list, last + 1, [])
|
||||
slid_range ++ :lists.reverse(start_to_middle, tail)
|
||||
slide_list_last(list, last + 1, [], start_to_middle)
|
||||
end
|
||||
|
||||
# You asked for a middle index off the end of the list... you get what we've got
|
||||
@@ -2918,16 +2943,16 @@ defmodule Enum do
|
||||
:lists.reverse(acc)
|
||||
end
|
||||
|
||||
defp slide_list_last([h | t], last, acc) when last > 0 do
|
||||
slide_list_last(t, last - 1, [h | acc])
|
||||
defp slide_list_last([h | t], last, acc, start_to_middle) when last > 0 do
|
||||
slide_list_last(t, last - 1, [h | acc], start_to_middle)
|
||||
end
|
||||
|
||||
defp slide_list_last(rest, 0, acc) do
|
||||
{:lists.reverse(acc), rest}
|
||||
defp slide_list_last(rest, 0, acc, start_to_middle) do
|
||||
:lists.reverse(acc, :lists.reverse(start_to_middle, rest))
|
||||
end
|
||||
|
||||
defp slide_list_last([], _, acc) do
|
||||
{:lists.reverse(acc), []}
|
||||
defp slide_list_last([], _, acc, start_to_middle) do
|
||||
:lists.reverse(acc, :lists.reverse(start_to_middle))
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4193,7 +4218,7 @@ defmodule Enum do
|
||||
|
||||
`zip_with/2` can be used to transpose lists of lists:
|
||||
|
||||
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
|
||||
iex> Enum.zip_with([[1, 2], [3, 4]], & &1)
|
||||
[[1, 3], [2, 4]]
|
||||
|
||||
"""
|
||||
@@ -4304,11 +4329,24 @@ defmodule Enum do
|
||||
empty.()
|
||||
|
||||
_ ->
|
||||
last = last - rem(last - first, step)
|
||||
# The endpoint shortcut is only valid for sorters consistent with
|
||||
# the natural integer order of the range elements, which is known
|
||||
# to hold for the default sorters; any other sorter traverses the
|
||||
# elements, seeded with the first one since the range is not empty
|
||||
if fun == (&<=/2) or fun == (&>=/2) do
|
||||
last = last - rem(last - first, step)
|
||||
|
||||
case fun.(first, last) do
|
||||
true -> first
|
||||
false -> last
|
||||
case fun.(first, last) do
|
||||
true -> first
|
||||
false -> last
|
||||
end
|
||||
else
|
||||
reduce_range(first + step, last, step, first, fn element, acc ->
|
||||
case fun.(acc, element) do
|
||||
true -> acc
|
||||
false -> element
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4411,10 +4449,9 @@ defmodule Enum do
|
||||
## any?/2 all?/2
|
||||
|
||||
defp predicate_list([h | t], initial, fun) do
|
||||
if !!fun.(h) == initial do
|
||||
predicate_list(t, initial, fun)
|
||||
else
|
||||
not initial
|
||||
case !!fun.(h) do
|
||||
^initial -> predicate_list(t, initial, fun)
|
||||
_ -> not initial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4425,10 +4462,9 @@ defmodule Enum do
|
||||
defp predicate_range(first, last, step, initial, fun)
|
||||
when step > 0 and first <= last
|
||||
when step < 0 and first >= last do
|
||||
if !!fun.(first) == initial do
|
||||
predicate_range(first + step, last, step, initial, fun)
|
||||
else
|
||||
not initial
|
||||
case !!fun.(first) do
|
||||
^initial -> predicate_range(first + step, last, step, initial, fun)
|
||||
_ -> not initial
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4507,19 +4543,20 @@ defmodule Enum do
|
||||
|
||||
# dedup
|
||||
|
||||
defp dedup_list([value | tail], acc) do
|
||||
acc =
|
||||
case acc do
|
||||
[^value | _] -> acc
|
||||
_ -> [value | 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: []
|
||||
|
||||
dedup_list(tail, acc)
|
||||
## dedup_by
|
||||
|
||||
defp dedup_by_list([head | tail], fun, prev, acc) do
|
||||
case fun.(head) do
|
||||
^prev -> dedup_by_list(tail, fun, prev, acc)
|
||||
new_val -> dedup_by_list(tail, fun, new_val, [head | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp dedup_list([], acc) do
|
||||
acc
|
||||
end
|
||||
defp dedup_by_list([], _fun, _prev, acc), do: :lists.reverse(acc)
|
||||
|
||||
## drop
|
||||
|
||||
@@ -5168,7 +5205,16 @@ defimpl Enumerable, for: Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
|
||||
reduce =
|
||||
quote generated: true do
|
||||
reduce(
|
||||
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
|
||||
var!(acc),
|
||||
var!(fun)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(reduce) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
reduce(Map.put(range, :step, step), acc, fun)
|
||||
end
|
||||
@@ -5191,12 +5237,12 @@ defimpl Enumerable, for: Range do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
def member?(first..last//step, value) when is_integer(value) do
|
||||
if step > 0 do
|
||||
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
||||
else
|
||||
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
||||
end
|
||||
def member?(first..last//step, value) when is_integer(value) and step > 0 do
|
||||
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
||||
end
|
||||
|
||||
def member?(first..last//step, value) when is_integer(value) and step < 0 do
|
||||
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
@@ -5215,11 +5261,17 @@ defimpl Enumerable, for: Range do
|
||||
end
|
||||
|
||||
def slice(first.._//step = range) do
|
||||
{:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
|
||||
{:ok, Range.size(range), &slice(first + &1 * step, step * &3, &2)}
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def slice(%{__struct__: Range, first: first, last: last} = range) do
|
||||
|
||||
slice =
|
||||
quote generated: true do
|
||||
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
|
||||
end
|
||||
|
||||
def unquote(slice) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
slice(Map.put(range, :step, step))
|
||||
end
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
@@ -1046,7 +1046,7 @@ defmodule RuntimeError do
|
||||
iex> raise "oops!"
|
||||
** (RuntimeError) oops!
|
||||
|
||||
You should use this exceptions sparingly, since most of the time it might be
|
||||
You should use this exception sparingly, since most of the time it might be
|
||||
better to define your own exceptions specific to your application or library.
|
||||
Sometimes, however, there are situations in which you don't expect a condition to
|
||||
happen, but you want to give a meaningful error message if it does. In those cases,
|
||||
@@ -1137,7 +1137,7 @@ defmodule SystemLimitError do
|
||||
|
||||
For example, this can happen if you try to create an atom that is too large:
|
||||
|
||||
iex> String.to_atom(String.duplicate("a", 100_000))
|
||||
iex> String.to_unsafe_atom(String.duplicate("a", 100_000))
|
||||
** (SystemLimitError) a system limit has been reached
|
||||
"""
|
||||
|
||||
@@ -1807,7 +1807,7 @@ defmodule UndefinedFunctionError do
|
||||
|
||||
defp load_module({name, _path, _loaded?}) do
|
||||
name
|
||||
|> List.to_atom()
|
||||
|> List.to_unsafe_atom()
|
||||
|> Code.ensure_loaded()
|
||||
end
|
||||
|
||||
@@ -2376,7 +2376,7 @@ defmodule File.Error do
|
||||
@moduledoc """
|
||||
An exception that is raised when a file operation fails.
|
||||
|
||||
For example, this exception is raised, when trying to read a non existent file:
|
||||
For example, this exception is raised, when trying to read a nonexistent file:
|
||||
|
||||
iex> File.read!("nonexistent_file.txt")
|
||||
** (File.Error) could not read file "nonexistent_file.txt": no such file or directory
|
||||
@@ -2409,7 +2409,7 @@ defmodule File.CopyError do
|
||||
@moduledoc """
|
||||
An exception that is raised when copying a file fails.
|
||||
|
||||
For example, this exception is raised when trying to copy to file or directory that isn't present:
|
||||
For example, this exception is raised when trying to copy to a file or directory that isn't present:
|
||||
|
||||
iex> File.cp_r!("non_existent", "source_dir/subdir")
|
||||
** (File.CopyError) could not copy recursively from "non_existent" to "source_dir/subdir". non_existent: no such file or directory
|
||||
@@ -2477,7 +2477,7 @@ defmodule File.LinkError do
|
||||
@moduledoc """
|
||||
An exception that is raised when linking a file fails.
|
||||
|
||||
For example, this exception is raised when trying to link to file that isn't present:
|
||||
For example, this exception is raised when trying to link to a file that isn't present:
|
||||
|
||||
iex> File.ln!("existing.txt", "link.txt")
|
||||
** (File.LinkError) could not create hard link from "link.txt" to "existing.txt": no such file or directory
|
||||
|
||||
+110
-26
@@ -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
|
||||
|
||||
@@ -881,7 +897,7 @@ defmodule File do
|
||||
File.copy("non_existing.txt", "copy.txt")
|
||||
#=> {:error, :enoent}
|
||||
"""
|
||||
@spec copy(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
|
||||
@spec copy(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
|
||||
{:ok, non_neg_integer} | {:error, posix | :badarg | :terminated}
|
||||
def copy(source, destination, bytes_count \\ :infinity) do
|
||||
source = normalize_path_or_io_device(source)
|
||||
@@ -902,7 +918,7 @@ defmodule File do
|
||||
File.copy!("non_existing.txt", "copy.txt")
|
||||
** (File.CopyError) could not copy from "non_existing.txt" to "copy.txt": no such file or directory
|
||||
"""
|
||||
@spec copy!(Path.t() | io_device, Path.t() | io_device, pos_integer | :infinity) ::
|
||||
@spec copy!(Path.t() | io_device, Path.t() | io_device, non_neg_integer | :infinity) ::
|
||||
non_neg_integer
|
||||
def copy!(source, destination, bytes_count \\ :infinity) do
|
||||
case copy(source, destination, bytes_count) do
|
||||
@@ -1087,7 +1103,7 @@ defmodule File do
|
||||
|
||||
@doc ~S"""
|
||||
Copies the contents in `source` to `destination` recursively, maintaining the
|
||||
source directory structure and modes.
|
||||
source directory structure and regular file modes.
|
||||
|
||||
If `source` is a file or a symbolic link to it, `destination` must be a path
|
||||
to an existent file, a symbolic link to one, or a path to a non-existent file.
|
||||
@@ -1098,7 +1114,9 @@ defmodule File do
|
||||
If the source is a file, it copies `source` to `destination`. If the `source`
|
||||
is a directory, it copies the contents inside source into the `destination` directory.
|
||||
|
||||
If a file already exists in the destination, it invokes the optional `on_conflict`
|
||||
For regular files, their respective file modes are preserved in the destination.
|
||||
Directory modes are preserved only when `:preserve_directory_permissions` is `true`.
|
||||
If a file already exists in the destination, it invokes the optional `:on_conflict`
|
||||
callback given as an option. See "Options" for more information.
|
||||
|
||||
This function may fail while copying files, in such cases, it will leave the
|
||||
@@ -1116,6 +1134,12 @@ defmodule File do
|
||||
|
||||
Special files such as device files, sockets, and named pipes are not copied.
|
||||
|
||||
Typical error reasons are:
|
||||
|
||||
* `:enoent` - `source` does not exist
|
||||
* `:eisdir` - `source` is a file and `destination` is a directory
|
||||
* `:einval` - `destination` is the same as or a subdirectory of `source`
|
||||
|
||||
## Options
|
||||
|
||||
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
|
||||
@@ -1129,6 +1153,11 @@ defmodule File do
|
||||
dereferenced and have their contents copied instead when set to `true`. If the dereferenced
|
||||
files do not exist, than the operation fails. The default is `false`.
|
||||
|
||||
* `:preserve_directory_permissions` - (since v1.20.0) when `true`, the permissions of
|
||||
source directories are copied to the destination directories after their contents are
|
||||
written. This is useful when source directories are read-only or have restricted
|
||||
permissions that must be preserved. The default is `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
# Copies file "a.txt" to "b.txt"
|
||||
@@ -1146,11 +1175,16 @@ defmodule File do
|
||||
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
|
||||
|
||||
File.cp_r("non_existing.txt", "copy.txt")
|
||||
#=> {:error, :enoent}
|
||||
#=> {:error, :enoent, "non_existing.txt"}
|
||||
|
||||
# Copying into a subdirectory of source is not allowed
|
||||
File.cp_r("src", "src/dest")
|
||||
#=> {:error, :einval, "src/dest"}
|
||||
"""
|
||||
@spec cp_r(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean()
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
) ::
|
||||
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
|
||||
|
||||
@@ -1172,6 +1206,7 @@ defmodule File do
|
||||
def cp_r(source, destination, options) when is_list(options) do
|
||||
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
|
||||
dereference? = Keyword.get(options, :dereference_symlinks, false)
|
||||
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
|
||||
|
||||
source =
|
||||
source
|
||||
@@ -1183,9 +1218,25 @@ defmodule File do
|
||||
|> IO.chardata_to_string()
|
||||
|> assert_no_null_byte!("File.cp_r/3")
|
||||
|
||||
case do_cp_r(source, destination, on_conflict, dereference?, []) do
|
||||
{:error, _, _} = error -> error
|
||||
res -> {:ok, res}
|
||||
source_parts = source |> Path.expand() |> Path.split()
|
||||
dest_parts = destination |> Path.expand() |> Path.split()
|
||||
|
||||
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
|
||||
{:error, :einval, destination}
|
||||
else
|
||||
dereference = if dereference?, do: MapSet.new(), else: nil
|
||||
|
||||
case do_cp_r(
|
||||
source,
|
||||
destination,
|
||||
on_conflict,
|
||||
dereference,
|
||||
preserve_directory_permissions?,
|
||||
[]
|
||||
) do
|
||||
{:error, _, _} = error -> error
|
||||
res -> {:ok, res}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1206,7 +1257,8 @@ defmodule File do
|
||||
"""
|
||||
@spec cp_r!(Path.t(), Path.t(),
|
||||
on_conflict: on_conflict_callback,
|
||||
dereference_symlinks: boolean()
|
||||
dereference_symlinks: boolean(),
|
||||
preserve_directory_permissions: boolean()
|
||||
) :: [binary]
|
||||
def cp_r!(source, destination, options \\ []) do
|
||||
case cp_r(source, destination, options) do
|
||||
@@ -1223,7 +1275,7 @@ defmodule File do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
|
||||
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
|
||||
case :elixir_utils.read_link_type(src) do
|
||||
{:ok, :regular} ->
|
||||
case do_cp_file(src, dest, on_conflict, acc) do
|
||||
@@ -1236,8 +1288,15 @@ defmodule File do
|
||||
|
||||
{:ok, :symlink} ->
|
||||
case :file.read_link(src) do
|
||||
{:ok, link} when dereference? ->
|
||||
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
|
||||
{:ok, link} when dereference != nil ->
|
||||
resolved = Path.expand(link, Path.dirname(src))
|
||||
|
||||
if MapSet.member?(dereference, resolved) do
|
||||
{:error, :eloop, src}
|
||||
else
|
||||
dereference = MapSet.put(dereference, resolved)
|
||||
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
|
||||
end
|
||||
|
||||
{:ok, link} ->
|
||||
do_cp_link(link, src, dest, on_conflict, acc)
|
||||
@@ -1251,9 +1310,35 @@ defmodule File do
|
||||
{:ok, files} ->
|
||||
case mkdir(dest) do
|
||||
success when success in [:ok, {:error, :eexist}] ->
|
||||
Enum.reduce(files, [dest | acc], fn x, acc ->
|
||||
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
|
||||
files
|
||||
|> Enum.reduce_while([dest | acc], fn x, acc ->
|
||||
case do_cp_r(
|
||||
Path.join(src, x),
|
||||
Path.join(dest, x),
|
||||
on_conflict,
|
||||
dereference,
|
||||
preserve_dir_perms?,
|
||||
acc
|
||||
) do
|
||||
{:error, _, _} = error -> {:halt, error}
|
||||
acc -> {:cont, acc}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
files when preserve_dir_perms? ->
|
||||
# Change the directory after writing files in case
|
||||
# it was originally read only
|
||||
case copy_file_mode(src, dest) do
|
||||
:ok -> files
|
||||
{:error, reason} -> {:error, reason, src}
|
||||
end
|
||||
|
||||
files ->
|
||||
files
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, dest}
|
||||
@@ -1272,14 +1357,13 @@ 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
|
||||
|
||||
defp copy_file_mode(src, dest) do
|
||||
with {:ok, dest_fileinfo} <- stat(dest),
|
||||
{:ok, src_fileinfo} <- stat(src) do
|
||||
write_stat(dest, %{dest_fileinfo | mode: src_fileinfo.mode})
|
||||
with {:ok, src_fileinfo} <- stat(src) do
|
||||
chmod(dest, src_fileinfo.mode)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2136,7 +2220,7 @@ defmodule File do
|
||||
type. If you pass, for example, `[encoding: :utf8]` or
|
||||
`[encoding: {:utf16, :little}]` in the modes parameter, the underlying stream
|
||||
will use `IO.write/2` and the `String.Chars` protocol to convert the data.
|
||||
See `IO.binwrite/2` and `IO.write/2` .
|
||||
See `IO.binwrite/2` and `IO.write/2`.
|
||||
|
||||
One may also consider passing the `:delayed_write` option if the stream
|
||||
is meant to be written to under a tight loop.
|
||||
|
||||
@@ -18,7 +18,13 @@ defmodule File.Stream do
|
||||
|
||||
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
@type t :: %__MODULE__{
|
||||
path: Path.t(),
|
||||
modes: [term()],
|
||||
line_or_bytes: :line | pos_integer(),
|
||||
raw: boolean(),
|
||||
node: node()
|
||||
}
|
||||
|
||||
@doc false
|
||||
def __build__(path, line_or_bytes, modes) do
|
||||
@@ -119,7 +125,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 +235,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
|
||||
|
||||
+190
-141
@@ -25,7 +25,7 @@ defmodule Float do
|
||||
and arithmetic due to the fact most decimal fractions cannot be
|
||||
represented by a floating-point binary and most operations are not exact,
|
||||
but operate on approximations. Those issues are not specific
|
||||
to Elixir, they are a property of floating point representation itself.
|
||||
to Elixir, they are a property of floating-point representation itself.
|
||||
|
||||
For example, the numbers 0.1 and 0.01 are two of them, what means the result
|
||||
of squaring 0.1 does not give 0.01 neither the closest representable. Here is
|
||||
@@ -167,51 +167,73 @@ defmodule Float do
|
||||
parse_unsigned(binary)
|
||||
end
|
||||
|
||||
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, false, false, [digit])
|
||||
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, false)
|
||||
|
||||
defp parse_unsigned(binary) when is_binary(binary), do: :error
|
||||
|
||||
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, dot?, e?, [digit | acc])
|
||||
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, dot?)
|
||||
|
||||
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, false, [digit, ?. | acc])
|
||||
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9,
|
||||
do: parse_mantissa(binary, rest, true)
|
||||
|
||||
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
|
||||
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?)
|
||||
when exp_marker in ~c"eE" and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
|
||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
||||
|
||||
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
|
||||
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?)
|
||||
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
|
||||
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)
|
||||
|
||||
# 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
|
||||
acc
|
||||
|> add_dot(dot?)
|
||||
|> :lists.reverse()
|
||||
|> :erlang.list_to_float()
|
||||
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?)
|
||||
|
||||
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
|
||||
do: parse_exponent(binary, exp_pos, rest, dot?)
|
||||
|
||||
defp parse_exponent(binary, exp_pos, rest, dot?),
|
||||
do: finish_exponent(binary, exp_pos, rest, dot?)
|
||||
|
||||
defp finish_mantissa(binary, rest, _dot? = true) do
|
||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
else
|
||||
float -> {float, rest}
|
||||
end
|
||||
|
||||
defp parse_unsigned(rest, dot?, false = _e?, acc) do
|
||||
float =
|
||||
acc
|
||||
|> add_dot(dot?)
|
||||
|> :lists.reverse()
|
||||
|> :erlang.list_to_float()
|
||||
|
||||
{float, rest}
|
||||
# Bare integer: * 1.0 casts to the nearest float without building a new binary,
|
||||
# and raises ArithmeticError on overflow (for example a 400-digit integer).
|
||||
defp finish_mantissa(binary, rest, _dot? = false) do
|
||||
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
|
||||
rescue
|
||||
ArithmeticError -> :error
|
||||
end
|
||||
|
||||
defp add_dot(acc, true), do: acc
|
||||
defp add_dot(acc, false), do: [?0, ?. | acc]
|
||||
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400".
|
||||
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do
|
||||
{:erlang.binary_to_float(consumed(binary, rest)), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
|
||||
# form a valid float literal.
|
||||
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
|
||||
len = byte_size(binary) - byte_size(rest)
|
||||
|
||||
literal =
|
||||
IO.iodata_to_binary([
|
||||
:binary.part(binary, 0, exp_pos),
|
||||
".0",
|
||||
:binary.part(binary, exp_pos, len - exp_pos)
|
||||
])
|
||||
|
||||
{:erlang.binary_to_float(literal), rest}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
defp consumed(binary, ""), do: binary
|
||||
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest float less than or equal to `number`.
|
||||
@@ -264,7 +286,7 @@ defmodule Float do
|
||||
@doc """
|
||||
Rounds a float to the smallest float greater than or equal to `number`.
|
||||
|
||||
`ceil/2` also accepts a precision to round a floating-point value down
|
||||
`ceil/2` also accepts a precision to round a floating-point value up
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
|
||||
The operation is performed on the binary floating point, without a
|
||||
@@ -333,7 +355,7 @@ defmodule Float do
|
||||
and therefore the number above is internally represented as 5.567499999,
|
||||
which explains the behavior above. If you want exact rounding for decimals,
|
||||
you must use a decimal library. The behavior above is also in accordance
|
||||
to reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||
with reference implementations, such as "Correctly Rounded Binary-Decimal and
|
||||
Decimal-Binary Conversions" by David M. Gay.
|
||||
|
||||
## Examples
|
||||
@@ -355,15 +377,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 float === 0.0 or 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 +396,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.
|
||||
@@ -656,7 +705,7 @@ defmodule Float do
|
||||
end
|
||||
|
||||
defp invalid_precision_message(precision) do
|
||||
"precision #{precision} is out of valid range of #{inspect(@precision_range)}"
|
||||
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}"
|
||||
end
|
||||
|
||||
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
|
||||
|
||||
@@ -69,7 +69,6 @@ defmodule Function do
|
||||
| :name
|
||||
| :new_index
|
||||
| :new_uniq
|
||||
| :pid
|
||||
| :type
|
||||
| :uniq
|
||||
|
||||
@@ -112,8 +111,6 @@ defmodule Function do
|
||||
When `fun` is an anonymous function (that is, the type is `:local`), the following
|
||||
additional keys are returned:
|
||||
|
||||
* `:pid` - PID of the process that originally created the function.
|
||||
|
||||
* `:index` - (integer) an index into the module function table.
|
||||
|
||||
* `:new_index` - (integer) an index into the module function table.
|
||||
@@ -159,7 +156,7 @@ defmodule Function do
|
||||
`:module`, `:name`, `:arity`, `:env`, or `:type`.
|
||||
|
||||
For anonymous functions, there is also information about any of the
|
||||
atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
|
||||
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`.
|
||||
For a named function, the value of any of these items is always the
|
||||
atom `:undefined`.
|
||||
|
||||
@@ -179,8 +176,6 @@ defmodule Function do
|
||||
iex> fun = &String.length/1
|
||||
iex> Function.info(fun, :name)
|
||||
{:name, :length}
|
||||
iex> Function.info(fun, :pid)
|
||||
{:pid, :undefined}
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule GenEvent do
|
||||
alternative. GenStage is an external Elixir library maintained by the Elixir
|
||||
team; it provides a tool to implement systems that exchange events in a
|
||||
demand-driven way with built-in support for back-pressure. See the [GenStage
|
||||
documentation](https://hexdocs.pm/gen_stage) for more information.
|
||||
documentation](https://gen-stage.hexdocs.pm) for more information.
|
||||
|
||||
### `:gen_event`
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -564,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}
|
||||
@@ -857,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]
|
||||
@@ -1152,7 +1158,7 @@ defmodule GenServer do
|
||||
|
||||
## Timeouts
|
||||
|
||||
`timeout` is an integer greater than zero which specifies how many
|
||||
`timeout` is a non-negative integer which specifies how many
|
||||
milliseconds to wait for a reply, or the atom `:infinity` to wait
|
||||
indefinitely. The default value is `5000`. If no reply is received within
|
||||
the specified time, the function call fails and the caller exits. If the
|
||||
|
||||
@@ -265,13 +265,13 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def reduce(dict, acc, fun) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module.reduce(dict, acc, fun)
|
||||
end
|
||||
|
||||
def member?(dict, {key, value}) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
|
||||
end
|
||||
|
||||
@@ -281,7 +281,7 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def count(dict) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
{:ok, module.size(dict)}
|
||||
end
|
||||
|
||||
@@ -296,7 +296,7 @@ defimpl Collectable, for: HashDict do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
|
||||
collector_fun = fn
|
||||
dict, {:cont, {key, value}} -> module.put(dict, key, value)
|
||||
@@ -315,7 +315,7 @@ defimpl Inspect, for: HashDict do
|
||||
|
||||
def inspect(dict, opts) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -279,19 +279,19 @@ defimpl Enumerable, for: HashSet do
|
||||
|
||||
def reduce(set, acc, fun) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module.reduce(set, acc, fun)
|
||||
end
|
||||
|
||||
def member?(set, term) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
{:ok, module.member?(set, term)}
|
||||
end
|
||||
|
||||
def count(set) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
{:ok, module.size(set)}
|
||||
end
|
||||
|
||||
@@ -306,7 +306,7 @@ defimpl Collectable, for: HashSet do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
|
||||
collector_fun = fn
|
||||
set, {:cont, term} -> module.put(set, term)
|
||||
@@ -325,7 +325,7 @@ defimpl Inspect, for: HashSet do
|
||||
|
||||
def inspect(set, opts) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -92,7 +92,7 @@ defprotocol Inspect do
|
||||
end
|
||||
|
||||
inspect(%Point{x: 1})
|
||||
%Point{x: 1, y: 0}
|
||||
#=> %Point{x: 1, y: 0}
|
||||
|
||||
## Custom implementation
|
||||
|
||||
@@ -387,13 +387,20 @@ defimpl Inspect, for: List do
|
||||
close = color_doc("]", :list, opts)
|
||||
|
||||
cond do
|
||||
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
inspected =
|
||||
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
|
||||
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
|
||||
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
|
||||
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
|
||||
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
{split, tail} =
|
||||
if is_integer(printable_limit) do
|
||||
case Enum.split(term, printable_limit) do
|
||||
{split, []} -> {split, []}
|
||||
{split, _} -> {split, " ++ ..."}
|
||||
end
|
||||
else
|
||||
{term, []}
|
||||
end
|
||||
|
||||
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
|
||||
inspected = [?~, ?c, ?", escaped, ?" | tail]
|
||||
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
|
||||
|
||||
keyword?(term) ->
|
||||
@@ -407,6 +414,19 @@ defimpl Inspect, for: List do
|
||||
end
|
||||
end
|
||||
|
||||
defp unicode_list?(_, 0), do: true
|
||||
|
||||
defp unicode_list?([char | rest], counter)
|
||||
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
|
||||
do: unicode_list?(rest, decrement(counter))
|
||||
|
||||
defp unicode_list?([], _counter), do: true
|
||||
defp unicode_list?(_, _counter), do: false
|
||||
|
||||
@compile {:inline, decrement: 1}
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
|
||||
@doc false
|
||||
def keyword({key, value}, opts) do
|
||||
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
|
||||
@@ -416,8 +436,8 @@ defimpl Inspect, for: List do
|
||||
|
||||
@doc false
|
||||
def keyword?([{key, _value} | rest]) when is_atom(key) do
|
||||
case Atom.to_charlist(key) do
|
||||
[?E, ?l, ?i, ?x, ?i, ?r, ?.] ++ _ -> false
|
||||
case Atom.to_string(key) do
|
||||
"Elixir." <> _ -> false
|
||||
_ -> keyword?(rest)
|
||||
end
|
||||
end
|
||||
@@ -669,6 +689,11 @@ defimpl Inspect, for: Any do
|
||||
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
|
||||
end
|
||||
|
||||
# A temporary clause to deal with native records until they are officially supported
|
||||
def inspect(native_record, _opts) do
|
||||
:io_lib.format("~p", [native_record]) |> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
def inspect_as_struct(map, name, infos, opts) do
|
||||
open = color_doc("#" <> name <> "<", :map, opts)
|
||||
sep = color_doc(",", :map, opts)
|
||||
@@ -696,7 +721,15 @@ defimpl Inspect, for: Range do
|
||||
end
|
||||
|
||||
# TODO: Remove me on v2.0
|
||||
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
|
||||
inspect =
|
||||
quote generated: true do
|
||||
inspect(
|
||||
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
|
||||
var!(opts)
|
||||
)
|
||||
end
|
||||
|
||||
def unquote(inspect) do
|
||||
step = if first <= last, do: 1, else: -1
|
||||
inspect(Map.put(range, :step, step), opts)
|
||||
end
|
||||
|
||||
@@ -21,8 +21,8 @@ defmodule Inspect.Opts do
|
||||
is `:decimal` and if it is printable, otherwise in bit syntax. See
|
||||
`String.printable?/1` to learn when a string is printable.
|
||||
|
||||
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
|
||||
non-printable elements will be escaped.
|
||||
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
|
||||
non-printable code points will be escaped. Other lists will be printed as lists.
|
||||
|
||||
When `:as_lists` all lists will be printed as lists.
|
||||
|
||||
@@ -46,8 +46,8 @@ defmodule Inspect.Opts do
|
||||
* `:limit` - limits the number of items that are inspected for tuples,
|
||||
bitstrings, maps, lists and any other collection of items, with the exception of
|
||||
printable strings and printable charlists which use the `:printable_limit` option.
|
||||
It accepts a positive integer or `:infinity`. It defaults to `100` since
|
||||
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
|
||||
It accepts a positive integer or `:infinity`. It defaults to `200` since
|
||||
`Elixir v1.20.0`, as it has better defaults to deal with nested collections.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
|
||||
|
||||
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
|
||||
charlists: :infer,
|
||||
custom_options: [],
|
||||
inspect_fun: &Inspect.inspect/2,
|
||||
limit: 100,
|
||||
limit: 200,
|
||||
pretty: false,
|
||||
printable_limit: 4096,
|
||||
safe: true,
|
||||
|
||||
@@ -18,6 +18,45 @@ defmodule Integer do
|
||||
|
||||
import Bitwise
|
||||
|
||||
@doc """
|
||||
Counts the number of set bits (1) in the binary representation of a non-negative `integer`.
|
||||
|
||||
This operation is known as the Hamming weight or population count.
|
||||
|
||||
Raises an `ArithmeticError` if `integer` is negative.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.popcount(0)
|
||||
0
|
||||
|
||||
iex> Integer.popcount(1)
|
||||
1
|
||||
|
||||
iex> Integer.popcount(0b10110101)
|
||||
5
|
||||
|
||||
iex> Integer.popcount(255)
|
||||
8
|
||||
|
||||
iex> Integer.popcount(0b1111111111111111)
|
||||
16
|
||||
|
||||
iex> Integer.popcount(-1)
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec popcount(non_neg_integer) :: non_neg_integer
|
||||
def popcount(integer) when is_integer(integer) and integer < 0,
|
||||
do: :erlang.error(:badarith, [integer])
|
||||
|
||||
def popcount(integer) when is_integer(integer),
|
||||
do: popcount(integer, 0)
|
||||
|
||||
defp popcount(0, acc), do: acc
|
||||
defp popcount(n, acc), do: popcount(n &&& n - 1, acc + 1)
|
||||
|
||||
@doc """
|
||||
Determines if `integer` is odd.
|
||||
|
||||
@@ -69,7 +108,7 @@ defmodule Integer do
|
||||
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
|
||||
|
||||
@doc """
|
||||
Computes `base` raised to power of `exponent`.
|
||||
Computes `base` raised to the power of `exponent`.
|
||||
|
||||
Both `base` and `exponent` must be integers.
|
||||
The exponent must be zero or positive.
|
||||
@@ -258,8 +297,9 @@ defmodule Integer do
|
||||
|
||||
defp undigits([], _base, acc), do: acc
|
||||
|
||||
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
defp undigits([digit | _], base, _)
|
||||
when is_integer(digit) and (digit >= base or digit <= -base),
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
|
||||
defp undigits([digit | tail], base, acc) when is_integer(digit),
|
||||
do: undigits(tail, base, acc * base + digit)
|
||||
@@ -270,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.
|
||||
@@ -289,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, ""}
|
||||
|
||||
@@ -489,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}
|
||||
|
||||
@@ -498,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 is_integer(b) and b > 0, do: {b, 0, 1}
|
||||
def extended_gcd(0, b) when is_integer(b) and b < 0, do: {-b, 0, -1}
|
||||
def extended_gcd(a, 0) when is_integer(a) and a > 0, do: {a, 1, 0}
|
||||
def extended_gcd(a, 0) when is_integer(a) and a < 0, do: {-a, -1, 0}
|
||||
|
||||
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
|
||||
extended_gcd(integer2, integer1, 0, 1, 1, 0)
|
||||
|
||||
@@ -128,7 +128,7 @@ defmodule IO do
|
||||
@type nodata :: {:error, term} | :eof
|
||||
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
|
||||
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, String.Chars.t()}]
|
||||
|
||||
@typedoc """
|
||||
Stacktrace information as keyword options for `warn/2`.
|
||||
@@ -152,9 +152,9 @@ defmodule IO do
|
||||
|
||||
The `device` is iterated as specified by the `line_or_chars` argument:
|
||||
|
||||
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
|
||||
iterated by that number of bytes. This should be the preferred mode for reading
|
||||
non-textual inputs.
|
||||
* if `line_or_chars` is an integer, it is the number of Unicode
|
||||
code points to be retrieved for devices open in Unicode/utf8 mode.
|
||||
Otherwise, it is the number of raw bytes to be retrieved.
|
||||
|
||||
* if `line_or_chars` is `:line`, the device is iterated line by line.
|
||||
CRLF newlines ("\r\n") are automatically normalized to "\n".
|
||||
@@ -521,7 +521,7 @@ defmodule IO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from IO device `:stdio`.
|
||||
Gets a number of characters from IO device `:stdio`.
|
||||
|
||||
If `:stdio` is a Unicode device, `count` implies
|
||||
the number of Unicode code points to be retrieved.
|
||||
@@ -549,7 +549,7 @@ defmodule IO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a number of bytes from the IO `device`.
|
||||
Gets a number of characters from the IO `device`.
|
||||
|
||||
If the IO `device` is a Unicode device, `count` implies
|
||||
the number of Unicode code points to be retrieved.
|
||||
|
||||
@@ -111,13 +111,15 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defsequence = fn name, code, terminator ->
|
||||
sequence = "\e[#{code}#{terminator}"
|
||||
|
||||
@spec unquote(name)() :: String.t()
|
||||
def unquote(name)() do
|
||||
"\e[#{unquote(code)}#{unquote(terminator)}"
|
||||
unquote(sequence)
|
||||
end
|
||||
|
||||
defp format_sequence(unquote(name)) do
|
||||
unquote(name)()
|
||||
unquote(sequence)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -159,7 +161,7 @@ defmodule IO.ANSI do
|
||||
|
||||
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
|
||||
@doc "Sets alternative font #{font_n}."
|
||||
defsequence.(:"font_#{font_n}", font_n + 10, "m")
|
||||
defsequence.(String.to_unsafe_atom("font_#{font_n}"), font_n + 10, "m")
|
||||
end
|
||||
|
||||
@doc "Normal color or intensity."
|
||||
@@ -193,13 +195,13 @@ defmodule IO.ANSI do
|
||||
defsequence.(color, code + 30, "m")
|
||||
|
||||
@doc "Sets foreground color to light #{color}."
|
||||
defsequence.(:"light_#{color}", code + 90, "m")
|
||||
defsequence.(String.to_unsafe_atom("light_#{color}"), code + 90, "m")
|
||||
|
||||
@doc "Sets background color to #{color}."
|
||||
defsequence.(:"#{color}_background", code + 40, "m")
|
||||
defsequence.(String.to_unsafe_atom("#{color}_background"), code + 40, "m")
|
||||
|
||||
@doc "Sets background color to light #{color}."
|
||||
defsequence.(:"light_#{color}_background", code + 100, "m")
|
||||
defsequence.(String.to_unsafe_atom("light_#{color}_background"), code + 100, "m")
|
||||
end
|
||||
|
||||
@doc "Default text color."
|
||||
@@ -332,7 +334,7 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, true, true) do
|
||||
[acc | IO.ANSI.reset()]
|
||||
[acc | reset()]
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, _emit?, _append_reset) do
|
||||
|
||||
@@ -673,7 +673,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
# Characters that can mark the beginning or the end of a word.
|
||||
# Only support the most common ones at this moment.
|
||||
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
@delimiters [?\s, ?', ?", ?!, ??, ?,, ?:, ?;, ?/, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
|
||||
|
||||
### Inline start
|
||||
|
||||
@@ -31,7 +31,7 @@ defmodule IO.Stream do
|
||||
@type t :: %__MODULE__{
|
||||
device: IO.device(),
|
||||
raw: boolean(),
|
||||
line_or_bytes: :line | non_neg_integer()
|
||||
line_or_bytes: :line | pos_integer()
|
||||
}
|
||||
|
||||
@doc false
|
||||
|
||||
+12
-11
@@ -24,7 +24,7 @@ defprotocol JSON.Encoder do
|
||||
> #### Leaking Private Information {: .error}
|
||||
>
|
||||
> Prefer using `:only` to avoid accidentally leaking private information when
|
||||
> new fields are added. Other approaches should be used with auction.
|
||||
> new fields are added. Other approaches should be used with caution.
|
||||
|
||||
You can also use `Protocol.derive/3` if you don't own the struct that you want
|
||||
to encode to JSON:
|
||||
@@ -67,7 +67,7 @@ defprotocol JSON.Encoder do
|
||||
|
||||
{io, _prefix} =
|
||||
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
|
||||
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
|
||||
key = IO.iodata_to_binary([prefix, :json.encode_binary(Atom.to_string(field)), ?:])
|
||||
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
|
||||
end)
|
||||
|
||||
@@ -130,25 +130,25 @@ end
|
||||
|
||||
defimpl JSON.Encoder, for: BitString do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_binary(value)
|
||||
:json.encode_binary(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: List do
|
||||
def encode(value, encoder) do
|
||||
:elixir_json.encode_list(value, encoder)
|
||||
:json.encode_list(value, encoder)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Integer do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_integer(value)
|
||||
:json.encode_integer(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Float do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_float(value)
|
||||
:json.encode_float(value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -175,6 +175,7 @@ defimpl JSON.Encoder, for: Map do
|
||||
|
||||
# Erlang supports only numbers, binaries, and atoms as keys,
|
||||
# we support anything that implements the String.Chars protocol.
|
||||
@compile inline: [key: 2]
|
||||
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
|
||||
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
|
||||
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
|
||||
@@ -408,7 +409,7 @@ defmodule JSON do
|
||||
decoders = Keyword.put_new(decoders, :null, nil)
|
||||
|
||||
try do
|
||||
:elixir_json.decode(binary, acc, Map.new(decoders))
|
||||
:json.decode(binary, acc, Map.new(decoders))
|
||||
catch
|
||||
:error, :unexpected_end ->
|
||||
{:error, {:unexpected_end, byte_size(binary)}}
|
||||
@@ -528,16 +529,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)
|
||||
|
||||
+109
-89
@@ -142,7 +142,7 @@ defmodule Kernel do
|
||||
* [Patterns and guards](patterns-and-guards.md) - an introduction to patterns,
|
||||
guards, and extensions
|
||||
* [Syntax reference](syntax-reference.md) - the language syntax reference
|
||||
* [Typespecs reference](typespecs.md)- types and function specifications, including list of types
|
||||
* [Typespecs reference](typespecs.md) - types and function specifications, including list of types
|
||||
* [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode
|
||||
|
||||
## Guards
|
||||
@@ -2069,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
|
||||
|
||||
@@ -2104,7 +2109,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
[optimize_boolean: true, type_check: {:case, :"!!"}],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when unquote(x_is_false_or_nil()) -> false
|
||||
@@ -2118,7 +2123,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
[optimize_boolean: true, type_check: {:case, :!}],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when unquote(x_is_false_or_nil()) -> true
|
||||
@@ -2747,7 +2752,7 @@ defmodule Kernel do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(term) do
|
||||
%_{__exception__: true} -> true
|
||||
%_{__exception__: _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2759,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
|
||||
@@ -2787,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
|
||||
|
||||
@@ -2805,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
|
||||
@@ -3085,7 +3088,7 @@ defmodule Kernel do
|
||||
@doc """
|
||||
Pops a key from the given nested structure.
|
||||
|
||||
Uses the `Access` protocol to traverse the structures
|
||||
Uses the `Access` behaviour to traverse the structures
|
||||
according to the given `keys`, unless the `key` is a
|
||||
function. If the key is a function, it will be invoked
|
||||
as specified in `get_and_update_in/3`.
|
||||
@@ -4049,7 +4052,7 @@ defmodule Kernel do
|
||||
|
||||
defp build_if(condition, do: do_clause, else: else_clause) do
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
[optimize_boolean: true, type_check: {:case, :if}],
|
||||
quote do
|
||||
case unquote(condition) do
|
||||
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
|
||||
@@ -4100,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
|
||||
@@ -4370,7 +4379,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
|
||||
|
||||
annotate_case(
|
||||
[type_check: :expr],
|
||||
[type_check: {:case, :&&}],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
@@ -4413,7 +4422,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "||")
|
||||
|
||||
annotate_case(
|
||||
[type_check: :expr],
|
||||
[type_check: {:case, :||}],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
@@ -4694,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 ->
|
||||
@@ -4716,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)
|
||||
@@ -4751,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
|
||||
@@ -4816,12 +4815,12 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp in_list(left, head, tail, expand, right, in_body?) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
|
||||
defp in_list(left, head, tail, expand, right) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right), [head | tail])
|
||||
:lists.foldl("e(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
|
||||
end
|
||||
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right) do
|
||||
case expand.(tail) do
|
||||
[] ->
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(head)))
|
||||
@@ -4830,15 +4829,7 @@ defmodule Kernel do
|
||||
quote do
|
||||
Kernel.or(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
|
||||
)
|
||||
end
|
||||
|
||||
tail when in_body? ->
|
||||
quote do
|
||||
Kernel.or(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
:lists.member(unquote(left), unquote(tail))
|
||||
unquote(in_list(left, tail_head, tail, expand, right))
|
||||
)
|
||||
end
|
||||
|
||||
@@ -4847,7 +4838,7 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp comp(left, right, _expand, _right, _in_body?) do
|
||||
defp comp(left, right, _expand, _right) do
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(right)))
|
||||
end
|
||||
|
||||
@@ -4969,6 +4960,7 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec binary_slice(binary, integer, non_neg_integer) :: binary
|
||||
def binary_slice(binary, start, size)
|
||||
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
|
||||
total = byte_size(binary)
|
||||
@@ -5042,6 +5034,7 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec binary_slice(binary, Range.t()) :: binary
|
||||
def binary_slice(binary, first..last//step)
|
||||
when is_binary(binary) and step > 0 do
|
||||
total = byte_size(binary)
|
||||
@@ -5156,7 +5149,7 @@ defmodule Kernel do
|
||||
warning saying that a module has been redefined.
|
||||
|
||||
There are some modules that Elixir does not currently implement but it
|
||||
may be implement in the future. Those modules are reserved and defining
|
||||
may implement in the future. Those modules are reserved and defining
|
||||
them will result in a compilation error:
|
||||
|
||||
defmodule Any do
|
||||
@@ -5223,6 +5216,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: []
|
||||
|
||||
@@ -5252,12 +5250,12 @@ defmodule Kernel do
|
||||
# defmodule Alias nested
|
||||
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
|
||||
module = :elixir_aliases.concat([env.module, h])
|
||||
alias = String.to_atom("Elixir." <> Atom.to_string(h))
|
||||
alias = String.to_unsafe_atom("Elixir." <> Atom.to_string(h))
|
||||
opts = [as: alias, warn: false]
|
||||
|
||||
case t do
|
||||
[] -> {module, module, opts}
|
||||
_ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
|
||||
_ -> {String.to_unsafe_atom(Enum.join([module | t], ".")), module, opts}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5564,7 +5562,7 @@ defmodule Kernel do
|
||||
when the struct is printed:
|
||||
|
||||
defmodule User do
|
||||
@derive {Inspect, only: :name}
|
||||
@derive {Inspect, only: [:name]}
|
||||
defstruct name: nil, age: nil
|
||||
end
|
||||
|
||||
@@ -5591,6 +5589,9 @@ defmodule Kernel do
|
||||
defstruct name: nil, age: 10 + 11
|
||||
end
|
||||
|
||||
`@enforce_keys` must be set to an atom or a list of unique atoms,
|
||||
all of which must name fields defined by `defstruct/1`
|
||||
|
||||
Now trying to build a struct without the name key will fail:
|
||||
|
||||
%User{age: 21}
|
||||
@@ -5875,11 +5876,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)
|
||||
@@ -5887,31 +5892,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()
|
||||
@@ -6279,7 +6283,7 @@ defmodule Kernel do
|
||||
step through the code it sees). For general stepping, you can set breakpoints
|
||||
using `IEx.break!/4`.
|
||||
|
||||
For more information, [see IEx documentation](https://hexdocs.pm/iex/IEx.html#module-dbg-and-breakpoints).
|
||||
For more information, [see IEx documentation](https://iex.hexdocs.pm/IEx.html#module-dbg-and-breakpoints).
|
||||
|
||||
## Configuring the debug function
|
||||
|
||||
@@ -6326,7 +6330,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
|
||||
|
||||
@@ -6370,7 +6382,7 @@ defmodule Kernel do
|
||||
|
||||
### Passing timeouts
|
||||
|
||||
You can also pass timeouts directly to this functions, that is, milliseconds or
|
||||
You can also pass timeouts directly to this function, that is, milliseconds or
|
||||
the atom `:infinity`. In this case, this function just returns the given argument.
|
||||
|
||||
## Examples
|
||||
@@ -6415,12 +6427,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
|
||||
|
||||
@@ -6882,7 +6902,7 @@ defmodule Kernel do
|
||||
defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string)
|
||||
when alias >= ?A and alias <= ?Z do
|
||||
string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1)
|
||||
{String.to_atom("Elixir." <> last_part), string}
|
||||
{String.to_unsafe_atom("Elixir." <> last_part), string}
|
||||
end
|
||||
|
||||
defp maybe_atomize_calendar(_last_part, string) do
|
||||
@@ -6993,7 +7013,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> :lists.map(&String.to_atom/1, parts)
|
||||
?a -> :lists.map(&String.to_unsafe_atom/1, parts)
|
||||
?c -> :lists.map(&String.to_charlist/1, parts)
|
||||
end
|
||||
|
||||
@@ -7002,7 +7022,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> quote(do: :lists.map(&String.to_atom/1, unquote(parts)))
|
||||
?a -> quote(do: :lists.map(&String.to_unsafe_atom/1, unquote(parts)))
|
||||
?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts)))
|
||||
end
|
||||
end
|
||||
@@ -7036,7 +7056,7 @@ defmodule Kernel do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, #{exp} is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
||||
@@ -49,7 +49,7 @@ defmodule Kernel.CLI do
|
||||
|
||||
@doc """
|
||||
Runs the given function by catching any failure
|
||||
and printing them to stdout. `at_exit` hooks are
|
||||
and printing them to stderr. `at_exit` hooks are
|
||||
also invoked before exiting.
|
||||
|
||||
This function is used by Elixir's CLI and also
|
||||
@@ -99,9 +99,14 @@ defmodule Kernel.CLI do
|
||||
Shared helper for error formatting on CLI tools.
|
||||
"""
|
||||
def format_error(kind, reason, stacktrace) do
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
[banner, rest]
|
||||
end
|
||||
|
||||
defp format_error_parts(kind, reason, stacktrace) do
|
||||
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
|
||||
|
||||
iodata =
|
||||
banner =
|
||||
case blamed do
|
||||
%FunctionClauseError{} ->
|
||||
formatted = Exception.format_banner(kind, reason, stacktrace)
|
||||
@@ -112,7 +117,7 @@ defmodule Kernel.CLI do
|
||||
Exception.format_banner(kind, blamed, stacktrace)
|
||||
end
|
||||
|
||||
[iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
|
||||
{banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -179,7 +184,8 @@ defmodule Kernel.CLI do
|
||||
## Error handling
|
||||
|
||||
defp print_error(kind, reason, stacktrace) do
|
||||
IO.write(:stderr, format_error(kind, reason, stacktrace))
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest])
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
|
||||
@@ -200,7 +206,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir, :elixir_aliases, :elixir_clauses, :elixir_compiler, :elixir_def] ++
|
||||
[:elixir_def, :elixir_dispatch, :elixir_expand, :elixir_lexical] ++
|
||||
[:elixir_dispatch, :elixir_expand, :elixir_lexical] ++
|
||||
[:elixir_map, :elixir_module] ++
|
||||
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
|
||||
[Kernel.ErrorHandler, Module.ParallelChecker]
|
||||
@@ -338,7 +344,7 @@ defmodule Kernel.CLI do
|
||||
parse_argv(t, %{config | verbose_compile: true})
|
||||
end
|
||||
|
||||
defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
|
||||
defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do
|
||||
parse_argv(t, %{config | profile: :time})
|
||||
end
|
||||
|
||||
@@ -427,7 +433,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do
|
||||
node = List.to_atom(node)
|
||||
node = List.to_unsafe_atom(node)
|
||||
|
||||
# Explicitly connect the node in case the rpc node was started with --sname/--name undefined.
|
||||
_ = :net_kernel.connect_node(node)
|
||||
|
||||
@@ -23,7 +23,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate a require
|
||||
must be warned if unused.
|
||||
that must be warned if unused.
|
||||
"""
|
||||
def warn_require(pid, meta, module, alias) do
|
||||
:gen_server.cast(pid, {:warn_require, module, meta, alias})
|
||||
@@ -32,7 +32,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an alias
|
||||
must be warned if unused.
|
||||
that must be warned if unused.
|
||||
"""
|
||||
def warn_alias(pid, meta, alias, module) do
|
||||
:gen_server.cast(pid, {:warn_alias, alias, meta})
|
||||
@@ -41,7 +41,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an import
|
||||
must be warned if unused.
|
||||
that must be warned if unused.
|
||||
"""
|
||||
def warn_import(pid, module) do
|
||||
:gen_server.cast(pid, {:warn_import, module})
|
||||
|
||||
@@ -25,9 +25,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
|
||||
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
|
||||
each_module: (Path.t(), module(), binary() -> term()),
|
||||
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
|
||||
{:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
each_cycle: (-> {:compile, [Path.t()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [{module(), Path.t()}], [Code.diagnostic(:warning)]}),
|
||||
long_compilation_threshold: pos_integer(),
|
||||
long_verification_threshold: pos_integer(),
|
||||
verification: boolean(),
|
||||
@@ -35,7 +34,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
dest: Path.t(),
|
||||
beam_timestamp: term(),
|
||||
return_diagnostics: boolean(),
|
||||
max_concurrency: pos_integer()
|
||||
max_concurrency: pos_integer(),
|
||||
purge_compiler_modules: boolean()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
@@ -100,6 +100,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
This function allows a developer to perform such tasks.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@spec pmap(Enumerable.t(input), (input -> output)) :: [output]
|
||||
when input: term, output: term
|
||||
def pmap(collection, fun) when is_function(fun, 1) do
|
||||
ref = make_ref()
|
||||
|
||||
@@ -171,25 +173,25 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:each_long_verification` (since v1.19.0) - for each file that takes more
|
||||
than a given timeout (see the `:long_verification_threshold` option) to
|
||||
compile, invoke this callback passing the module as its argument (and
|
||||
verify, invoke this callback passing the module as its argument (and
|
||||
optionally the PID of the process verifying the module)
|
||||
|
||||
* `:each_module` - for each module compiled, invokes the callback passing
|
||||
the file, module and the module bytecode
|
||||
|
||||
* `:each_cycle` - after the given files are compiled, invokes this function
|
||||
that should return the following values:
|
||||
* `:each_cycle` - invoked after each compilation cycle and should return one
|
||||
of the following values:
|
||||
* `{:compile, modules, warnings}` - to continue compilation with a list of
|
||||
further modules to compile
|
||||
further module files to compile
|
||||
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
|
||||
of modules because dependent modules have changed
|
||||
of `{module, path}` pairs because dependent modules have changed
|
||||
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) to check for files
|
||||
taking too long to compile. For each file that exceeds the threshold, the
|
||||
`:each_long_compilation` callback is invoked. Defaults to `10` seconds.
|
||||
|
||||
* `:long_verification_threshold` (since v1.19.0) - the timeout (in seconds) to
|
||||
check for modules taking too long to compile. For each module that exceeds the
|
||||
check for modules taking too long to verify. For each module that exceeds the
|
||||
threshold, the `:each_long_verification` callback is invoked. Defaults to
|
||||
`10` seconds.
|
||||
|
||||
@@ -197,8 +199,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
deprecation warnings, and type checking should run. Defaults to `true`.
|
||||
We recommend disabling it only for debugging purposes.
|
||||
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
|
||||
each module type check, and group pass checker
|
||||
|
||||
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
|
||||
after compilation (see `Code.purge_compiler_modules/0`)
|
||||
|
||||
* `:dest` - the destination directory for the BEAM files. When using `compile/2`,
|
||||
this information is only used to properly annotate the BEAM files before
|
||||
@@ -209,7 +214,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
This option must be set to true, except for backward compatibility reasons.
|
||||
|
||||
* `:max_concurrency` - the maximum number of files to compile in parallel.
|
||||
Setting this option to 1 will compile files sequentially.
|
||||
@@ -266,7 +271,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
This option must be set to true, except for backward compatibility reasons.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@@ -336,7 +341,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, %{}, %{}, [], %{}, [], [], %{
|
||||
@@ -353,7 +365,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)
|
||||
@@ -448,7 +461,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
modules = write_module_binaries(result, state.output, state)
|
||||
profile(state, "after compile callback", state.after_compile)
|
||||
|
||||
runtime_warnings =
|
||||
{runtime_warnings, errors} =
|
||||
if state.verification? do
|
||||
profile(
|
||||
state,
|
||||
@@ -459,11 +472,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}
|
||||
@@ -570,11 +591,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} ->
|
||||
@@ -821,7 +842,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)
|
||||
|
||||
@@ -879,8 +900,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} ->
|
||||
|
||||
@@ -768,8 +768,8 @@ defmodule Kernel.SpecialForms do
|
||||
It is used in typespecs to specify the type of a variable,
|
||||
function or of a type itself:
|
||||
|
||||
@type number :: integer | float
|
||||
@spec add(number, number) :: number
|
||||
@type num :: integer | float
|
||||
@spec add(num, num) :: num
|
||||
|
||||
It may also be used in bit strings to specify the type
|
||||
of a given bit segment:
|
||||
@@ -1333,9 +1333,9 @@ defmodule Kernel.SpecialForms do
|
||||
sum(1, value, 3)
|
||||
end
|
||||
|
||||
|
||||
Which the argument for the `:sum` function call is not the
|
||||
expected result:
|
||||
However, the code above does not work as expected, because this
|
||||
injects the representation of the `value` variable,
|
||||
not its contents:
|
||||
|
||||
{:sum, [], [1, {:value, [], Elixir}, 3]}
|
||||
|
||||
@@ -2077,9 +2077,9 @@ defmodule Kernel.SpecialForms do
|
||||
iex> try do
|
||||
...> 1 / 0
|
||||
...> rescue
|
||||
...> x in [ArithmeticError] -> [:rescued, is_exception(x)]
|
||||
...> x in [ArithmeticError] -> {:rescued, Exception.message(x)}
|
||||
...> end
|
||||
[:rescued, true]
|
||||
{:rescued, "bad argument in arithmetic expression"}
|
||||
|
||||
Rescue different errors with separate clauses:
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -676,6 +686,13 @@ defmodule Kernel.Typespec do
|
||||
{{:type, location(meta), :range, [left, right]}, state}
|
||||
end
|
||||
|
||||
defp typespec({:..//, _meta, [_first, _last, _step]} = range, _vars, caller, _state) do
|
||||
compile_error(
|
||||
caller,
|
||||
"ranges with steps are not supported in typespecs, got: #{Macro.to_string(range)}"
|
||||
)
|
||||
end
|
||||
|
||||
# Handle special forms
|
||||
defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do
|
||||
typespec(caller.module, vars, caller, state)
|
||||
@@ -850,29 +867,29 @@ defmodule Kernel.Typespec do
|
||||
{{:type, location(meta), :nonempty_string, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
defp typespec({type, meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
if type == :char_list do
|
||||
warning = "the char_list() type is deprecated, use charlist()"
|
||||
IO.warn(warning, caller)
|
||||
end
|
||||
|
||||
typespec(quote(do: :elixir.charlist()), vars, caller, state)
|
||||
remote_typespec(:charlist, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state)
|
||||
defp typespec({:nonempty_charlist, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:nonempty_charlist, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:struct, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.struct()), vars, caller, state)
|
||||
defp typespec({:struct, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:struct, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state)
|
||||
defp typespec({:as_boolean, meta, [arg]}, vars, caller, state) do
|
||||
remote_typespec(:as_boolean, meta, [arg], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state)
|
||||
defp typespec({:keyword, meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
remote_typespec(:keyword, meta, args, vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:fun, meta, args}, vars, caller, state) do
|
||||
@@ -997,6 +1014,10 @@ defmodule Kernel.Typespec do
|
||||
{{:remote_type, location(meta), [remote, name, args]}, state}
|
||||
end
|
||||
|
||||
defp remote_typespec(name, meta, args, vars, caller, state) do
|
||||
typespec({{:., meta, [:elixir, name]}, meta, args}, vars, caller, state)
|
||||
end
|
||||
|
||||
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
|
||||
defp collect_union(v), do: [v]
|
||||
|
||||
|
||||
@@ -162,16 +162,8 @@ defmodule Kernel.Utils do
|
||||
# TODO: Make it raise on v2.0
|
||||
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
|
||||
|
||||
foreach = fn
|
||||
key when is_atom(key) ->
|
||||
:ok
|
||||
|
||||
key ->
|
||||
raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect(key)}"
|
||||
end
|
||||
|
||||
:lists.foreach(foreach, enforce_keys)
|
||||
struct = :maps.from_list([__struct__: module] ++ fields)
|
||||
field_map = :maps.from_list(fields)
|
||||
struct = :maps.put(:__struct__, module, field_map)
|
||||
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
|
||||
|
||||
body =
|
||||
@@ -215,7 +207,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
end
|
||||
|
||||
case enforce_keys -- :maps.keys(struct) do
|
||||
case enforce_keys -- :maps.keys(field_map) do
|
||||
[] ->
|
||||
mapper = fn {key, val} ->
|
||||
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
|
||||
@@ -225,10 +217,9 @@ defmodule Kernel.Utils do
|
||||
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
|
||||
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
|
||||
|
||||
error_keys ->
|
||||
invalid_keys ->
|
||||
raise ArgumentError,
|
||||
"@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||||
"#{inspect(fields)}"
|
||||
"unknown or duplicate keys given to @enforce_keys, got: #{inspect(invalid_keys)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -266,7 +257,7 @@ defmodule Kernel.Utils do
|
||||
module.exception([])
|
||||
end
|
||||
|
||||
def raise(%_{__exception__: true} = exception) do
|
||||
def raise(%_{__exception__: _} = exception) do
|
||||
exception
|
||||
end
|
||||
|
||||
@@ -329,21 +320,27 @@ defmodule Kernel.Utils do
|
||||
|
||||
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
|
||||
def defguard(args, expr, env) do
|
||||
{^args, vars} = extract_refs_from_args(args)
|
||||
env = :elixir_env.with_vars(%{env | context: :guard}, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
{_, vars} = extract_refs_from_args(args)
|
||||
guard_expr = expand_defguard(expr, %{env | context: :guard}, vars)
|
||||
body_expr = expand_defguard(expr, %{env | context: nil}, vars)
|
||||
|
||||
quote do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
unquote(literal_quote(unquote_every_ref(expr, vars), []))
|
||||
unquote(literal_quote(unquote_every_ref(guard_expr, vars), []))
|
||||
|
||||
false ->
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
|
||||
unquote(literal_quote(unquote_refs_once(body_expr, vars, env), generated: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_defguard(expr, env, vars) do
|
||||
env = :elixir_env.with_vars(env, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
expr
|
||||
end
|
||||
|
||||
defp extract_refs_from_args(args) do
|
||||
Macro.postwalk(args, [], fn
|
||||
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
|
||||
@@ -369,7 +366,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) ->
|
||||
@@ -382,7 +379,7 @@ defmodule Kernel.Utils do
|
||||
{new_var, acc}
|
||||
|
||||
%{} ->
|
||||
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
generated = String.to_unsafe_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
new_var = Macro.unique_var(generated, module)
|
||||
{new_var, Map.put(acc, pair, {new_var, var})}
|
||||
end
|
||||
|
||||
+69
-37
@@ -38,7 +38,7 @@ defmodule Keyword do
|
||||
|
||||
## Duplicate keys and ordering
|
||||
|
||||
A keyword may have duplicate keys so it is not strictly a key-value
|
||||
A keyword list may have duplicate keys so it is not strictly a key-value
|
||||
data type. However, most of the functions in this module work on a
|
||||
key-value structure and behave similar to the functions you would
|
||||
find in the `Map` module. For example, `Keyword.get/3` will get the first
|
||||
@@ -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 """
|
||||
@@ -260,38 +269,60 @@ defmodule Keyword do
|
||||
@doc since: "1.13.0"
|
||||
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
|
||||
{:ok, keyword()} | {:error, [atom]}
|
||||
def validate([], values) when is_list(values), do: {:ok, move_pairs!(values, [])}
|
||||
|
||||
def validate(keyword, values) when is_list(keyword) and is_list(values) do
|
||||
validate(keyword, values, [], [], [])
|
||||
validate_merge(keyword, values, [], keyword)
|
||||
end
|
||||
|
||||
defp validate([{key, _} = pair | keyword], values1, values2, acc, bad_keys) when is_atom(key) do
|
||||
case find_key!(key, values1, values2) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
defp validate_merge([], values, values_pre, original),
|
||||
do: {:ok, move_pairs!(values, move_pairs!(values_pre, original))}
|
||||
|
||||
:error ->
|
||||
case find_key!(key, values2, values1) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, [pair | acc], bad_keys)
|
||||
|
||||
:error ->
|
||||
validate(keyword, values1, values2, acc, [key | bad_keys])
|
||||
end
|
||||
defp validate_merge([{key, _} = pair | keyword], [head | tail], values_pre, original)
|
||||
when is_atom(key) do
|
||||
case head do
|
||||
^key -> validate_merge(keyword, tail, values_pre, original)
|
||||
{^key, _} -> validate_merge(keyword, tail, values_pre, original)
|
||||
_ -> validate_merge([pair | keyword], tail, [head | values_pre], original)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], values1, values2, acc, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
|
||||
defp validate_merge([{key, _} | keyword], [], values_pre, original) when is_atom(key) do
|
||||
case find_key!(key, values_pre, []) do
|
||||
{new_values, new_values_pre} ->
|
||||
validate_merge(keyword, new_values, new_values_pre, original)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values_pre, [key])
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], _values1, _values2, _acc, bad_keys) do
|
||||
{:error, bad_keys}
|
||||
defp validate_merge([pair | _], _, _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
)
|
||||
|
||||
defp validate_fallback([{key, _} | keyword], values, bad_keys)
|
||||
when is_atom(key) do
|
||||
case find_key!(key, values, []) do
|
||||
{rest, acc} ->
|
||||
validate_fallback(keyword, rest ++ acc, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values, [key | bad_keys])
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([pair | _], _values1, _values2, _acc, []) do
|
||||
raise ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
end
|
||||
defp validate_fallback([], _, bad), do: {:error, bad}
|
||||
|
||||
defp validate_fallback([p | _], _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(p)}"
|
||||
)
|
||||
|
||||
defp find_key!(key, [key | rest], acc), do: {rest, acc}
|
||||
defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc}
|
||||
@@ -486,7 +517,7 @@ defmodule Keyword do
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{current, :lists.reverse(acc, t)}
|
||||
{current, :lists.reverse(acc, delete(t, key))}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
@@ -552,7 +583,7 @@ defmodule Keyword do
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{value, :lists.reverse(acc, t)}
|
||||
{value, :lists.reverse(acc, delete(t, key))}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
@@ -956,7 +987,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])
|
||||
@@ -1292,7 +1323,8 @@ defmodule Keyword do
|
||||
@doc """
|
||||
Drops the given `keys` from the keyword list.
|
||||
|
||||
Removes duplicate keys from the new keyword list.
|
||||
If a key occurs multiple times, all of its entries are removed when that key
|
||||
is included in `keys`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1333,9 +1365,9 @@ defmodule Keyword do
|
||||
"""
|
||||
@spec pop(t, key, default) :: {value | default, t}
|
||||
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {default, keywords}
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {default, keywords}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1360,9 +1392,9 @@ defmodule Keyword do
|
||||
@doc since: "1.10.0"
|
||||
@spec pop!(t, key) :: {value, t}
|
||||
def pop!(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> raise KeyError, key: key, term: keywords
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> raise KeyError, key: key, term: keywords
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1402,7 +1434,7 @@ defmodule Keyword do
|
||||
do: {values, acc}
|
||||
|
||||
@doc """
|
||||
Lazily returns and removes all values associated with `key` in the keyword list.
|
||||
Lazily returns the first value for `key` and removes all associated entries in the keyword list.
|
||||
|
||||
This is useful if the default value is very expensive to calculate or
|
||||
generally difficult to set up and tear down again.
|
||||
@@ -1425,9 +1457,9 @@ defmodule Keyword do
|
||||
@spec pop_lazy(t, key, (-> value)) :: {value, t}
|
||||
def pop_lazy(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {fun.(), keywords}
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {fun.(), keywords}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+152
-38
@@ -102,7 +102,7 @@ defmodule List do
|
||||
Even though the representation changed, the raw data does remain a list of
|
||||
integers, which can be handled as such:
|
||||
|
||||
iex> inspect(~c"abc", charlists: :as_list)
|
||||
iex> inspect(~c"abc", charlists: :as_lists)
|
||||
"[97, 98, 99]"
|
||||
iex> Enum.map(~c"abc", fn num -> 1000 + num end)
|
||||
[1097, 1098, 1099]
|
||||
@@ -298,6 +298,30 @@ 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
|
||||
|
||||
"""
|
||||
@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.
|
||||
|
||||
@@ -326,6 +350,31 @@ 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
|
||||
|
||||
"""
|
||||
@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
|
||||
@@ -380,7 +429,7 @@ defmodule List do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec keyfind!([tuple], any, non_neg_integer) :: any
|
||||
@spec keyfind!([tuple], any, non_neg_integer) :: tuple
|
||||
def keyfind!(list, key, position) when is_integer(position) do
|
||||
:lists.keyfind(key, position + 1, list) ||
|
||||
raise KeyError,
|
||||
@@ -465,8 +514,8 @@ defmodule List do
|
||||
|
||||
As in `Enum.sort/2`, avoid using the default sorting function to sort
|
||||
structs, as by default it performs structural comparison instead of a
|
||||
semantic one. In such cases, you shall pass a sorting function as third
|
||||
element or any module that implements a `compare/2` function. For example,
|
||||
semantic one. In such cases, you shall pass a sorting function as the third
|
||||
argument or any module that implements a `compare/2` function. For example,
|
||||
if you have tuples with user names and their birthday, and you want to
|
||||
sort on their birthday, in both ascending and descending order, you should
|
||||
do:
|
||||
@@ -606,7 +655,7 @@ defmodule List do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps `term` in a list if this is not list.
|
||||
Wraps `term` in a list if it is not a list.
|
||||
|
||||
If `term` is already a list, it returns the list.
|
||||
If `term` is `nil`, it returns an empty list.
|
||||
@@ -905,9 +954,12 @@ defmodule List do
|
||||
@spec pop_at(list, integer, any) :: {any, list}
|
||||
def pop_at(list, index, default \\ nil) when is_integer(index) do
|
||||
if index < 0 do
|
||||
do_pop_at(list, length(list) + index, default, [])
|
||||
case length(list) + index do
|
||||
index when index < 0 -> {default, list}
|
||||
index -> do_pop_at(list, index, default, [], list)
|
||||
end
|
||||
else
|
||||
do_pop_at(list, index, default, [])
|
||||
do_pop_at(list, index, default, [], list)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -969,31 +1021,51 @@ defmodule List do
|
||||
:lists.suffix(suffix, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an atom.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_atom(~c"Elixir")
|
||||
:Elixir
|
||||
|
||||
iex> List.to_atom(~c"🌢 Elixir")
|
||||
:"🌢 Elixir"
|
||||
|
||||
"""
|
||||
@spec to_atom(charlist) :: atom
|
||||
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
|
||||
def to_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an existing atom or creates a new one.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point.
|
||||
|
||||
> #### Dynamic Atom Creation {: .warning}
|
||||
>
|
||||
> This function creates atoms dynamically and atoms are
|
||||
> not garbage-collected. Therefore, `charlist` should not be an
|
||||
> untrusted value, such as input received from a socket or during
|
||||
> a web request. Consider using `to_existing_atom/1` instead.
|
||||
|
||||
By default, the maximum number of atoms is `1_048_576`. This limit
|
||||
can be raised or lowered using the VM option `+t`.
|
||||
|
||||
The maximum atom size is 255 Unicode code points.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_unsafe_atom(~c"Elixir")
|
||||
:Elixir
|
||||
|
||||
iex> List.to_unsafe_atom(~c"🌢 Elixir")
|
||||
:"🌢 Elixir"
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_unsafe_atom(charlist) :: atom
|
||||
def to_unsafe_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an existing atom.
|
||||
|
||||
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point. Raises an `ArgumentError` if the atom does not exist.
|
||||
|
||||
@@ -1024,6 +1096,47 @@ defmodule List do
|
||||
:erlang.list_to_existing_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to one of the `allowed_atoms` or raises.
|
||||
|
||||
Raises an `ArgumentError` if the atom either does not exist or is not within
|
||||
the existing list.
|
||||
|
||||
This should be preferred to `to_existing_atom/1` if the list is known upfront,
|
||||
since there is no risk that the atom has not been loaded.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_existing_atom(~c"foo", [:foo, :bar])
|
||||
:foo
|
||||
|
||||
iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
|
||||
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
|
||||
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
|
||||
atom = :erlang.list_to_existing_atom(charlist)
|
||||
|
||||
if atom not in allowed_atoms do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
atom
|
||||
end
|
||||
|
||||
# used just to have a less cryptic stacktrace and consistent error
|
||||
@doc false
|
||||
def __to_existing_atom__(charlist, allowed_atoms) do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
defp to_existing_atom_unexpected(charlist, allowed_atoms) do
|
||||
raise ArgumentError,
|
||||
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the float whose text representation is `charlist`.
|
||||
|
||||
@@ -1035,7 +1148,7 @@ defmodule List do
|
||||
2.2017764
|
||||
|
||||
"""
|
||||
@spec to_float(charlist) :: float
|
||||
@spec to_float(nonempty_charlist) :: float
|
||||
def to_float(charlist) do
|
||||
:erlang.list_to_float(charlist)
|
||||
end
|
||||
@@ -1051,7 +1164,7 @@ defmodule List do
|
||||
123
|
||||
|
||||
"""
|
||||
@spec to_integer(charlist) :: integer
|
||||
@spec to_integer(nonempty_charlist) :: integer
|
||||
def to_integer(charlist) do
|
||||
:erlang.list_to_integer(charlist)
|
||||
end
|
||||
@@ -1069,7 +1182,7 @@ defmodule List do
|
||||
1023
|
||||
|
||||
"""
|
||||
@spec to_integer(charlist, 2..36) :: integer
|
||||
@spec to_integer(nonempty_charlist, 2..36) :: integer
|
||||
def to_integer(charlist, base) do
|
||||
:erlang.list_to_integer(charlist, base)
|
||||
end
|
||||
@@ -1138,7 +1251,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list)}
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
"""
|
||||
else
|
||||
result when is_binary(result) ->
|
||||
@@ -1190,7 +1303,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list)}
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
"""
|
||||
else
|
||||
result when is_list(result) ->
|
||||
@@ -1255,13 +1368,13 @@ defmodule List do
|
||||
|
||||
defp myers_difference_with_diff_script(list1, list2, diff_script) do
|
||||
path = {0, list1, list2, []}
|
||||
find_script(0, length(list1) + length(list2), [path], diff_script)
|
||||
find_script(0, [path], diff_script)
|
||||
end
|
||||
|
||||
defp find_script(envelope, max, paths, diff_script) do
|
||||
defp find_script(envelope, paths, diff_script) do
|
||||
case each_diagonal(-envelope, envelope, paths, [], diff_script) do
|
||||
{:done, edits} -> compact_reverse(edits, [])
|
||||
{:next, paths} -> find_script(envelope + 1, max, paths, diff_script)
|
||||
{:next, paths} -> find_script(envelope + 1, paths, diff_script)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1406,15 +1519,16 @@ defmodule List do
|
||||
|
||||
# pop_at
|
||||
|
||||
defp do_pop_at([], _index, default, acc) do
|
||||
{default, :lists.reverse(acc)}
|
||||
# The original list is returned when the index is out of bounds
|
||||
defp do_pop_at([], _index, default, _acc, original) do
|
||||
{default, original}
|
||||
end
|
||||
|
||||
defp do_pop_at([head | tail], 0, _default, acc) do
|
||||
defp do_pop_at([head | tail], 0, _default, acc, _original) do
|
||||
{head, :lists.reverse(acc, tail)}
|
||||
end
|
||||
|
||||
defp do_pop_at([head | tail], index, default, acc) do
|
||||
do_pop_at(tail, index - 1, default, [head | acc])
|
||||
defp do_pop_at([head | tail], index, default, acc, original) do
|
||||
do_pop_at(tail, index - 1, default, [head | acc], original)
|
||||
end
|
||||
end
|
||||
|
||||
+162
-64
@@ -166,7 +166,7 @@ defmodule Macro do
|
||||
of a `__block__` or the right side of `->`. The last expression of the
|
||||
block does not have metadata if it is not followed by an end of line
|
||||
character (either a newline or `;`). This entry may appear multiple times
|
||||
in the same metadata if the expression is surround by parens
|
||||
in the same metadata if the expression is surrounded by parens
|
||||
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword.
|
||||
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
|
||||
@@ -235,7 +235,7 @@ defmodule Macro do
|
||||
`div/2` function, so that the AST for that function will become `{:div, [],
|
||||
[100, 5]}` (`div(100, 5)`).
|
||||
"""
|
||||
@spec unpipe(t()) :: [t()]
|
||||
@spec unpipe(t()) :: [{t(), non_neg_integer}]
|
||||
def unpipe(expr) do
|
||||
:lists.reverse(unpipe(expr, []))
|
||||
end
|
||||
@@ -518,7 +518,7 @@ defmodule Macro do
|
||||
@doc since: "1.11.3"
|
||||
@spec generate_unique_arguments(0, context :: atom) :: []
|
||||
@spec generate_unique_arguments(pos_integer, context) ::
|
||||
[{atom, [counter: integer], context}, ...]
|
||||
[{atom, metadata(), context}, ...]
|
||||
when context: atom
|
||||
def generate_unique_arguments(amount, context),
|
||||
do: generate_arguments(amount, context, &unique_var/2)
|
||||
@@ -527,7 +527,7 @@ defmodule Macro do
|
||||
|
||||
defp generate_arguments(amount, context, fun)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
for id <- 1..amount, do: fun.(String.to_unsafe_atom("arg" <> Integer.to_string(id)), context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -576,7 +576,7 @@ defmodule Macro do
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec unique_var(var, context) :: {var, [counter: integer], context}
|
||||
@spec unique_var(var, context) :: {var, metadata(), context}
|
||||
when var: atom, context: atom
|
||||
def unique_var(var, context) when is_atom(var) and is_atom(context) do
|
||||
{var, [counter: :elixir_module.next_counter(context)], context}
|
||||
@@ -690,7 +690,39 @@ defmodule Macro do
|
||||
"""
|
||||
@spec prewalk(t, (t -> t)) :: t
|
||||
def prewalk(ast, fun) when is_function(fun, 1) do
|
||||
elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
do_prewalk(fun.(ast), fun)
|
||||
end
|
||||
|
||||
# Mirrors do_traverse/4 with an always-pre fun and no accumulator,
|
||||
# avoiding the wrapper closures and tuple threading of traverse/4.
|
||||
# Each clause dispatches on the already-transformed node, so fun's
|
||||
# rewrites are descended into, exactly as in traverse/4.
|
||||
defp do_prewalk({form, meta, args}, fun) when is_atom(form) do
|
||||
{form, meta, do_prewalk_args(args, fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk({form, meta, args}, fun) do
|
||||
form = do_prewalk(fun.(form), fun)
|
||||
{form, meta, do_prewalk_args(args, fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk({left, right}, fun) do
|
||||
left = do_prewalk(fun.(left), fun)
|
||||
{left, do_prewalk(fun.(right), fun)}
|
||||
end
|
||||
|
||||
defp do_prewalk(list, fun) when is_list(list) do
|
||||
do_prewalk_args(list, fun)
|
||||
end
|
||||
|
||||
defp do_prewalk(x, _fun) do
|
||||
x
|
||||
end
|
||||
|
||||
defp do_prewalk_args(args, _fun) when is_atom(args), do: args
|
||||
|
||||
defp do_prewalk_args(args, fun) when is_list(args) do
|
||||
:lists.map(fn x -> do_prewalk(fun.(x), fun) end, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -728,11 +760,41 @@ defmodule Macro do
|
||||
"""
|
||||
@spec postwalk(t, (t -> t)) :: t
|
||||
def postwalk(ast, fun) when is_function(fun, 1) do
|
||||
elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
||||
do_postwalk(ast, fun)
|
||||
end
|
||||
|
||||
# Mirrors do_traverse/4 with an always-post fun and no accumulator,
|
||||
# avoiding the wrapper closures and tuple threading of traverse/4
|
||||
defp do_postwalk({form, meta, args}, fun) when is_atom(form) do
|
||||
fun.({form, meta, do_postwalk_args(args, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk({form, meta, args}, fun) do
|
||||
form = do_postwalk(form, fun)
|
||||
fun.({form, meta, do_postwalk_args(args, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk({left, right}, fun) do
|
||||
left = do_postwalk(left, fun)
|
||||
fun.({left, do_postwalk(right, fun)})
|
||||
end
|
||||
|
||||
defp do_postwalk(list, fun) when is_list(list) do
|
||||
fun.(do_postwalk_args(list, fun))
|
||||
end
|
||||
|
||||
defp do_postwalk(x, fun) do
|
||||
fun.(x)
|
||||
end
|
||||
|
||||
defp do_postwalk_args(args, _fun) when is_atom(args), do: args
|
||||
|
||||
defp do_postwalk_args(args, fun) when is_list(args) do
|
||||
:lists.map(fn x -> do_postwalk(x, fun) end, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This functions behaves like `prewalk/3`, but performs a depth-first,
|
||||
This function behaves like `prewalk/3`, but performs a depth-first,
|
||||
post-order traversal of quoted expressions using an accumulator.
|
||||
"""
|
||||
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
|
||||
@@ -940,7 +1002,7 @@ defmodule Macro do
|
||||
|
||||
This is useful when a struct needs to be expanded at
|
||||
compilation time and the struct being expanded may or may
|
||||
not have been compiled (including structs in the defined
|
||||
not have been compiled (including structs defined
|
||||
under the module being compiled). For compiled modules,
|
||||
it will invoke `module.__info__(:struct)`.
|
||||
|
||||
@@ -971,9 +1033,15 @@ defmodule Macro do
|
||||
}
|
||||
]
|
||||
def struct_info!(module, env) when is_atom(module) do
|
||||
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
|
||||
{:ok, info} -> info
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
meta = [line: env.line]
|
||||
|
||||
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do
|
||||
{:ok, info} ->
|
||||
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
|
||||
info
|
||||
|
||||
{:error, desc} ->
|
||||
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1028,7 +1096,7 @@ defmodule Macro do
|
||||
defp find_invalid(other), do: {:error, other}
|
||||
|
||||
@doc """
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
pre-order traversal.
|
||||
|
||||
## Examples
|
||||
@@ -1086,7 +1154,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
Returns an enumerable that traverses the `ast` in depth-first,
|
||||
post-order traversal.
|
||||
|
||||
## Examples
|
||||
@@ -1189,14 +1257,15 @@ defmodule Macro do
|
||||
The mapping function receives an integer representing the code point
|
||||
of the character it wants to unescape. There are also the special atoms
|
||||
`:newline`, `:unicode`, and `:hex`, which control newline, unicode,
|
||||
and escaping respectively.
|
||||
and escaping respectively, and for which the mapping function must return
|
||||
a boolean.
|
||||
|
||||
Here is the default mapping function implemented by Elixir:
|
||||
|
||||
def unescape_map(:newline), do: true
|
||||
def unescape_map(:unicode), do: true
|
||||
def unescape_map(:hex), do: true
|
||||
def unescape_map(?0), do: ?0
|
||||
def unescape_map(?0), do: 0
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(?b), do: ?\b
|
||||
def unescape_map(?d), do: ?\d
|
||||
@@ -1219,7 +1288,11 @@ defmodule Macro do
|
||||
Macro.unescape_string("example\\n", &unescape_map(&1))
|
||||
|
||||
"""
|
||||
@spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
|
||||
@spec unescape_string(
|
||||
String.t(),
|
||||
(non_neg_integer | :newline | :unicode | :hex ->
|
||||
non_neg_integer | boolean)
|
||||
) :: String.t()
|
||||
def unescape_string(string, map) do
|
||||
:elixir_interpolation.unescape_string(string, map)
|
||||
end
|
||||
@@ -1734,9 +1807,9 @@ defmodule Macro do
|
||||
|
||||
defp kw_blocks_to_string(kw, fun) do
|
||||
Enum.reduce(unquote(kw_keywords), " ", fn x, acc ->
|
||||
case Keyword.has_key?(kw, x) do
|
||||
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
|
||||
false -> acc
|
||||
case Keyword.fetch(kw, x) do
|
||||
{:ok, value} -> acc <> kw_block_to_string(x, value, fun)
|
||||
:error -> acc
|
||||
end
|
||||
end) <> "end"
|
||||
end
|
||||
@@ -1850,6 +1923,7 @@ defmodule Macro do
|
||||
definition compile-time, and you can use `Macro.expand/2`.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@spec compile_apply(module(), atom(), [term()], Macro.Env.t()) :: term()
|
||||
def compile_apply(mod, fun, args, caller) do
|
||||
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
|
||||
Kernel.apply(mod, fun, args)
|
||||
@@ -1974,10 +2048,9 @@ defmodule Macro do
|
||||
|
||||
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
|
||||
when is_atom(atom) and is_atom(field) and env.context != :match do
|
||||
if Map.has_key?(env, field) do
|
||||
{maybe_escape_map(Map.get(env, field)), true}
|
||||
else
|
||||
{original, false}
|
||||
case Map.fetch(env, field) do
|
||||
{:ok, value} -> {maybe_escape_map(value), true}
|
||||
:error -> {original, false}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2115,6 +2188,9 @@ defmodule Macro do
|
||||
def quoted_literal?({:__aliases__, _, args}),
|
||||
do: quoted_literal?(args)
|
||||
|
||||
def quoted_literal?({:__block__, _, [wrapped]}),
|
||||
do: quoted_literal?(wrapped)
|
||||
|
||||
def quoted_literal?({:%, _, [left, right]}),
|
||||
do: quoted_literal?(left) and quoted_literal?(right)
|
||||
|
||||
@@ -2190,7 +2266,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
|
||||
@@ -2494,7 +2570,7 @@ defmodule Macro do
|
||||
|
||||
### As a remote call
|
||||
|
||||
Inspect an atom the function name of a remote call.
|
||||
Inspect an atom as the function name of a remote call.
|
||||
|
||||
iex> Macro.inspect_atom(:remote_call, :foo)
|
||||
"foo"
|
||||
@@ -2615,55 +2691,77 @@ defmodule Macro do
|
||||
:unquoted_operator
|
||||
|
||||
true ->
|
||||
charlist = Atom.to_charlist(atom)
|
||||
|
||||
if valid_alias?(charlist) do
|
||||
:alias
|
||||
else
|
||||
case :elixir_config.identifier_tokenizer().tokenize(charlist) do
|
||||
{kind, _acc, [], _, _, special} ->
|
||||
cond do
|
||||
kind != :identifier or :lists.member(:at, special) ->
|
||||
:not_callable
|
||||
|
||||
# identifier_tokenizer used to return errors for non-nfc, but
|
||||
# now it nfc-normalizes everything. However, lack of nfc is
|
||||
# still a good reason to quote an atom when printing.
|
||||
:lists.member(:nfkc, special) ->
|
||||
:other
|
||||
|
||||
true ->
|
||||
:identifier
|
||||
end
|
||||
|
||||
_ ->
|
||||
:other
|
||||
end
|
||||
end
|
||||
classify_binary(Atom.to_string(atom), atom)
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
|
||||
defp valid_alias?(_other), do: false
|
||||
# ASCII identifiers and aliases are recognized on the binary to avoid building a
|
||||
# charlist and running the (unicode aware) tokenizer, which dominates the cost of
|
||||
# classifying keyword list, map and struct keys.
|
||||
defp classify_binary(<<char, rest::binary>>, atom)
|
||||
when char >= ?a and char <= ?z
|
||||
when char == ?_ do
|
||||
if valid_identifier_rest?(rest), do: :identifier, else: classify_with_tokenizer(atom)
|
||||
end
|
||||
|
||||
defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
|
||||
do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
|
||||
defp classify_binary("Elixir" <> rest, atom) do
|
||||
if valid_alias_piece?(rest), do: :alias, else: classify_with_tokenizer(atom)
|
||||
end
|
||||
|
||||
defp valid_alias_piece?([]), do: true
|
||||
defp valid_alias_piece?(_other), do: false
|
||||
defp classify_binary(_binary, atom), do: classify_with_tokenizer(atom)
|
||||
|
||||
defp trim_leading_while_valid_identifier([char | rest])
|
||||
defp classify_with_tokenizer(atom) do
|
||||
case :elixir_config.identifier_tokenizer().tokenize(Atom.to_charlist(atom)) do
|
||||
{kind, _acc, [], _, _, special} ->
|
||||
cond do
|
||||
kind != :identifier or :lists.member(:at, special) ->
|
||||
:not_callable
|
||||
|
||||
# identifier_tokenizer used to return errors for non-nfc, but
|
||||
# now it nfc-normalizes everything. However, lack of nfc is
|
||||
# still a good reason to quote an atom when printing.
|
||||
:lists.member(:nfkc, special) ->
|
||||
:other
|
||||
|
||||
true ->
|
||||
:identifier
|
||||
end
|
||||
|
||||
_ ->
|
||||
:other
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_identifier_rest?(<<char, rest::binary>>)
|
||||
when char >= ?a and char <= ?z
|
||||
when char >= ?A and char <= ?Z
|
||||
when char >= ?0 and char <= ?9
|
||||
when char == ?_ do
|
||||
trim_leading_while_valid_identifier(rest)
|
||||
valid_identifier_rest?(rest)
|
||||
end
|
||||
|
||||
defp trim_leading_while_valid_identifier(other) do
|
||||
other
|
||||
defp valid_identifier_rest?(<<char>>) when char == ?? when char == ?!, do: true
|
||||
defp valid_identifier_rest?(<<>>), do: true
|
||||
defp valid_identifier_rest?(_other), do: false
|
||||
|
||||
defp valid_alias_piece?(<<?., char, rest::binary>>) when char >= ?A and char <= ?Z,
|
||||
do: valid_alias_piece_rest?(rest)
|
||||
|
||||
defp valid_alias_piece?(<<>>), do: true
|
||||
defp valid_alias_piece?(_other), do: false
|
||||
|
||||
# A helper returning the rest of the binary would build a sub binary per piece,
|
||||
# so branch back into valid_alias_piece?/1 to keep the match context.
|
||||
defp valid_alias_piece_rest?(<<char, rest::binary>>)
|
||||
when char >= ?a and char <= ?z
|
||||
when char >= ?A and char <= ?Z
|
||||
when char >= ?0 and char <= ?9
|
||||
when char == ?_ do
|
||||
valid_alias_piece_rest?(rest)
|
||||
end
|
||||
|
||||
defp valid_alias_piece_rest?(other), do: valid_alias_piece?(other)
|
||||
|
||||
@doc """
|
||||
Default backend for `Kernel.dbg/2`.
|
||||
|
||||
@@ -2693,7 +2791,7 @@ defmodule Macro do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, dbg is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
@@ -2948,6 +3046,8 @@ defmodule Macro do
|
||||
@doc false
|
||||
def __dbg__(to_debug, header, options) do
|
||||
{print_location?, options} = Keyword.pop(options, :print_location, true)
|
||||
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
|
||||
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
|
||||
ansi_enabled? = options[:syntax_colors] != []
|
||||
|
||||
if print_location? and is_binary(header) do
|
||||
@@ -2955,8 +3055,6 @@ defmodule Macro do
|
||||
:ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?))
|
||||
end
|
||||
|
||||
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
|
||||
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
|
||||
{formatted, result} = dbg_format_ast_to_debug(to_debug, options)
|
||||
:ok = IO.write(IO.ANSI.format([formatted, ?\n], ansi_enabled?))
|
||||
result
|
||||
|
||||
+14
-13
@@ -203,23 +203,23 @@ defmodule Macro.Env do
|
||||
Returns a keyword list containing the file and line
|
||||
information as keys.
|
||||
"""
|
||||
@spec location(t) :: keyword
|
||||
@spec location(t) :: [file: file, line: line]
|
||||
def location(env)
|
||||
|
||||
def location(%{__struct__: Macro.Env, file: file, line: line}) do
|
||||
[file: file, line: line]
|
||||
end
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
|
||||
when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
do: Keyword.fetch(aliases, String.to_unsafe_atom("Elixir.#{atom}"))
|
||||
|
||||
@doc """
|
||||
Returns the modules from which the given `{name, arity}` was
|
||||
@@ -234,7 +234,7 @@ defmodule Macro.Env do
|
||||
> This function does not emit compiler tracing events,
|
||||
> which may block the compiler from correctly tracking
|
||||
> dependencies. Use this function for reflection purposes
|
||||
> but to do not use it to expand imports into qualified
|
||||
> but do not use it to expand imports into qualified
|
||||
> calls. Instead, use `expand_import/5`.
|
||||
|
||||
## Examples
|
||||
@@ -345,7 +345,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.require/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForms.require/2` plus:
|
||||
|
||||
* #{trace_option}
|
||||
|
||||
@@ -367,7 +367,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
|
||||
{:ok, t} | {:error, String.t()}
|
||||
def define_require(env, meta, module, opts \\ [])
|
||||
when is_list(meta) and is_atom(module) and is_list(opts) do
|
||||
{trace, opts} = Keyword.pop(opts, :trace, true)
|
||||
@@ -385,7 +386,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.import/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForms.import/2` plus:
|
||||
|
||||
* `:emit_warnings` - emit warnings found when defining imports
|
||||
|
||||
@@ -393,7 +394,7 @@ defmodule Macro.Env do
|
||||
|
||||
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
|
||||
The function will be invoked with `:functions` or `:macros` argument.
|
||||
It has to return a list of `{function, arity}` key value pairs.
|
||||
It has to return a list of `{function, arity}` key-value pairs.
|
||||
If it fails, it defaults to using module metadata based on `module_info/1`.
|
||||
|
||||
## Examples
|
||||
@@ -405,7 +406,7 @@ defmodule Macro.Env do
|
||||
iex> Macro.Env.lookup_import(env, {:flatten, 1})
|
||||
[{:function, List}]
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.import/2`:
|
||||
It accepts the same options as `Kernel.SpecialForms.import/2`:
|
||||
|
||||
iex> env = __ENV__
|
||||
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
|
||||
@@ -447,7 +448,7 @@ defmodule Macro.Env do
|
||||
|
||||
## Additional options
|
||||
|
||||
It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
|
||||
It accepts the same options as `Kernel.SpecialForms.alias/2` plus:
|
||||
|
||||
* #{trace_option}
|
||||
|
||||
@@ -699,7 +700,7 @@ defmodule Macro.Env do
|
||||
@doc """
|
||||
Returns the environment stacktrace.
|
||||
"""
|
||||
@spec stacktrace(t) :: list
|
||||
@spec stacktrace(t) :: [{module, atom, arity, keyword}]
|
||||
def stacktrace(%{__struct__: Macro.Env} = env) do
|
||||
cond do
|
||||
is_nil(env.module) ->
|
||||
|
||||
+14
-5
@@ -201,7 +201,7 @@ defmodule Map do
|
||||
%{}
|
||||
|
||||
"""
|
||||
@spec new :: map
|
||||
@spec new :: %{}
|
||||
def new, do: %{}
|
||||
|
||||
@doc """
|
||||
@@ -547,6 +547,8 @@ defmodule Map do
|
||||
:erlang.error({:badmap, non_map})
|
||||
end
|
||||
|
||||
defp take([], _map, []), do: %{}
|
||||
|
||||
defp take([], _map, acc) do
|
||||
:maps.from_list(acc)
|
||||
end
|
||||
@@ -1107,7 +1109,7 @@ defmodule Map do
|
||||
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
|
||||
false
|
||||
|
||||
Comparison between keys and values is done with `===/3`,
|
||||
Comparison between keys and values is done with `===/2`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Map.equal?(%{a: 1.0}, %{a: 1})
|
||||
@@ -1136,9 +1138,9 @@ defmodule Map do
|
||||
> #### Performance considerations {: .tip}
|
||||
>
|
||||
> If you find yourself doing multiple calls to `Map.filter/2`
|
||||
> and `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
|
||||
> a map at the end using `Map.new/1`.
|
||||
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
|
||||
> a map at the end using `Map.new/1` or `Map.new/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1170,6 +1172,13 @@ defmodule Map do
|
||||
|
||||
See also `filter/2`.
|
||||
|
||||
> #### Performance considerations {: .tip}
|
||||
>
|
||||
> If you find yourself doing multiple calls to `Map.filter/2`
|
||||
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
|
||||
> a map at the end using `Map.new/1` or `Map.new/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
|
||||
|
||||
+23
-17
@@ -51,13 +51,23 @@ defmodule MapSet do
|
||||
that they share many properties, including logarithmic time complexity. Erlang
|
||||
`:sets` (version 2) are implemented on top of maps, so see the documentation
|
||||
for `Map` for more information on its execution time complexity.
|
||||
|
||||
> #### Dialyzer opaqueness warnings {: .warning}
|
||||
>
|
||||
> `MapSet` internally relies on the `:sets` module which uses
|
||||
> opaque types. This might cause Dialyzer to report opaqueness violations.
|
||||
> These can be silenced by setting the following module attribute:
|
||||
>
|
||||
> @dialyzer :no_opaque
|
||||
|
||||
"""
|
||||
|
||||
@type value :: term
|
||||
|
||||
# We don't use opaque because MapSets can be inlined,
|
||||
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
|
||||
# either via module attributes or by the compiler.
|
||||
@typep internal(value) :: :sets.set(value)
|
||||
# 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)
|
||||
@@ -170,18 +180,14 @@ defmodule MapSet do
|
||||
@doc since: "1.14.0"
|
||||
@spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
|
||||
def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do
|
||||
{small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1}
|
||||
|
||||
disjointer_fun = fn elem, {small, acc} ->
|
||||
if :sets.is_element(elem, small) do
|
||||
{:sets.del_element(elem, small), acc}
|
||||
map =
|
||||
if :sets.is_disjoint(set1, set2) do
|
||||
:sets.union(set1, set2)
|
||||
else
|
||||
{small, [elem | acc]}
|
||||
:sets.union(:sets.subtract(set1, set2), :sets.subtract(set2, set1))
|
||||
end
|
||||
end
|
||||
|
||||
{new_small, list} = :sets.fold(disjointer_fun, {small, []}, large)
|
||||
%{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))}
|
||||
%{map_set1 | map: map}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -204,7 +210,7 @@ defmodule MapSet do
|
||||
Checks if two sets are equal.
|
||||
|
||||
The comparison between elements is done using `===/2`,
|
||||
which a set with `1` is not equivalent to a set with
|
||||
which means a set with `1` is not equivalent to a set with
|
||||
`1.0`.
|
||||
|
||||
## Examples
|
||||
@@ -343,7 +349,7 @@ defmodule MapSet do
|
||||
> If you find yourself doing multiple calls to `MapSet.filter/2`
|
||||
> and `MapSet.reject/2` in a pipeline, it is likely more efficient
|
||||
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
|
||||
> a map at the end using `MapSet.new/1`.
|
||||
> a set at the end using `MapSet.new/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -399,15 +405,15 @@ defmodule MapSet do
|
||||
iex> while_false
|
||||
MapSet.new([1, 3])
|
||||
|
||||
iex> {while_true, while_false} = MapSet.split_with(MapSet.new(), fn {_k, v} -> v > 50 end)
|
||||
iex> {while_true, while_false} = MapSet.split_with(MapSet.new([10, 20, 60, 70]), fn v -> v > 50 end)
|
||||
iex> while_true
|
||||
MapSet.new([])
|
||||
MapSet.new([60, 70])
|
||||
iex> while_false
|
||||
MapSet.new([])
|
||||
MapSet.new([10, 20])
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
|
||||
@spec split_with(t(a), (a -> as_boolean(term))) :: {t(a), t(a)} when a: value
|
||||
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
|
||||
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
|
||||
{%MapSet{map: while_true}, %MapSet{map: while_false}}
|
||||
|
||||
+58
-46
@@ -190,7 +190,7 @@ defmodule Module do
|
||||
|
||||
* a string (often a heredoc)
|
||||
* `false`, which will make the entity invisible to documentation-extraction
|
||||
tools like [`ExDoc`](https://hexdocs.pm/ex_doc/)
|
||||
tools like [`ExDoc`](https://ex-doc.hexdocs.pm/)
|
||||
* a keyword list, since Elixir 1.7.0
|
||||
|
||||
For example:
|
||||
@@ -216,7 +216,7 @@ defmodule Module do
|
||||
|
||||
As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc`
|
||||
also accept a keyword list that serves as a way to provide arbitrary metadata
|
||||
about the entity. Tools like [`ExDoc`](https://hexdocs.pm/ex_doc/) and
|
||||
about the entity. Tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) and
|
||||
`IEx` may use this information to display annotations. A common use
|
||||
case is the `:since` key, which may be used to annotate in which version the
|
||||
function was introduced.
|
||||
@@ -263,7 +263,7 @@ defmodule Module do
|
||||
|
||||
Tools may use this information to ensure the module is recompiled
|
||||
in case any of the external resources change, see for example:
|
||||
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
|
||||
[`mix compile.elixir`](https://mix.hexdocs.pm/Mix.Tasks.Compile.Elixir.html).
|
||||
|
||||
The specified file path provided is interpreted as relative to
|
||||
the folder containing the project's `mix.exs`, which is the
|
||||
@@ -321,7 +321,7 @@ defmodule Module do
|
||||
|
||||
Accepts a string (often a heredoc) or `false` where `@moduledoc false`
|
||||
will make the module invisible to documentation extraction tools like
|
||||
[`ExDoc`](https://hexdocs.pm/ex_doc/).
|
||||
[`ExDoc`](https://ex-doc.hexdocs.pm/).
|
||||
|
||||
Similarly to `@doc` also accepts a keyword list to provide metadata
|
||||
about the module. For more details, see the documentation of `@doc`
|
||||
@@ -366,7 +366,8 @@ defmodule Module do
|
||||
Unlike other hooks, `@on_definition` will only invoke functions and
|
||||
never macros. This is to avoid `@on_definition` callbacks from
|
||||
redefining functions that have just been defined in favor of more
|
||||
explicit approaches.
|
||||
explicit approaches. They are also invoked in the reverse order of
|
||||
registration.
|
||||
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__on_definition__/6`.
|
||||
@@ -656,7 +657,8 @@ defmodule Module do
|
||||
@spec module_info(:attributes) :: keyword()
|
||||
@spec module_info(:compile) :: keyword()
|
||||
@spec module_info(:md5) :: binary()
|
||||
@spec module_info(:nifs) :: module()
|
||||
@spec module_info(:nifs) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
@spec module_info(:exports) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
@spec module_info(:functions) :: [function_info]
|
||||
@@ -679,7 +681,7 @@ defmodule Module do
|
||||
information. For documentation, see `c:Module.__info__/1`.
|
||||
'''
|
||||
|
||||
@type definition :: {atom, arity}
|
||||
@type definition :: {function_name :: atom, arity}
|
||||
@type def_kind :: :def | :defp | :defmacro | :defmacrop
|
||||
|
||||
@type create_opts :: [
|
||||
@@ -752,7 +754,7 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec reserved_attributes() :: map
|
||||
@spec reserved_attributes() :: %{optional(atom()) => %{doc: binary()}}
|
||||
def reserved_attributes() do
|
||||
%{
|
||||
after_compile: %{
|
||||
@@ -958,7 +960,7 @@ defmodule Module do
|
||||
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -984,7 +986,7 @@ defmodule Module do
|
||||
It handles binaries and atoms. If one of the aliases
|
||||
is nil, it is discarded.
|
||||
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1015,7 +1017,7 @@ defmodule Module do
|
||||
If the alias was not referenced yet, fails with `ArgumentError`.
|
||||
It handles binaries and atoms.
|
||||
|
||||
> #### Untracked compile-time dependencies {. :warning}
|
||||
> #### Untracked compile-time dependencies {: .warning}
|
||||
>
|
||||
> Use this function with care, as dynamically defining
|
||||
> module names at compilation time may lead to
|
||||
@@ -1039,7 +1041,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
|
||||
@@ -1146,7 +1148,7 @@ defmodule Module do
|
||||
defp simplify_var(var, guess_priority) do
|
||||
case Atom.to_string(var) do
|
||||
"_" -> {:_, [], guess_priority}
|
||||
"_" <> rest -> {String.to_atom(rest), [], guess_priority}
|
||||
"_" <> rest -> {String.to_unsafe_atom(rest), [], guess_priority}
|
||||
_ -> {var, [], nil}
|
||||
end
|
||||
end
|
||||
@@ -1157,7 +1159,7 @@ defmodule Module do
|
||||
rescue
|
||||
ArgumentError -> module
|
||||
else
|
||||
module_name -> String.to_atom(Macro.underscore(List.last(module_name)))
|
||||
module_name -> String.to_unsafe_atom(Macro.underscore(List.last(module_name)))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1190,7 +1192,7 @@ defmodule Module do
|
||||
defp expand_key(key, counters) do
|
||||
case counters do
|
||||
%{^key => count} when is_integer(count) and count >= 1 ->
|
||||
{{:"#{key}#{count}", [], Elixir}, Map.put(counters, key, count - 1)}
|
||||
{{String.to_unsafe_atom("#{key}#{count}"), [], Elixir}, Map.put(counters, key, count - 1)}
|
||||
|
||||
_ ->
|
||||
{{key, [], Elixir}, counters}
|
||||
@@ -1223,7 +1225,8 @@ defmodule Module do
|
||||
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
|
||||
|
||||
# Otherwise, returns a generic guess
|
||||
defp merge_signature({_, meta, _}, _newer, i), do: {:"arg#{i}", meta, Elixir}
|
||||
defp merge_signature({_, meta, _}, _newer, i),
|
||||
do: {String.to_unsafe_atom("arg#{i}"), meta, Elixir}
|
||||
|
||||
@doc """
|
||||
Checks if the module defines the given function or macro.
|
||||
@@ -1249,17 +1252,18 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec defines?(module, definition) :: boolean
|
||||
def defines?(module, {name, arity} = tuple)
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 do
|
||||
def defines?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
|
||||
:ets.member(set, {:def, tuple})
|
||||
:ets.member(set, {:def, definition})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the module defines a function or macro of the
|
||||
given `kind`.
|
||||
given kind.
|
||||
|
||||
`kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
|
||||
`def_kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
|
||||
|
||||
This function can only be used on modules that have not yet been compiled.
|
||||
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
|
||||
@@ -1275,12 +1279,13 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec defines?(module, definition, def_kind) :: boolean
|
||||
def defines?(module, {name, arity} = tuple, def_kind)
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 and
|
||||
def defines?(module, {function_name, arity} = definition, def_kind)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 and
|
||||
def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
|
||||
|
||||
case :ets.lookup(set, {:def, tuple}) do
|
||||
case :ets.lookup(set, {:def, definition}) do
|
||||
[{_, ^def_kind, _, _, _, _}] -> true
|
||||
_ -> false
|
||||
end
|
||||
@@ -1293,7 +1298,9 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec defines_type?(module, definition) :: boolean
|
||||
def defines_type?(module, definition) when is_atom(module) do
|
||||
def defines_type?(module, {function_name, arity} = definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
Kernel.Typespec.defines_type?(module, definition)
|
||||
end
|
||||
|
||||
@@ -1306,7 +1313,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
|
||||
|
||||
@@ -1339,7 +1348,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.
|
||||
@@ -1358,7 +1367,7 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec overridables_in(module) :: [atom]
|
||||
@spec overridables_in(module) :: [definition]
|
||||
def overridables_in(module) when is_atom(module) do
|
||||
assert_not_compiled!(__ENV__.function, module, :all)
|
||||
:elixir_overridable.overridables_for(module)
|
||||
@@ -1407,10 +1416,10 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec definitions_in(module, def_kind) :: [definition]
|
||||
def definitions_in(module, kind)
|
||||
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
def definitions_in(module, def_kind)
|
||||
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
{set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in)
|
||||
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
:ets.select(set, [{{{:def, :"$1"}, def_kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1441,16 +1450,17 @@ defmodule Module do
|
||||
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
|
||||
| nil
|
||||
@doc since: "1.12.0"
|
||||
def get_definition(module, {name, arity}, options \\ [])
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
|
||||
def get_definition(module, {function_name, arity} = _definition, options \\ [])
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 and is_list(options) do
|
||||
{set, bag} = data_tables_for!(module, __ENV__.function, "")
|
||||
|
||||
case :ets.lookup(set, {:def, {name, arity}}) do
|
||||
case :ets.lookup(set, {:def, {function_name, arity}}) do
|
||||
[{_key, kind, meta, _, _, _}] ->
|
||||
clauses =
|
||||
if options[:skip_clauses],
|
||||
do: [],
|
||||
else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
|
||||
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2)
|
||||
|
||||
{:v1, kind, meta, clauses}
|
||||
|
||||
@@ -1467,10 +1477,11 @@ defmodule Module do
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec delete_definition(module, definition) :: boolean()
|
||||
def delete_definition(module, {name, arity})
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) do
|
||||
def delete_definition(module, {function_name, arity} = _definition)
|
||||
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
|
||||
arity <= 255 do
|
||||
assert_not_compiled!(__ENV__.function, module, :writeable)
|
||||
:elixir_def.take_definition(module, {name, arity}) != false
|
||||
:elixir_def.take_definition(module, {function_name, arity}) != false
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1486,20 +1497,20 @@ defmodule Module do
|
||||
given.
|
||||
"""
|
||||
@spec make_overridable(module, [definition]) :: :ok
|
||||
def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
|
||||
def make_overridable(module, definitions) when is_atom(module) and is_list(definitions) do
|
||||
assert_not_compiled!(__ENV__.function, module, :writeable)
|
||||
|
||||
func = fn
|
||||
{function_name, arity} = tuple
|
||||
{function_name, arity} = definition
|
||||
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 ->
|
||||
case :elixir_def.take_definition(module, tuple) do
|
||||
case :elixir_def.take_definition(module, definition) do
|
||||
false ->
|
||||
raise ArgumentError,
|
||||
"cannot make function #{function_name}/#{arity} " <>
|
||||
"overridable because it was not defined"
|
||||
|
||||
clause ->
|
||||
:elixir_overridable.record_overridable(module, tuple, clause)
|
||||
:elixir_overridable.record_overridable(module, definition, clause)
|
||||
end
|
||||
|
||||
other ->
|
||||
@@ -1508,7 +1519,7 @@ defmodule Module do
|
||||
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
|
||||
end
|
||||
|
||||
:lists.foreach(func, tuples)
|
||||
:lists.foreach(func, definitions)
|
||||
end
|
||||
|
||||
@spec make_overridable(module, module) :: :ok
|
||||
@@ -1566,9 +1577,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 """
|
||||
|
||||
@@ -228,11 +228,7 @@ defmodule Module.Behaviour do
|
||||
defp behaviour_defined?(callbacks, behaviour) do
|
||||
callbacks
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.any?(fn
|
||||
{_kind, ^behaviour, _optional?} -> true
|
||||
{_kind, _behaviour, _optional?} -> false
|
||||
end)
|
||||
|> Enum.any?(fn list -> :lists.keymember(behaviour, 2, list) end)
|
||||
end
|
||||
|
||||
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
|
||||
|
||||
@@ -10,6 +10,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type error() :: term()
|
||||
@type mode() :: :erlang | :elixir | :protocol
|
||||
|
||||
@typedoc """
|
||||
@@ -19,6 +20,7 @@ defmodule Module.ParallelChecker do
|
||||
{:max_concurrency, pos_integer()}
|
||||
| {:long_verification_threshold, pos_integer()}
|
||||
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
|
||||
| {:profile, :time}
|
||||
| {atom(), term()}
|
||||
]
|
||||
|
||||
@@ -66,17 +68,37 @@ defmodule Module.ParallelChecker do
|
||||
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
|
||||
# Protocols may have been consolidated. So if we know their beam location,
|
||||
# we discard their module map on purpose and start from file.
|
||||
info =
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
List.to_string(beam_location)
|
||||
else
|
||||
cache_from_module_map(table, module_map, signatures)
|
||||
end
|
||||
|
||||
inner_spawn(pid, checker, table, module, info, log?)
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
spawn_and_register_cache(pid, checker, table, module, List.to_string(beam_location), log?)
|
||||
else
|
||||
{mode, module_tuple} = cache_from_module_map(table, module_map, signatures)
|
||||
ref = make_ref()
|
||||
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
|
||||
register_cache_and_checker(checker, mode, module, spawned, ref)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp inner_spawn(pid, checker, table, module, info, log?) do
|
||||
defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
|
||||
spawn(fn ->
|
||||
mon_ref = Process.monitor(pid)
|
||||
|
||||
receive do
|
||||
{^ref, :check, profile} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
Process.link(pid)
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
{warnings, errors} = check_module(module_tuple, {checker, table}, log?, profile)
|
||||
send(pid, {__MODULE__, module, warnings, errors})
|
||||
send(checker, {__MODULE__, :checked, ref})
|
||||
|
||||
{:DOWN, ^mon_ref, _, _, _} ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp spawn_and_register_cache(pid, checker, table, module, info, log?) do
|
||||
ref = make_ref()
|
||||
|
||||
spawned =
|
||||
@@ -87,48 +109,28 @@ defmodule Module.ParallelChecker do
|
||||
{^ref, :cache} ->
|
||||
Process.link(pid)
|
||||
|
||||
{mode, module_tuple} =
|
||||
cond do
|
||||
is_binary(info) ->
|
||||
location =
|
||||
case :code.which(module) do
|
||||
[_ | _] = path -> path
|
||||
_ -> info
|
||||
end
|
||||
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok,
|
||||
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
|
||||
else
|
||||
_ -> {:not_found, nil}
|
||||
end
|
||||
|
||||
is_tuple(info) ->
|
||||
info
|
||||
location =
|
||||
case :code.which(module) do
|
||||
[_ | _] = path -> path
|
||||
_ -> info
|
||||
end
|
||||
|
||||
# We only make the module available now, so they are not visible during inference
|
||||
:ets.insert(table, {module, mode})
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
{^ref, :check} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
warnings =
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, table}, log?)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
send(pid, {__MODULE__, module, warnings})
|
||||
send(checker, {__MODULE__, :done, module})
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", exck}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(exck) do
|
||||
mode = cache_chunk(table, module, contents)
|
||||
module_tuple = module_map_to_module_tuple(module_map)
|
||||
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
|
||||
:ets.insert(table, {module, mode})
|
||||
send(checker, {__MODULE__, :cached, module, spawned, ref})
|
||||
else
|
||||
_ ->
|
||||
# Nothing to check, so we notify everyone we are done
|
||||
:ets.insert(table, {module, :uncached})
|
||||
send(checker, {__MODULE__, :cached, module, nil, ref})
|
||||
send(pid, {__MODULE__, module, [], []})
|
||||
end
|
||||
|
||||
{:DOWN, ^mon_ref, _, _, _} ->
|
||||
@@ -136,7 +138,7 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end)
|
||||
|
||||
register(checker, module, spawned, ref)
|
||||
register_cache(checker, module, spawned, ref)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -184,39 +186,44 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
Returns the updated list of warnings from the verification.
|
||||
"""
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: {[warning()], [error()]}
|
||||
def verify({checker, table}, runtime_files) do
|
||||
value = :erlang.get(:elixir_code_diagnostics)
|
||||
log? = not match?({_, false}, value)
|
||||
|
||||
for {module, file} <- runtime_files do
|
||||
inner_spawn(self(), checker, table, module, file, log?)
|
||||
spawn_and_register_cache(self(), checker, table, module, file, log?)
|
||||
end
|
||||
|
||||
count = :gen_server.call(checker, :start, :infinity)
|
||||
diagnostics = collect_results(count, [])
|
||||
{warnings, errors} = collect_results(count, [], [])
|
||||
|
||||
case :erlang.get(:elixir_code_diagnostics) do
|
||||
:undefined -> :ok
|
||||
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
|
||||
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
|
||||
end
|
||||
|
||||
diagnostics
|
||||
{warnings, errors}
|
||||
end
|
||||
|
||||
defp collect_results(0, diagnostics) do
|
||||
diagnostics
|
||||
defp collect_results(0, warnings, errors) do
|
||||
{warnings, errors}
|
||||
end
|
||||
|
||||
defp collect_results(count, diagnostics) do
|
||||
defp collect_results(count, warnings, errors) do
|
||||
receive do
|
||||
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
diagnostic = %{diagnostic | file: file && Path.absname(file)}
|
||||
collect_results(count, [diagnostic | diagnostics])
|
||||
|
||||
{__MODULE__, _module, new_diagnostics} ->
|
||||
collect_results(count - 1, new_diagnostics ++ diagnostics)
|
||||
if Map.get(diagnostic, :severity, :warning) == :error do
|
||||
collect_results(count, warnings, [diagnostic | errors])
|
||||
else
|
||||
collect_results(count, [diagnostic | warnings], errors)
|
||||
end
|
||||
|
||||
{__MODULE__, _module, new_warnings, new_errors} ->
|
||||
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -256,7 +263,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module_tuple, cache, log?) do
|
||||
defp check_module(module_tuple, cache, log?, profile) do
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
|
||||
module_tuple
|
||||
|
||||
@@ -270,18 +277,23 @@ defmodule Module.ParallelChecker do
|
||||
definitions
|
||||
)
|
||||
|
||||
diagnostics =
|
||||
module
|
||||
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|
||||
type_warnings =
|
||||
profile(module, profile, fn ->
|
||||
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
|
||||
end)
|
||||
|
||||
{warnings, errors} =
|
||||
type_warnings
|
||||
|> Kernel.++(behaviour_warnings)
|
||||
|> group_warnings()
|
||||
|> emit_warnings(file, log?)
|
||||
|> group_diagnostics()
|
||||
|> emit_diagnostics(file, log?)
|
||||
|> Enum.split_with(&(&1.severity == :warning))
|
||||
|
||||
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
|
||||
apply(verify_mod, verify_fun, [module])
|
||||
end)
|
||||
|
||||
diagnostics
|
||||
{warnings, errors}
|
||||
end
|
||||
|
||||
defp module_map_to_module_tuple(module_map) do
|
||||
@@ -332,16 +344,16 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Warning helpers
|
||||
|
||||
defp group_warnings(warnings) do
|
||||
defp group_diagnostics(triplets) do
|
||||
{ungrouped, grouped} =
|
||||
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(warning)
|
||||
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(term)
|
||||
|
||||
if Map.get(diagnostic, :group, false) do
|
||||
locations = MapSet.new([location])
|
||||
|
||||
grouped =
|
||||
Map.update(grouped, warning, {locations, diagnostic}, fn
|
||||
Map.update(grouped, term, {locations, diagnostic}, fn
|
||||
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
|
||||
end)
|
||||
|
||||
@@ -359,7 +371,7 @@ defmodule Module.ParallelChecker do
|
||||
Enum.sort(ungrouped ++ grouped)
|
||||
end
|
||||
|
||||
defp emit_warnings(warnings, file, log?) do
|
||||
defp emit_diagnostics(warnings, file, log?) do
|
||||
Enum.flat_map(warnings, fn {locations, diagnostic} ->
|
||||
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
|
||||
log? and print_diagnostics(diagnostics)
|
||||
@@ -521,8 +533,12 @@ defmodule Module.ParallelChecker do
|
||||
:gen_server.call(server, {:unlock, module, mode}, :infinity)
|
||||
end
|
||||
|
||||
defp register(server, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register, module, pid, ref})
|
||||
defp register_cache_and_checker(server, mode, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, ref})
|
||||
end
|
||||
|
||||
defp register_cache(server, module, pid, ref) do
|
||||
:gen_server.cast(server, {:register_cache, module, pid, ref})
|
||||
end
|
||||
|
||||
## Server callbacks
|
||||
@@ -565,11 +581,13 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
state = %{
|
||||
waiting: %{},
|
||||
modules: [],
|
||||
caches: [],
|
||||
checkers: [],
|
||||
spawned: %{},
|
||||
schedulers: schedulers,
|
||||
threshold: threshold,
|
||||
callback: callback,
|
||||
profile: Keyword.get(options, :profile),
|
||||
protocols: [],
|
||||
table: table
|
||||
}
|
||||
@@ -577,20 +595,9 @@ defmodule Module.ParallelChecker do
|
||||
:gen_server.enter_loop(__MODULE__, [], state)
|
||||
end
|
||||
|
||||
def handle_call(:start, _from, %{modules: modules, protocols: protocols, table: table} = state) do
|
||||
def handle_call(:start, _from, %{caches: caches, protocols: protocols, table: table} = state) do
|
||||
:ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
|
||||
|
||||
for {_module, pid, ref} <- modules do
|
||||
send(pid, {ref, :cache})
|
||||
end
|
||||
|
||||
for {_module, _pid, ref} <- modules do
|
||||
receive do
|
||||
{^ref, :cached} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
{:reply, length(modules), run_checkers(%{state | protocols: []})}
|
||||
{:reply, length(caches), run_caches(%{state | protocols: []})}
|
||||
end
|
||||
|
||||
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
|
||||
@@ -620,10 +627,21 @@ defmodule Module.ParallelChecker do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({__MODULE__, :done, module}, state) do
|
||||
# Unfortunately we cannot assume uniqueness because the same module
|
||||
# may be defined by mistake several times
|
||||
{timer, spawned} = Map.pop(state.spawned, module)
|
||||
def handle_info({__MODULE__, :cached, module, pid, ref}, state) do
|
||||
{_nil, spawned} = Map.pop(state.spawned, ref)
|
||||
|
||||
state =
|
||||
if pid do
|
||||
%{state | spawned: spawned, checkers: [{module, pid, ref} | state.checkers]}
|
||||
else
|
||||
%{state | spawned: spawned}
|
||||
end
|
||||
|
||||
{:noreply, run_caches(state)}
|
||||
end
|
||||
|
||||
def handle_info({__MODULE__, :checked, ref}, state) do
|
||||
{timer, spawned} = Map.pop(state.spawned, ref)
|
||||
timer && Process.cancel_timer(timer)
|
||||
{:noreply, run_checkers(%{state | spawned: spawned})}
|
||||
end
|
||||
@@ -632,11 +650,41 @@ defmodule Module.ParallelChecker do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
|
||||
{:noreply, %{state | modules: [{module, pid, ref} | modules]}}
|
||||
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do
|
||||
{:noreply, %{state | caches: [{module, pid, ref} | caches]}}
|
||||
end
|
||||
|
||||
defp run_checkers(%{modules: []} = state) do
|
||||
def handle_cast(
|
||||
{:register_cache_and_checker, mode, module, pid, ref},
|
||||
%{caches: caches, checkers: checkers} = state
|
||||
) do
|
||||
{:noreply,
|
||||
%{state | caches: [{module, mode} | caches], checkers: [{module, pid, ref} | checkers]}}
|
||||
end
|
||||
|
||||
defp run_caches(%{caches: [], spawned: spawned} = state) do
|
||||
if spawned == %{}, do: run_checkers(state), else: state
|
||||
end
|
||||
|
||||
defp run_caches(%{spawned: spawned, schedulers: schedulers} = state)
|
||||
when map_size(spawned) >= schedulers do
|
||||
state
|
||||
end
|
||||
|
||||
defp run_caches(%{caches: [cache | caches]} = state) do
|
||||
case cache do
|
||||
{_module, pid, ref} ->
|
||||
send(pid, {ref, :cache})
|
||||
spawned = Map.put(state.spawned, ref, nil)
|
||||
run_caches(%{state | caches: caches, spawned: spawned})
|
||||
|
||||
{module, mode} ->
|
||||
:ets.insert(state.table, {module, mode})
|
||||
run_caches(%{state | caches: caches})
|
||||
end
|
||||
end
|
||||
|
||||
defp run_checkers(%{checkers: []} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
@@ -645,10 +693,25 @@ defmodule Module.ParallelChecker do
|
||||
state
|
||||
end
|
||||
|
||||
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
|
||||
send(pid, {ref, :check})
|
||||
timer = Process.send_after(self(), {__MODULE__, :timeout, module, pid}, state.threshold)
|
||||
spawned = Map.put(state.spawned, module, timer)
|
||||
run_checkers(%{state | modules: modules, spawned: spawned})
|
||||
defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do
|
||||
send(pid, {ref, :check, state.profile})
|
||||
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
|
||||
spawned = Map.put(state.spawned, ref, timer)
|
||||
run_checkers(%{state | checkers: checkers, spawned: spawned})
|
||||
end
|
||||
|
||||
defp profile(module, :time, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
|
||||
)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp profile(_module, _profile, fun) do
|
||||
fun.()
|
||||
end
|
||||
end
|
||||
|
||||
+299
-52
@@ -4,7 +4,7 @@
|
||||
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
alias Module.Types.{Descr, Expr, Pattern, Helpers}
|
||||
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern}
|
||||
|
||||
# The mode controls what happens on function application when
|
||||
# there are gradual arguments. Non-gradual arguments always
|
||||
@@ -86,7 +86,7 @@ defmodule Module.Types do
|
||||
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
|
||||
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
|
||||
{[{fun_arity, inferred} | types], private, context}
|
||||
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
|
||||
else
|
||||
{types, private, context}
|
||||
end
|
||||
@@ -136,8 +136,8 @@ defmodule Module.Types do
|
||||
defp impl_for(attrs) do
|
||||
case List.keyfind(attrs, :__impl__, 0) do
|
||||
{:__impl__, [protocol: protocol, for: for]} ->
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
|
||||
{for, protocol.behaviour_info(:callbacks)}
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
|
||||
{for, protocol.__protocol__(:functions)}
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -178,7 +178,7 @@ defmodule Module.Types do
|
||||
default = Keyword.get(meta, :defaults, 0)
|
||||
|
||||
cond do
|
||||
Keyword.get(meta, :context) != nil ->
|
||||
Keyword.get(meta, :context) != nil or Keyword.get(meta, :from_super) == true ->
|
||||
:ok
|
||||
|
||||
default == 0 ->
|
||||
@@ -232,7 +232,7 @@ defmodule Module.Types do
|
||||
|
||||
context =
|
||||
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
|
||||
# Optimized version of finder, since we already the definition
|
||||
# Optimized version of finder, since we already have the definition
|
||||
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
|
||||
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
context
|
||||
@@ -251,15 +251,21 @@ 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
|
||||
{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 ->
|
||||
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}
|
||||
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
end
|
||||
@@ -289,11 +295,11 @@ defmodule Module.Types do
|
||||
|
||||
local_sigs ->
|
||||
case finder.(fun_arity) do
|
||||
{mode, {fun_arity, kind, meta, clauses}, expected} ->
|
||||
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
|
||||
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
@@ -306,9 +312,10 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
|
||||
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
|
||||
context =
|
||||
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
|
||||
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
Module.Types.Traverse.of_expr(body, stack, context)
|
||||
end)
|
||||
|
||||
@@ -316,57 +323,234 @@ defmodule Module.Types do
|
||||
{inferred, [{0, 0}], context}
|
||||
end
|
||||
|
||||
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
|
||||
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
|
||||
{fun, _arity} = fun_arity
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
stack = fresh_stack(stack, mode, fun_arity)
|
||||
base_info = {:def, kind, fun, expected}
|
||||
|
||||
{_, _, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, [], [], context}, fn
|
||||
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
|
||||
context = fresh_context(context)
|
||||
case clauses do
|
||||
[{meta, args, [], {:super, _, [_ | _]} = body}] ->
|
||||
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context)
|
||||
|
||||
try do
|
||||
{trees, context} =
|
||||
Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context)
|
||||
_ ->
|
||||
infer_local_handler(clauses, base_info, kind, fun, expected, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
{return_type, context} =
|
||||
Expr.of_expr(body, Descr.term(), body, stack, context)
|
||||
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
|
||||
stack = with_file_meta(stack, meta)
|
||||
guards = []
|
||||
previous = Pattern.init_previous()
|
||||
fresh_context = fresh_context(context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
args_types = Pattern.of_domain(trees, context)
|
||||
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, args_types, return_type, total - 1, [])
|
||||
add_inferred(inferred, clause_types, return_type, total - 1, [])
|
||||
|
||||
if type_index == -1 do
|
||||
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
|
||||
else
|
||||
{index + 1, total, [{index, type_index} | mapping], inferred, context}
|
||||
end
|
||||
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.opt_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
|
||||
{_, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {Pattern.init_previous(), [], context}, fn
|
||||
{meta, args, guards, body}, {previous, inferred, acc_context} ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
fresh_context = fresh_context(acc_context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
try do
|
||||
{trees, precise?, head_no_previous_args_types, previous, head_context} =
|
||||
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
|
||||
|
||||
{return_type, context} =
|
||||
Expr.of_expr(body, Descr.term(), body, stack, head_context)
|
||||
|
||||
args_types = Pattern.of_domain(trees, stack, context)
|
||||
|
||||
head_args_types =
|
||||
case inferred do
|
||||
[] -> nil
|
||||
_ -> Pattern.of_domain(trees, stack, head_context)
|
||||
end
|
||||
|
||||
args_triplet = {args_types, head_args_types, head_no_previous_args_types}
|
||||
inferred = [{args_triplet, return_type, precise?} | inferred]
|
||||
{previous, inferred, context}
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
|
||||
end
|
||||
end)
|
||||
|
||||
{clauses_types, mapping, domain} =
|
||||
clauses_types
|
||||
|> Enum.reverse()
|
||||
|> group_clauses()
|
||||
|
||||
domain =
|
||||
case clauses_types do
|
||||
[_] ->
|
||||
nil
|
||||
|
||||
_ ->
|
||||
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 = {:infer, domain, clauses_types}
|
||||
{inferred, mapping, restore_context(clauses_context, context)}
|
||||
end
|
||||
|
||||
defp group_clauses(clauses) do
|
||||
{_, all_clauses, filtered_clauses, non_empty?} =
|
||||
Enum.reduce(clauses, {0, [], [], false}, fn
|
||||
{_args_triplet, return, precise?} = clause,
|
||||
{index, all_clauses, filtered_clauses, non_empty?} ->
|
||||
empty? = Descr.empty?(return)
|
||||
indexed_clause = {clause, index}
|
||||
|
||||
filtered_clauses =
|
||||
if precise? and empty? do
|
||||
filtered_clauses
|
||||
else
|
||||
[indexed_clause | filtered_clauses]
|
||||
end
|
||||
|
||||
{index + 1, [indexed_clause | all_clauses], filtered_clauses, non_empty? or not empty?}
|
||||
end)
|
||||
|
||||
clauses =
|
||||
if non_empty? do
|
||||
Enum.reverse(filtered_clauses)
|
||||
else
|
||||
Enum.reverse(all_clauses)
|
||||
end
|
||||
|
||||
[
|
||||
{{{args, _head_args, _head_no_previous_args}, _return, _precise?}, _index}
|
||||
| clauses_tail
|
||||
] = clauses
|
||||
|
||||
domain =
|
||||
Enum.reduce(clauses_tail, args, fn
|
||||
{{{args, head_args, head_no_previous_args}, _return, _precise?}, _index}, domain ->
|
||||
compute_domain(args, head_args, head_no_previous_args, domain)
|
||||
end)
|
||||
|
||||
{_, mapping, inferred} =
|
||||
Enum.reduce(clauses, {0, [], []}, fn
|
||||
{{{args, _head_args, _head_no_previous_args}, return, _precise?}, index},
|
||||
{total, mapping, inferred} ->
|
||||
{type_index, inferred} = add_inferred(inferred, args, return, total - 1, [])
|
||||
|
||||
if type_index == -1 do
|
||||
{total + 1, [{index, total} | mapping], inferred}
|
||||
else
|
||||
{total, [{index, type_index} | mapping], inferred}
|
||||
end
|
||||
end)
|
||||
|
||||
{Enum.reverse(inferred), mapping, domain}
|
||||
end
|
||||
|
||||
defp compute_domain(
|
||||
[arg | args_types],
|
||||
[head_arg | head_args_types],
|
||||
[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:
|
||||
#
|
||||
# opt_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.opt_union(Descr.upper_bound(no_prev_arg), d)
|
||||
else
|
||||
Descr.opt_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),
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])}
|
||||
|
||||
defp add_inferred([head | tail], args, return, index, acc),
|
||||
do: add_inferred(tail, args, return, index - 1, [head | acc])
|
||||
@@ -374,6 +558,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.opt_union(existing_arg, arg) | acc], true)
|
||||
end
|
||||
|
||||
defp union_args([_ | _], [_ | _], _acc, true), do: nil
|
||||
|
||||
# In theory fully equal args are merged on add_inferred
|
||||
defp union_args([], [], acc, _changed?), do: Enum.reverse(acc)
|
||||
|
||||
defp with_file_meta(stack, meta) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
|
||||
@@ -435,7 +671,9 @@ defmodule Module.Types do
|
||||
# The mode to be used, see the @modes attribute
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler
|
||||
local_handler: handler,
|
||||
# Reverse arrow handling (nil | :cache | :except_none | :include_none)
|
||||
reverse_arrow: nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -446,36 +684,45 @@ defmodule Module.Types do
|
||||
warnings: [],
|
||||
# All vars and their types
|
||||
vars: %{},
|
||||
# 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 matches and guards
|
||||
# Track metadata specific to patterns and guards
|
||||
pattern_info: nil,
|
||||
# If type checking has found an error/failure
|
||||
failed: false,
|
||||
# Local signatures used by local handler
|
||||
local_sigs: %{},
|
||||
# Track which clauses have been used across private local calls
|
||||
local_used: %{}
|
||||
local_used: %{},
|
||||
# Cached reverse arrows
|
||||
reverse_arrows: %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp fresh_stack(stack, mode, function) when mode in @modes do
|
||||
%{stack | mode: mode, function: function}
|
||||
%{stack | mode: mode, function: function, reverse_arrow: nil}
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
%{context | vars: %{}, failed: false}
|
||||
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
|
||||
end
|
||||
|
||||
defp restore_context(later_context, %{vars: vars, failed: failed}) do
|
||||
%{later_context | vars: vars, failed: failed}
|
||||
defp restore_context(later_context, %{
|
||||
vars: vars,
|
||||
failed: failed,
|
||||
reverse_arrows: reverse_arrows
|
||||
}) do
|
||||
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
|
||||
end
|
||||
|
||||
## Diagnostics
|
||||
|
||||
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
|
||||
%{
|
||||
message: "this clause of #{kind} #{fun}/#{arity} is never used"
|
||||
message:
|
||||
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+3679
-2201
File diff suppressed because it is too large
Load Diff
+595
-299
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ defmodule Module.Types.Helpers do
|
||||
## Warnings
|
||||
|
||||
@doc """
|
||||
Converts an itneger into ordinal.
|
||||
Converts an integer into ordinal.
|
||||
"""
|
||||
def integer_to_ordinal(i) do
|
||||
case rem(i, 10) do
|
||||
@@ -143,8 +143,9 @@ 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
|
||||
@@ -160,9 +161,9 @@ defmodule Module.Types.Helpers do
|
||||
_ ->
|
||||
{:ok, versions}
|
||||
end
|
||||
|
||||
node, versions ->
|
||||
{node, versions}
|
||||
else
|
||||
_ -> {node, versions}
|
||||
end
|
||||
end)
|
||||
|
||||
versions
|
||||
@@ -261,7 +262,8 @@ 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
|
||||
|
||||
@@ -327,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]}
|
||||
|
||||
@@ -352,42 +361,134 @@ 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
|
||||
else
|
||||
case
|
||||
[
|
||||
{:->, _,
|
||||
[
|
||||
[
|
||||
{: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]]}
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Module.Types.Of do
|
||||
@prefix quote(do: ...)
|
||||
@suffix quote(do: ...)
|
||||
|
||||
@integer_or_float union(integer(), float())
|
||||
@integer_or_float opt_union(integer(), float())
|
||||
@integer integer()
|
||||
@float float()
|
||||
@binary binary()
|
||||
@@ -31,7 +31,7 @@ defmodule Module.Types.Of do
|
||||
@doc """
|
||||
Marks a variable with error.
|
||||
|
||||
This purposedly deletes all traces of the variable,
|
||||
This purposely deletes all traces of the variable,
|
||||
as it is often invoked when the cause for error is elsewhere.
|
||||
"""
|
||||
def error_var({_, meta, _}, context) do
|
||||
@@ -48,7 +48,7 @@ defmodule Module.Types.Of do
|
||||
@doc """
|
||||
Declares a variable.
|
||||
"""
|
||||
def declare_var(var, context) do
|
||||
def declare_var(var, type \\ term(), context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
|
||||
@@ -58,7 +58,7 @@ defmodule Module.Types.Of do
|
||||
|
||||
vars ->
|
||||
data = %{
|
||||
type: term(),
|
||||
type: type,
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: [],
|
||||
@@ -86,6 +86,9 @@ 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
|
||||
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
@@ -103,22 +106,50 @@ defmodule Module.Types.Of do
|
||||
context
|
||||
end
|
||||
|
||||
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
|
||||
case compatible_intersection(old_type, type) do
|
||||
{:ok, new_type} when new_type != old_type ->
|
||||
data = %{
|
||||
data
|
||||
| type: new_type,
|
||||
off_traces: new_trace(expr, new_type, stack, off_traces)
|
||||
}
|
||||
case stack do
|
||||
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
|
||||
{old_type, context}
|
||||
|
||||
{new_type, %{context | vars: %{vars | version => data}}}
|
||||
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] ->
|
||||
new_type = opt_intersection(old_type, type)
|
||||
|
||||
_ ->
|
||||
{old_type, context}
|
||||
end
|
||||
else
|
||||
{old_type, context}
|
||||
case empty?(new_type) do
|
||||
true when reverse_arrow == :include_none ->
|
||||
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
|
||||
|
||||
@@ -140,7 +171,7 @@ defmodule Module.Types.Of do
|
||||
{:ok, error_type(), context}
|
||||
|
||||
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
|
||||
new_type = intersection(type, old_type)
|
||||
new_type = opt_intersection(type, old_type)
|
||||
|
||||
data = %{
|
||||
data
|
||||
@@ -169,8 +200,19 @@ defmodule Module.Types.Of do
|
||||
Preserves `context` in first argument while
|
||||
resetting it to the vars in the second argument.
|
||||
"""
|
||||
def reset_vars(context, %{vars: vars, conditional_vars: conditional_vars}),
|
||||
do: %{context | vars: vars, conditional_vars: conditional_vars}
|
||||
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.
|
||||
@@ -193,14 +235,17 @@ defmodule Module.Types.Of do
|
||||
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 Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
|
||||
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)
|
||||
opt_union(acc, type)
|
||||
end)
|
||||
|
||||
{_, context} = refine_body_var(version, type, expr, stack, context)
|
||||
@@ -219,8 +264,8 @@ defmodule Module.Types.Of do
|
||||
{Float, float()},
|
||||
{Function, fun()},
|
||||
{Integer, integer()},
|
||||
{List, union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: if_set(negation(atom())))},
|
||||
{List, opt_union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: {Module.Types.Descr.opt_negation(atom()), true})},
|
||||
{Port, port()},
|
||||
{PID, pid()},
|
||||
{Reference, reference()},
|
||||
@@ -249,7 +294,7 @@ defmodule Module.Types.Of do
|
||||
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
struct_type(struct, info)
|
||||
else
|
||||
open_map(__struct__: atom([struct]))
|
||||
open_map(__struct__: {atom([struct]), false})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -320,7 +365,7 @@ defmodule Module.Types.Of do
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, domain, [{key, value_type} | single], multiple}
|
||||
{dynamic?, domain, [{key, {value_type, false}} | single], multiple}
|
||||
|
||||
_ ->
|
||||
{dynamic?, domain, single, [{pos, value_type} | multiple]}
|
||||
@@ -336,37 +381,47 @@ defmodule Module.Types.Of do
|
||||
closed_map(non_multiple)
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
closed_map(non_multiple ++ [{key, type} | t])
|
||||
products = cartesian_map(tail)
|
||||
|
||||
for key <- keys, product <- products do
|
||||
closed_map(non_multiple ++ [{key, {type, false}} | product])
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
|> Enum.reduce(&opt_union/2)
|
||||
end
|
||||
|
||||
{if(dynamic?, do: dynamic(map), else: map), context}
|
||||
end
|
||||
|
||||
defp union_negated([], new_type, single, multiple) do
|
||||
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
|
||||
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
|
||||
single =
|
||||
Enum.map(single, fn
|
||||
{key, {old_type, optional?}} ->
|
||||
{key, {opt_union(old_type, new_type), optional?}}
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end)
|
||||
|
||||
{single, multiple}
|
||||
end
|
||||
|
||||
defp union_negated(negated, new_type, single, multiple) do
|
||||
{single, matched} =
|
||||
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
|
||||
if key in negated do
|
||||
{{key, old_type}, [key | matched]}
|
||||
else
|
||||
{{key, union(old_type, new_type)}, matched}
|
||||
end
|
||||
Enum.map_reduce(single, [], fn
|
||||
{key, {old_type, optional?}}, matched ->
|
||||
if key in negated do
|
||||
{{key, {old_type, optional?}}, [key | matched]}
|
||||
else
|
||||
{{key, {opt_union(old_type, new_type), optional?}}, matched}
|
||||
end
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} ->
|
||||
{keys, union(old_type, new_type)}
|
||||
{keys, opt_union(old_type, new_type)}
|
||||
end)
|
||||
|
||||
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
|
||||
{Enum.map(negated -- matched, fn key -> {key, {none(), true}} end) ++ single, multiple}
|
||||
end
|
||||
|
||||
defp pairs(pairs, expected, stack, context, of_fun) do
|
||||
@@ -375,7 +430,7 @@ defmodule Module.Types.Of do
|
||||
|
||||
expected_value_type =
|
||||
with {[key], [], []} <- pos_neg_domain,
|
||||
{_, expected_value_type} <- map_fetch_key(expected, key) do
|
||||
{_optional?, expected_value_type} <- map_fetch_key(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
_ -> term()
|
||||
@@ -410,54 +465,66 @@ defmodule Module.Types.Of do
|
||||
[[]]
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
|
||||
products = cartesian_map(tail)
|
||||
for key <- keys, product <- products, do: [{key, {type, false}} | product]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles instantiation of a new struct.
|
||||
|
||||
This is expanded and validated by the compiler, so don't need to check the fields.
|
||||
"""
|
||||
# TODO: Type check the fields match the struct
|
||||
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
|
||||
when is_atom(struct) do
|
||||
{_info, context} = struct_info(struct, meta, stack, context)
|
||||
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
|
||||
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
case map_fetch_key(expected, key) do
|
||||
{_, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
if is_nil(info) do
|
||||
{dynamic(), context}
|
||||
else
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
case map_fetch_key(expected, key) do
|
||||
{_optional?, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, {type, false}}, context}
|
||||
end)
|
||||
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
{closed_map([__struct__: {atom([struct]), false}] ++ args_types), context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `__info__(:struct)` information about a struct.
|
||||
"""
|
||||
def struct_info(struct, meta, stack, context) do
|
||||
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
|
||||
case stack.no_warn_undefined do
|
||||
%Macro.Env{} = env ->
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
|
||||
{:ok, info} -> {info, context}
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
{:error, _desc} -> {nil, context}
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Fetch the signature to validate for warnings.
|
||||
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
info =
|
||||
struct.__info__(:struct) ||
|
||||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
|
||||
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and
|
||||
struct.__info__(:struct)
|
||||
|
||||
{info, context}
|
||||
if info do
|
||||
{_, context} =
|
||||
Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
{info, context}
|
||||
else
|
||||
error = {:unknown_struct, kind, struct, must_exist?}
|
||||
{nil, error(error, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -468,12 +535,27 @@ defmodule Module.Types.Of do
|
||||
# we introduce typed structs. They are only used by exceptions.
|
||||
def struct_type(struct, info, args_types \\ []) do
|
||||
term = dynamic()
|
||||
pairs = for %{field: field} <- info, do: {field, term}
|
||||
pairs = [{:__struct__, atom([struct])} | pairs]
|
||||
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
|
||||
pairs = for %{field: field} <- info, do: {field, {term, false}}
|
||||
pairs = [{:__struct__, {atom([struct]), false}} | pairs]
|
||||
|
||||
pairs =
|
||||
if args_types == [] do
|
||||
pairs
|
||||
else
|
||||
pairs ++ args_types
|
||||
end
|
||||
|
||||
closed_map(pairs)
|
||||
end
|
||||
|
||||
@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 """
|
||||
@@ -559,7 +641,7 @@ defmodule Module.Types.Of do
|
||||
:integer when alignment_value == :default ->
|
||||
{0, context}
|
||||
|
||||
# There is no size, so the aligment depends on the type.
|
||||
# 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}
|
||||
@@ -615,8 +697,8 @@ defmodule Module.Types.Of do
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
end
|
||||
|
||||
defp specifier_size(match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
{actual, context} = Module.Types.Pattern.of_size(match_or_guard, arg, expr, stack, context)
|
||||
defp specifier_size(_match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
end
|
||||
|
||||
@@ -784,6 +866,37 @@ defmodule Module.Types.Of do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
|
||||
detail =
|
||||
case {Code.ensure_loaded?(module), must_exist?} do
|
||||
{true, false} ->
|
||||
"there is such module but it does not define a struct"
|
||||
|
||||
{false, false} ->
|
||||
"module #{inspect(module)} is not available or is yet to be defined"
|
||||
|
||||
{true, true} ->
|
||||
"the module may have been redefined as it no longer defines a struct"
|
||||
|
||||
{false, true} ->
|
||||
"the module was also only available but may have been removed during compilation"
|
||||
end
|
||||
|
||||
%{
|
||||
message: "struct #{inspect(module)} is undefined (#{detail})",
|
||||
group: true,
|
||||
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct_field, kind, module, field}) do
|
||||
%{
|
||||
message: "unknown key #{inspect(field)} for struct #{inspect(module)}",
|
||||
group: true,
|
||||
severity: if(kind == :pattern, do: :error, else: :warning)
|
||||
}
|
||||
end
|
||||
|
||||
defp dot_var?(expr) do
|
||||
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
|
||||
end
|
||||
|
||||
+1042
-391
File diff suppressed because it is too large
Load Diff
@@ -35,8 +35,19 @@ defmodule Module.Types.Traverse do
|
||||
end)
|
||||
end
|
||||
|
||||
# Structs, map update, tail operator
|
||||
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:%, :|] do
|
||||
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]}, stack, context) do
|
||||
context = of_expr(map, stack, context)
|
||||
context = of_expr(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
def of_expr({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) do
|
||||
context = of_expr(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
# Map update, tail operator
|
||||
def of_expr({:|, _meta, [left, right]}, stack, context) do
|
||||
context = of_expr(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
@@ -46,9 +57,9 @@ defmodule Module.Types.Traverse do
|
||||
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# left = right, left <_ right
|
||||
def of_expr({op, _meta, [_left, right]}, stack, context) when op in [:=, :<-] do
|
||||
# Skip the left side (pattern), only traverse right
|
||||
# left = right, left <- right
|
||||
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:=, :<-] do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
@@ -65,8 +76,9 @@ defmodule Module.Types.Traverse do
|
||||
end)
|
||||
end
|
||||
|
||||
# Treat -> as patterns for simplicity
|
||||
def of_expr({:->, _, [_head, body]}, stack, context) do
|
||||
# All non-handled -> are patterns
|
||||
def of_expr({:->, _, [head, body]}, stack, context) do
|
||||
context = of_pattern(head, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end
|
||||
|
||||
@@ -150,6 +162,45 @@ defmodule Module.Types.Traverse do
|
||||
context
|
||||
end
|
||||
|
||||
defp of_pattern({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) when is_atom(module) do
|
||||
context = of_pattern(pairs, stack, context)
|
||||
of_struct(module, pairs, :expr, meta, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern({left, _meta, right}, stack, context) do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_pattern(right, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern({left, right}, stack, context) do
|
||||
context = of_pattern(left, stack, context)
|
||||
of_pattern(right, stack, context)
|
||||
end
|
||||
|
||||
defp of_pattern([_ | _] = list, stack, context) do
|
||||
Enum.reduce(list, context, &of_pattern(&1, stack, &2))
|
||||
end
|
||||
|
||||
defp of_pattern(_, _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
defp of_struct(module, pairs, kind, meta, stack, context) do
|
||||
{info, context} = Module.Types.Of.struct_info(module, kind, meta, stack, context)
|
||||
|
||||
if info do
|
||||
Enum.reduce(pairs, context, fn {key, _value}, context ->
|
||||
if Enum.any?(info, &(&1.field == key)) do
|
||||
context
|
||||
else
|
||||
Module.Types.Of.unknown_struct_field(module, key, kind, meta, stack, context)
|
||||
end
|
||||
end)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp local_fun(meta, fun, arity, stack, context) do
|
||||
case stack.local_handler.(meta, {fun, arity}, stack, context) do
|
||||
false -> context
|
||||
|
||||
@@ -39,7 +39,7 @@ defmodule Node do
|
||||
Currently supported options are:
|
||||
|
||||
* `:name_domain` - determines the host name part of the node name. If `:longnames`,
|
||||
fully qualified domain names will be used which also is the default.
|
||||
fully qualified domain names will be used, which is also the default.
|
||||
If `:shortnames`, only the short name of the host will be used.
|
||||
|
||||
* `:net_ticktime` - The tick time to use in seconds. Defaults to the value of the
|
||||
@@ -47,7 +47,7 @@ defmodule Node do
|
||||
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
|
||||
for more information.
|
||||
|
||||
* `net_tickintensity` - The tick intensity to use. Defaults to the value of the
|
||||
* `:net_tickintensity` - The tick intensity to use. Defaults to the value of the
|
||||
`net_tickintensity` configuration under Erlang's `kernel` application.
|
||||
See [the `kernel` documentation](https://www.erlang.org/doc/apps/kernel/kernel_app.html)
|
||||
for more information.
|
||||
@@ -338,8 +338,6 @@ defmodule Node do
|
||||
|
||||
The default node is `Node.self/0`, the local node. If `node` is the local node,
|
||||
the function also sets the cookie of all other unknown nodes to `cookie`.
|
||||
|
||||
This function will raise `FunctionClauseError` if the given `node` is not alive.
|
||||
"""
|
||||
@spec set_cookie(t, atom) :: true
|
||||
def set_cookie(node \\ Node.self(), cookie) when is_atom(cookie) do
|
||||
|
||||
@@ -43,7 +43,7 @@ defmodule OptionParser do
|
||||
|
||||
defmodule ParseError do
|
||||
@moduledoc """
|
||||
An exception raised when parsing option fails.
|
||||
An exception raised when parsing an option fails.
|
||||
|
||||
For example, see `OptionParser.parse!/2`.
|
||||
"""
|
||||
@@ -387,7 +387,7 @@ defmodule OptionParser do
|
||||
do_parse(rest, config, new_opts, args, invalid, all?)
|
||||
|
||||
{:invalid, option, value, rest} ->
|
||||
# the option exist but it has wrong value
|
||||
# the option exists but it has wrong value
|
||||
do_parse(rest, config, opts, args, [{option, value} | invalid], all?)
|
||||
|
||||
{:undefined, option, _value, rest} ->
|
||||
@@ -463,7 +463,7 @@ defmodule OptionParser do
|
||||
defp next_with_config(["--" <> option | rest], config) do
|
||||
{option, value} = split_option(option)
|
||||
|
||||
if String.contains?(option, ["_"]) do
|
||||
if String.contains?(option, "_") do
|
||||
{:undefined, "--" <> option, value, rest}
|
||||
else
|
||||
tagged = tag_option(option, config)
|
||||
@@ -852,7 +852,7 @@ defmodule OptionParser do
|
||||
|> to_existing_key(allow_nonexistent_atoms?)
|
||||
end
|
||||
|
||||
defp to_existing_key(option, true), do: String.to_atom(option)
|
||||
defp to_existing_key(option, true), do: String.to_unsafe_atom(option)
|
||||
|
||||
defp to_existing_key(option, false) do
|
||||
try do
|
||||
@@ -952,8 +952,14 @@ defmodule OptionParser do
|
||||
|
||||
case types |> List.delete(:keep) |> List.first(:string) do
|
||||
:boolean ->
|
||||
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
case to_switch(name) do
|
||||
"--no-" <> _ = switch ->
|
||||
add_aliases(switch, name, reverse_aliases)
|
||||
|
||||
switch ->
|
||||
base = "#{switch}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
end
|
||||
|
||||
type ->
|
||||
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
|
||||
|
||||
@@ -170,6 +170,8 @@ defmodule PartitionSupervisor do
|
||||
| {:max_seconds, non_neg_integer()}
|
||||
| {:with_arguments, (args :: [term()], partition() -> updated_args :: [term()])}
|
||||
|
||||
defguardp is_name(name) when is_atom(name) or elem(name, 0) == :via
|
||||
|
||||
@doc false
|
||||
def child_spec(opts) when is_list(opts) do
|
||||
id =
|
||||
@@ -366,7 +368,7 @@ defmodule PartitionSupervisor do
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
|
||||
def resize!(name, partitions) when is_integer(partitions) do
|
||||
def resize!(name, partitions) when is_name(name) and is_integer(partitions) do
|
||||
supervisor =
|
||||
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
|
||||
|
||||
@@ -421,8 +423,8 @@ defmodule PartitionSupervisor do
|
||||
Returns the number of partitions for the partition supervisor.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec partitions(name()) :: pos_integer()
|
||||
def partitions(name) do
|
||||
@spec partitions(name()) :: non_neg_integer()
|
||||
def partitions(name) when is_name(name) do
|
||||
name |> table() |> partitions(name)
|
||||
end
|
||||
|
||||
@@ -470,7 +472,7 @@ defmodule PartitionSupervisor do
|
||||
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
|
||||
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
|
||||
]
|
||||
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
|
||||
def which_children(name) when is_name(name) do
|
||||
Supervisor.which_children(name)
|
||||
end
|
||||
|
||||
@@ -498,7 +500,7 @@ defmodule PartitionSupervisor do
|
||||
supervisors: non_neg_integer,
|
||||
workers: non_neg_integer
|
||||
}
|
||||
def count_children(supervisor) when is_atom(supervisor) do
|
||||
def count_children(supervisor) when is_name(supervisor) do
|
||||
Supervisor.count_children(supervisor)
|
||||
end
|
||||
|
||||
@@ -514,7 +516,7 @@ defmodule PartitionSupervisor do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec stop(name(), reason :: term, timeout) :: :ok
|
||||
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_atom(supervisor) do
|
||||
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_name(supervisor) do
|
||||
Supervisor.stop(supervisor, reason, timeout)
|
||||
end
|
||||
|
||||
|
||||
+27
-1
@@ -485,7 +485,7 @@ defmodule Path do
|
||||
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
|
||||
case :file.get_cwd() do
|
||||
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
|
||||
_ -> path
|
||||
_ -> IO.chardata_to_string(path)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -699,6 +699,32 @@ defmodule Path do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Safely joins two paths.
|
||||
|
||||
Returns `{:ok, path}` if `right` is safe to append to `left`, or `:error`
|
||||
otherwise. See `safe_relative/2` for the exact safety rules applied to `right`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.safe_join("foo", "bar")
|
||||
{:ok, "foo/bar"}
|
||||
|
||||
iex> Path.safe_join("foo", "../bar")
|
||||
:error
|
||||
|
||||
iex> Path.safe_join("foo", "/bar")
|
||||
:error
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec safe_join(t, t) :: {:ok, t} | :error
|
||||
def safe_join(left, right) do
|
||||
with {:ok, right} <- safe_relative(right, left) do
|
||||
{:ok, join(left, right)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Splits the path into a list at the path separator.
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ defmodule Port do
|
||||
* `{pid, {:connect, new_pid}}` - sets the `new_pid` as the new owner of
|
||||
the port. Once a port is opened, the port is linked and connected to the
|
||||
caller process and communication to the port only happens through the
|
||||
connected process. This message makes `new_pid` the new connected processes.
|
||||
connected process. This message makes `new_pid` the new connected process.
|
||||
Unless the port is dead, the port will reply to the old owner with
|
||||
`{port, :connected}`. See `connect/2`.
|
||||
|
||||
@@ -88,10 +88,10 @@ defmodule Port do
|
||||
> On Unix systems, arguments are passed to a new operating system
|
||||
> process as an array of strings but on Windows it is up to the child
|
||||
> process to parse them and some Windows programs may apply their own
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing.
|
||||
>
|
||||
> This is particularly troublesome when invoking `.bat` or `.com` files
|
||||
> as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> This is particularly troublesome when invoking `.bat`, `.cmd`, or `.com`
|
||||
> files as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> vulnerable to malicious input and can be used to run arbitrary shell
|
||||
> commands.
|
||||
>
|
||||
@@ -99,7 +99,7 @@ defmodule Port do
|
||||
> files or `.com` applications, you must not pass untrusted input as
|
||||
> arguments to the program. You may avoid accidentally executing them
|
||||
> by explicitly passing the extension of the program you want to run,
|
||||
> such as `.exe`, and double check the program is indeed not a batch
|
||||
> such as `.exe`, and double-checking the program is indeed not a batch
|
||||
> file or `.com` application.
|
||||
>
|
||||
> This affects both `spawn` and `spawn_executable`.
|
||||
@@ -156,7 +156,7 @@ defmodule Port do
|
||||
|
||||
We do not always have control over how third-party software terminates.
|
||||
If necessary, one workaround is to wrap the child application in a script that
|
||||
checks whether stdin has been closed. Here is such a script that has been
|
||||
checks whether stdin has been closed. Here is such a script that has been
|
||||
verified to work on bash shells:
|
||||
|
||||
#!/usr/bin/env bash
|
||||
|
||||
+38
-16
@@ -79,10 +79,11 @@ defmodule Process do
|
||||
@typedoc """
|
||||
A process destination.
|
||||
|
||||
A remote or local PID, a local port, a locally registered name, or a tuple in
|
||||
the form of `{registered_name, node}` for a registered name at another node.
|
||||
A remote or local PID, a local port, a process alias, a locally registered name,
|
||||
or a tuple in the form of `{registered_name, node}` for a registered name at
|
||||
another node.
|
||||
"""
|
||||
@type dest :: pid | port | (registered_name :: atom) | {registered_name :: atom, node}
|
||||
@type dest :: pid | port | alias | (registered_name :: atom) | {registered_name :: atom, node}
|
||||
|
||||
@doc """
|
||||
Tells whether the given process is alive on the local node.
|
||||
@@ -226,7 +227,7 @@ defmodule Process do
|
||||
>
|
||||
> The functions `Kernel.exit/1` and `Process.exit/2` are
|
||||
> named similarly but provide very different functionalities. The
|
||||
> `Kernel:exit/1` function should be used when the intent is to stop the current
|
||||
> `Kernel.exit/1` function should be used when the intent is to stop the current
|
||||
> process while `Process.exit/2` should be used when the intent is to send an
|
||||
> exit signal to another process. Note also that `Kernel.exit/1` can be caught
|
||||
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and
|
||||
@@ -339,7 +340,7 @@ defmodule Process do
|
||||
@doc """
|
||||
Sends a message to the given `dest`.
|
||||
|
||||
`dest` may be a remote or local PID, a local port, a locally
|
||||
`dest` may be a remote or local PID, a local port, a process alias, a locally
|
||||
registered name, or a tuple in the form of `{registered_name, node}` for a
|
||||
registered name at another node.
|
||||
|
||||
@@ -387,8 +388,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
|
||||
@@ -832,12 +831,16 @@ defmodule Process do
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec flag(:async_dist, boolean) :: boolean
|
||||
@spec flag(:error_handler, module) :: module
|
||||
@spec flag(:fullsweep_after, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:max_heap_size, heap_size) :: heap_size
|
||||
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
|
||||
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
|
||||
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:monitor_nodes, term) :: term
|
||||
@spec flag({:monitor_nodes, term}, term) :: term
|
||||
@spec flag(:priority, priority_level) :: priority_level
|
||||
@spec flag(:save_calls, 0..10_000) :: 0..10_000
|
||||
@spec flag(:sensitive, boolean) :: boolean
|
||||
@@ -980,10 +983,34 @@ defmodule Process do
|
||||
defdelegate unalias(alias), to: :erlang
|
||||
|
||||
@doc """
|
||||
Add a descriptive term to the current process.
|
||||
Returns the label set for the process `pid` as set with `set_label/1`
|
||||
or `:proc_lib.set_label/1`.
|
||||
|
||||
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
|
||||
Observer and in crash logs.
|
||||
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 """
|
||||
Adds a descriptive term to the current process.
|
||||
|
||||
The term does not need to be unique, and will be shown in Observer and in
|
||||
crash logs.
|
||||
This label may be useful for identifying a process as one of multiple in a
|
||||
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
|
||||
|
||||
@@ -997,12 +1024,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user