Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51c95b6000 | ||
|
|
12917b00e5 | ||
|
|
ab9ecf2d7f | ||
|
|
eca809285e | ||
|
|
8e71e65aae | ||
|
|
c3a27be073 | ||
|
|
e10f9fb6fe | ||
|
|
f66ffd0733 | ||
|
|
696bad8de1 | ||
|
|
96c8e31410 | ||
|
|
70be440359 | ||
|
|
c6d6683225 | ||
|
|
db2504eca6 | ||
|
|
98c3f5c602 | ||
|
|
db4684b2f5 | ||
|
|
f3f65e4cd1 | ||
|
|
5331174934 | ||
|
|
f4f2be67d7 | ||
|
|
686daadaaa | ||
|
|
6e8563e930 | ||
|
|
65be24a8c2 | ||
|
|
5bd7a90864 | ||
|
|
831046fcb7 | ||
|
|
6c44feae2f | ||
|
|
35eef87590 | ||
|
|
fa091c6bd9 | ||
|
|
a561f81539 | ||
|
|
739c1b6292 | ||
|
|
3e1e6ddf8a | ||
|
|
ccb8c5a2e7 | ||
|
|
07e580cf17 | ||
|
|
1f9f713d9c | ||
|
|
9814a4cd61 | ||
|
|
3dfdd80dff | ||
|
|
0fdc7510b8 | ||
|
|
0e8873238d | ||
|
|
ba02eb90a0 | ||
|
|
fdc7716bf3 | ||
|
|
a050c00fbc | ||
|
|
d395c3e3cf | ||
|
|
c3e903f339 | ||
|
|
452ba40855 | ||
|
|
8854492cc5 | ||
|
|
105770cfa0 | ||
|
|
28b053a122 | ||
|
|
033c7387e2 | ||
|
|
a72a388e1c | ||
|
|
440850f5a0 | ||
|
|
54328e810f | ||
|
|
a256c5578e | ||
|
|
ec65f8d48a | ||
|
|
f0fbf0241a | ||
|
|
51115133c4 | ||
|
|
6760cdbf14 | ||
|
|
383598b448 | ||
|
|
60fa61d05a | ||
|
|
a5df6a5a83 | ||
|
|
6760652582 | ||
|
|
b64d7488d0 | ||
|
|
5dd271d361 | ||
|
|
3b16807374 | ||
|
|
2a3a38ef78 | ||
|
|
0c7d09c570 | ||
|
|
c4f03dcee1 | ||
|
|
d69d771131 | ||
|
|
7d57dc9d9b | ||
|
|
7f86ea3334 | ||
|
|
a54e901479 | ||
|
|
05be7aa591 | ||
|
|
203d540c3a | ||
|
|
d7002a1a99 | ||
|
|
d2c9824ef9 | ||
|
|
04c9c244f1 | ||
|
|
00abb3b6e3 | ||
|
|
cf5e7d9f78 | ||
|
|
9e560c7909 | ||
|
|
f9c654b258 | ||
|
|
7e72cdf240 | ||
|
|
bdc2e99c29 | ||
|
|
c0ebd7a7a0 | ||
|
|
57fc6ff85c | ||
|
|
46f6ba563b | ||
|
|
3701740f9c | ||
|
|
43155fbe1f | ||
|
|
022cb66020 | ||
|
|
71562a679f | ||
|
|
96e79773c0 | ||
|
|
4ba750b41b | ||
|
|
6d89878dfc | ||
|
|
352b95544a | ||
|
|
a66cea94b1 | ||
|
|
ee758f987b | ||
|
|
ca1868e2a4 | ||
|
|
3667799f64 | ||
|
|
3b3c15b979 | ||
|
|
9394612a3d | ||
|
|
1e3041b579 | ||
|
|
ba619b1c3e | ||
|
|
e0e76a4257 | ||
|
|
40799f2688 | ||
|
|
e1b14cad05 | ||
|
|
0a33059a10 | ||
|
|
c9c1068fe5 | ||
|
|
d2ac69eafa | ||
|
|
917ad8ff2e | ||
|
|
46fa678f42 | ||
|
|
07de1abf21 | ||
|
|
df3239bab7 | ||
|
|
efedd91297 | ||
|
|
60733b8d7c | ||
|
|
b82ad0a512 | ||
|
|
fa4cbdf580 | ||
|
|
6c0e49f0b7 | ||
|
|
949450e9ba | ||
|
|
28671b0909 | ||
|
|
308c1d3b7b | ||
|
|
1faf678709 | ||
|
|
7312b40458 | ||
|
|
85bfb1e4da | ||
|
|
847dbe11e2 | ||
|
|
cfe53b0100 | ||
|
|
464f837edb | ||
|
|
8a39b80dd0 | ||
|
|
9d87fe434e | ||
|
|
9938ff428d | ||
|
|
2ed9f6e006 | ||
|
|
ba5646432e | ||
|
|
bd7bae5835 | ||
|
|
c6027f94ad | ||
|
|
a8fbfd096e | ||
|
|
60b6dc9a50 | ||
|
|
72454e9443 | ||
|
|
e78cb1a4b2 | ||
|
|
7bc73a3d18 | ||
|
|
bae1cc7cdf | ||
|
|
2df32865ad | ||
|
|
0a953d75fb | ||
|
|
54ea2b8715 | ||
|
|
48e385b886 | ||
|
|
3e0799ac22 | ||
|
|
ba18c3a0dd | ||
|
|
14fde590a8 | ||
|
|
c137ed5ad3 | ||
|
|
4ed86b60e4 | ||
|
|
82b28f932b | ||
|
|
9e40b8f786 | ||
|
|
e9eec1019a | ||
|
|
a94911881d | ||
|
|
f6ef422f4c | ||
|
|
2121cbd086 | ||
|
|
808500786c | ||
|
|
2b271d96f4 | ||
|
|
352c753524 | ||
|
|
1f62a456d1 | ||
|
|
5d773d9978 | ||
|
|
8872b2614e | ||
|
|
806b9c4cf6 | ||
|
|
9dc8d21a41 | ||
|
|
ea4e4dccbd | ||
|
|
8e819e8ac1 | ||
|
|
cf4272d22c | ||
|
|
9f9f56732f | ||
|
|
a4a2a6e2c1 | ||
|
|
aa7f19718c | ||
|
|
b468c456ae | ||
|
|
b0d4288174 | ||
|
|
98daa2def1 | ||
|
|
3cd953a49d | ||
|
|
cbfab981a3 | ||
|
|
00535b114c | ||
|
|
2d6df90b37 | ||
|
|
1a3832618e | ||
|
|
499781d5dd | ||
|
|
3ff002b8ef | ||
|
|
6fb8efc648 | ||
|
|
25ad73b67d | ||
|
|
ac4548f7cc | ||
|
|
5d57c91174 | ||
|
|
f40638d294 | ||
|
|
552b34a382 | ||
|
|
2e6c9c028f | ||
|
|
0170bc8266 | ||
|
|
123275eb92 | ||
|
|
51801f769a | ||
|
|
8ffb10309c | ||
|
|
2b05b8dcf3 | ||
|
|
b3e48bc1cb | ||
|
|
b84481958a | ||
|
|
fe591153da | ||
|
|
e1f347b75a | ||
|
|
fbe922c52c | ||
|
|
6f62b501be | ||
|
|
2a86a7087c | ||
|
|
02783343cb | ||
|
|
ffd8a5fa71 | ||
|
|
ba4722e794 | ||
|
|
d18f707dc9 | ||
|
|
4f4a69971b | ||
|
|
739de75187 | ||
|
|
941fbea976 | ||
|
|
00c8125bb4 | ||
|
|
1bfebe66a3 | ||
|
|
4713a734ad | ||
|
|
d0a3fdbfd7 | ||
|
|
1e4e05ef78 | ||
|
|
5cfaf11478 | ||
|
|
3cb9b8a671 | ||
|
|
dd9b802497 | ||
|
|
16730b59c0 | ||
|
|
095a3cc3bd | ||
|
|
45f29d226f | ||
|
|
3cef2ccf14 | ||
|
|
7ccabcca0a | ||
|
|
a58b74d1ac | ||
|
|
fe2b3b09c2 | ||
|
|
d17d17cd17 | ||
|
|
8ca5c720c8 | ||
|
|
9b90eb1f4f | ||
|
|
8e7befb108 | ||
|
|
d36d4a6be7 | ||
|
|
14c33a606f | ||
|
|
52f01c22bc | ||
|
|
9071853fe8 | ||
|
|
5b097ae638 | ||
|
|
1b6321e923 | ||
|
|
b690c0077a | ||
|
|
0501d8e276 | ||
|
|
86afbe9bea | ||
|
|
796c4e148a | ||
|
|
e789d389be | ||
|
|
b609add982 | ||
|
|
577797febb | ||
|
|
c16cc7aa9d | ||
|
|
b169129315 | ||
|
|
49d865b1a0 | ||
|
|
19f8951836 | ||
|
|
8dbb3c7d92 | ||
|
|
9a145026eb | ||
|
|
0607acd803 | ||
|
|
463b5d0888 | ||
|
|
35d8bd8639 | ||
|
|
83f5942090 | ||
|
|
151401c70d | ||
|
|
b293e29c5f | ||
|
|
3d279546ab | ||
|
|
bc20f8d0a1 | ||
|
|
4b6b810986 | ||
|
|
2f0e0f2b92 | ||
|
|
3e83ca9ef1 | ||
|
|
a903bda4e7 | ||
|
|
970b096206 | ||
|
|
d844c8e6ba | ||
|
|
0c8414d739 | ||
|
|
3578be2a22 | ||
|
|
6632d559d6 | ||
|
|
cd4701aeb3 | ||
|
|
04a3876e3f | ||
|
|
e7f0559255 | ||
|
|
b8ce1f8053 | ||
|
|
ec1613d847 | ||
|
|
cb0f3ef836 | ||
|
|
fdd15a86ee | ||
|
|
97bd828a03 | ||
|
|
de4388f2f8 | ||
|
|
2500d74b75 | ||
|
|
0170e1282f | ||
|
|
e154428541 | ||
|
|
4863b4cde4 | ||
|
|
71caf1f599 | ||
|
|
1d7ea39d74 | ||
|
|
a6aeeb90e6 | ||
|
|
ed272a96cb | ||
|
|
d5a1cf5107 | ||
|
|
2c8e75c3d3 | ||
|
|
09a70f4444 | ||
|
|
5957003b83 | ||
|
|
69e2fb6207 | ||
|
|
57ec559b33 | ||
|
|
6cfe09c50b | ||
|
|
4c27216f25 | ||
|
|
642ae17bc7 | ||
|
|
11f3d22c7c | ||
|
|
02b29740b2 | ||
|
|
620ed20210 | ||
|
|
828be52841 | ||
|
|
59c5ba5e73 | ||
|
|
d97a04e4fd | ||
|
|
afa860ab4a | ||
|
|
a58bbc7141 | ||
|
|
4d0ccc7545 | ||
|
|
67dec4683d | ||
|
|
3940206b03 | ||
|
|
e72bcf9013 | ||
|
|
35505839aa | ||
|
|
6f6e45f38a | ||
|
|
a7150c1f1c | ||
|
|
6a3345618a | ||
|
|
f0c4dc78ee | ||
|
|
1e583993a9 | ||
|
|
477e588acc | ||
|
|
77ee8afe9a | ||
|
|
6d8ce83715 | ||
|
|
123eb4e6d2 | ||
|
|
60a472e1ae | ||
|
|
9529e528cc | ||
|
|
a3b1ba5a44 | ||
|
|
cf9a833ff9 | ||
|
|
8df4898bd1 | ||
|
|
6a8fa2c7ef | ||
|
|
a570d919da | ||
|
|
21de3ae040 | ||
|
|
ef7fba17a0 | ||
|
|
590daa8ba7 | ||
|
|
a23f179e42 | ||
|
|
0ebc911237 | ||
|
|
7dd86ec1f7 | ||
|
|
560e721b9b | ||
|
|
642a794ace | ||
|
|
f3a177629f | ||
|
|
83f75008ca | ||
|
|
387aa6eb17 | ||
|
|
9e8749b7c6 | ||
|
|
9254b42b31 | ||
|
|
f1cfa523ff | ||
|
|
b247d7c991 | ||
|
|
4056d05527 | ||
|
|
3a1ea76bc0 | ||
|
|
26e6938ff1 | ||
|
|
649914c394 | ||
|
|
69d085abd4 | ||
|
|
b7e0e1002b | ||
|
|
e6f9cfb535 | ||
|
|
e85bcd86e5 | ||
|
|
09d59d4d19 | ||
|
|
c126521868 | ||
|
|
cb9c6e196f | ||
|
|
b7b6480b9c | ||
|
|
14af6e451a | ||
|
|
d0b2e7279b | ||
|
|
dec321f286 | ||
|
|
b9ba6aece9 | ||
|
|
e280d89951 | ||
|
|
6d3e3fa7d1 | ||
|
|
058b2243df | ||
|
|
7c102e8461 | ||
|
|
012ccf980a | ||
|
|
e465d2f5d6 | ||
|
|
d46f0bd825 | ||
|
|
1ae0e436f7 | ||
|
|
706b3b122f | ||
|
|
8dc4e06c73 | ||
|
|
66334fe9c7 | ||
|
|
97119f17ea | ||
|
|
99747c564f | ||
|
|
a01d7751ec | ||
|
|
2063105cbd | ||
|
|
ee9c5065a7 | ||
|
|
1c4ddd6c21 | ||
|
|
66a289056e | ||
|
|
fd871eab82 | ||
|
|
316cdf81e7 | ||
|
|
0331f3b9bf | ||
|
|
c0ae3645f7 | ||
|
|
0eb3fbbf41 | ||
|
|
1055b0b33b | ||
|
|
c6da1cfaa8 | ||
|
|
f52f2c7952 | ||
|
|
c5e3480f99 | ||
|
|
b3993aca6e | ||
|
|
9f334a73ca | ||
|
|
b3debbe1e8 | ||
|
|
fd49a67f34 | ||
|
|
3c026a8c5e | ||
|
|
a861e5b732 | ||
|
|
9cac52833e | ||
|
|
0f5585dab8 | ||
|
|
860f03f4c9 | ||
|
|
f7aa2d7228 | ||
|
|
f664700258 | ||
|
|
553b2b22a9 | ||
|
|
5120faa198 | ||
|
|
d199a18075 | ||
|
|
4253fdaead | ||
|
|
b9b60c2631 | ||
|
|
0883353f72 | ||
|
|
b23b94e8b5 | ||
|
|
5f68e9b58f | ||
|
|
8d3586814b | ||
|
|
afb2cc01fb | ||
|
|
78551c11c9 | ||
|
|
87d5255861 | ||
|
|
96c187ebbd | ||
|
|
36a1a984cb | ||
|
|
1f73984092 | ||
|
|
0a0487a6d4 | ||
|
|
02c50bb741 | ||
|
|
d1894ba0a8 | ||
|
|
b24bbd471e | ||
|
|
9f8593fd6b | ||
|
|
69f8c2c776 | ||
|
|
83d40bc555 | ||
|
|
587453bd48 | ||
|
|
a53e452079 | ||
|
|
58971401f1 | ||
|
|
9213f61dd9 | ||
|
|
747894bc89 | ||
|
|
2a6824282b | ||
|
|
6803c87465 | ||
|
|
0735f7360a | ||
|
|
212ead5991 | ||
|
|
13d66dac57 | ||
|
|
923e903ee2 | ||
|
|
d894408c91 | ||
|
|
5e7cc4cb6a | ||
|
|
31aea21ccb | ||
|
|
831e8b055d | ||
|
|
ba61b5c0d9 | ||
|
|
70c9632eb0 | ||
|
|
1474e212ab | ||
|
|
1059b86199 | ||
|
|
2235463265 | ||
|
|
27243d89e7 | ||
|
|
94b507841b | ||
|
|
84c1044164 | ||
|
|
961fa3116a | ||
|
|
60fafbd778 | ||
|
|
913debdd44 | ||
|
|
223f5cbff6 | ||
|
|
2cb5dce891 | ||
|
|
43ca597246 | ||
|
|
9840079335 | ||
|
|
4c1b559455 | ||
|
|
f527de5af1 | ||
|
|
121b585f9c | ||
|
|
802461b4ec | ||
|
|
a7e7c6aecd | ||
|
|
3efda3155c | ||
|
|
1f1a59d9f2 | ||
|
|
0f47a58bf8 | ||
|
|
5ac482e8ca | ||
|
|
72405592ad | ||
|
|
d6d2e8220d | ||
|
|
807e205b75 | ||
|
|
ff07dfb055 | ||
|
|
5353f971ae | ||
|
|
449c9a5f8b | ||
|
|
d44da48ee1 | ||
|
|
59605b459f | ||
|
|
e6f47dcabd | ||
|
|
9504103f30 | ||
|
|
4d740be5ef | ||
|
|
4075613727 | ||
|
|
22f70afe4c | ||
|
|
2a5d6e62de | ||
|
|
808eb2cb83 | ||
|
|
e053b25ccc | ||
|
|
b03cd62c19 | ||
|
|
3ae4ef7c03 | ||
|
|
4477115675 | ||
|
|
029a30000a | ||
|
|
291a9307fd | ||
|
|
ee9f38635e | ||
|
|
99f504e9dc | ||
|
|
9a959eac25 | ||
|
|
885b886982 | ||
|
|
a914197b52 | ||
|
|
c5d5e7f462 | ||
|
|
0a1b50e613 | ||
|
|
9c5bc2c205 | ||
|
|
52a8795d86 | ||
|
|
95e637aaa8 | ||
|
|
97d6588bbd | ||
|
|
b50e0f4ea7 | ||
|
|
f1e2144cd3 | ||
|
|
ebf31180f7 | ||
|
|
a1b3565e95 | ||
|
|
32fc3ec1dd | ||
|
|
99fa1dc788 | ||
|
|
7c31ce174a | ||
|
|
b08593b9d1 | ||
|
|
d826167865 | ||
|
|
b333510e61 | ||
|
|
919fb4cd5b | ||
|
|
030abba10c | ||
|
|
1877295a71 | ||
|
|
c8c42374a9 | ||
|
|
0643dff90c | ||
|
|
c6dc55d904 | ||
|
|
f6a7d3f6ef | ||
|
|
98c6bba436 | ||
|
|
cd1acc00a1 | ||
|
|
a3dc14c13f | ||
|
|
9540e9fd6a | ||
|
|
6e11c0df55 | ||
|
|
2cd4f21782 | ||
|
|
5ec685e041 | ||
|
|
99c86d49e4 | ||
|
|
af9b7889f4 | ||
|
|
ce5602bee1 | ||
|
|
b3201224b3 | ||
|
|
ec14cb6763 | ||
|
|
a3eefbe18a | ||
|
|
18284eeb7d | ||
|
|
783ceb4507 | ||
|
|
25972a4a03 | ||
|
|
ff88c12750 | ||
|
|
9860d0dbba | ||
|
|
6c5ad7b661 | ||
|
|
87ccbafced | ||
|
|
d41c0793e4 | ||
|
|
bba4d0ecef | ||
|
|
6a2f092ce6 | ||
|
|
ca674dc362 | ||
|
|
553bbb1668 | ||
|
|
e28bd03937 | ||
|
|
a3c6d12d4b | ||
|
|
1d1702810b | ||
|
|
cbc6029fec | ||
|
|
1956e95808 | ||
|
|
68c7631764 | ||
|
|
1bb1067273 | ||
|
|
94b1746b35 | ||
|
|
7e56b1e68e | ||
|
|
5f502d0bca | ||
|
|
4ad5749d00 | ||
|
|
467496ddba | ||
|
|
accadf541c | ||
|
|
6d642f819b | ||
|
|
7ce98af5a8 | ||
|
|
cacdca6336 | ||
|
|
dc24889fee | ||
|
|
1fa111de78 | ||
|
|
08e5df1c92 | ||
|
|
0f5fffc0cf | ||
|
|
44cb22a325 | ||
|
|
6c1894de6b | ||
|
|
3366cebc7d | ||
|
|
f2dd45025f | ||
|
|
9a78cbf96b | ||
|
|
a6cc1cc881 | ||
|
|
26dba62c07 | ||
|
|
0934eb6317 | ||
|
|
83bf61401b | ||
|
|
eb33048dd0 | ||
|
|
f418ed247c | ||
|
|
e7a6e690b7 | ||
|
|
f2c63bf030 | ||
|
|
c966088145 | ||
|
|
460f3a59c5 | ||
|
|
e077123c43 | ||
|
|
6fb04246d7 | ||
|
|
2e90f20723 | ||
|
|
38281616e4 | ||
|
|
bffb79e553 | ||
|
|
0e6b8732ba | ||
|
|
d3ce882a77 | ||
|
|
b31cb34a7b | ||
|
|
7e4d381078 | ||
|
|
46e4539ced | ||
|
|
810340e2fd | ||
|
|
3490a146be | ||
|
|
6e3c9c3f4e | ||
|
|
86739c56a6 | ||
|
|
076dece778 | ||
|
|
6f7ce49aa8 | ||
|
|
f88676db9b | ||
|
|
9ff1133c39 | ||
|
|
8e7c458e8f | ||
|
|
6b0372afe1 | ||
|
|
14b00b1a0b | ||
|
|
2e0ca9d1c8 | ||
|
|
c33af3aa39 | ||
|
|
6dc1e136e1 | ||
|
|
c1dba7abfa | ||
|
|
fa7f2e218d | ||
|
|
9b6ec7bae7 | ||
|
|
47d017be67 | ||
|
|
2b958c015c | ||
|
|
82b8c81004 | ||
|
|
7c8f18c148 | ||
|
|
4470c39ac7 | ||
|
|
1586801de9 | ||
|
|
e621e9744b | ||
|
|
3c9ff7b1ae | ||
|
|
620f7f6877 | ||
|
|
f1c4436dda | ||
|
|
7691ae9d30 | ||
|
|
4c839cae7e | ||
|
|
6c30171053 | ||
|
|
4e9dce2820 | ||
|
|
f38895cebb | ||
|
|
8a743c1ea9 | ||
|
|
776225f4e1 | ||
|
|
faefb0b882 | ||
|
|
e2c78e8ba9 | ||
|
|
21bca0ac24 | ||
|
|
554971c613 | ||
|
|
c88bb83e42 | ||
|
|
fedcff1597 | ||
|
|
4b75891326 | ||
|
|
13b5e53efa | ||
|
|
19ba37364a | ||
|
|
171d78e27d | ||
|
|
abf0793fb3 | ||
|
|
b23b75d709 | ||
|
|
74d33cba0c | ||
|
|
22dbddd3e8 | ||
|
|
8564cb84b1 | ||
|
|
c38465b38e | ||
|
|
603faa4396 | ||
|
|
54c1f280a4 | ||
|
|
ced30e1abc | ||
|
|
0fc80428cb | ||
|
|
23a68035be | ||
|
|
40f0f68016 | ||
|
|
b38a637a8d | ||
|
|
6f3e1e021a | ||
|
|
78f5e9730b | ||
|
|
f08775f403 | ||
|
|
d9ba93cee3 | ||
|
|
d43bd5fb40 | ||
|
|
504553a448 | ||
|
|
26e10fefee | ||
|
|
63a70cb19f | ||
|
|
a369190f42 | ||
|
|
877a0d865e | ||
|
|
b3ec50554a | ||
|
|
efdf1b2757 | ||
|
|
74549ec5e0 | ||
|
|
495148e27b | ||
|
|
eddadb2b6f | ||
|
|
2366f85df0 | ||
|
|
7f6cc35ae6 | ||
|
|
f90f926fcd | ||
|
|
98e60a8425 | ||
|
|
1891b64ee0 | ||
|
|
c0c5a486f9 | ||
|
|
e1935e0689 | ||
|
|
280f39dbd1 | ||
|
|
b98c5acfe0 |
@@ -1,18 +0,0 @@
|
||||
version: 1-{branch}+{build}
|
||||
|
||||
build_script:
|
||||
- cmd: C:\MinGW\msys\1.0\bin\make
|
||||
- cmd: rmdir /s /q .git
|
||||
before_test:
|
||||
- cmd: set PATH=%PATH%;C:\Program Files\erl8.3\erts-8.3\bin
|
||||
test_script:
|
||||
- cmd: C:\MinGW\msys\1.0\bin\make --keep-going test_windows
|
||||
|
||||
environment:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- platform: x86
|
||||
- platform: x64
|
||||
- platform: Any CPU
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
env:
|
||||
CIRRUS_CLONE_DEPTH: 50
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "+warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
test_template: &DEFAULT_TEST_SETTINGS
|
||||
# don't cancel the task execution if it's master or a release branch
|
||||
auto_cancellation: $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH !=~ 'v\d+\.\d+.*'
|
||||
|
||||
test_linux_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
container:
|
||||
image: buildpack-deps:trusty
|
||||
cpu: 8
|
||||
memory: 1536Mi
|
||||
|
||||
env:
|
||||
PATH: "${CIRRUS_WORKING_DIR}/otp/bin:${PATH}"
|
||||
|
||||
matrix:
|
||||
- name: Linux, ${OTP_RELEASE}, Ubuntu 14.04
|
||||
alias: Linux Stable
|
||||
matrix:
|
||||
- env:
|
||||
CHECK_POSIX_COMPLIANT: true
|
||||
CHECK_REPRODUCIBLE: true
|
||||
OTP_RELEASE: OTP-22.1
|
||||
- env:
|
||||
OTP_RELEASE: OTP-22.0
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.3.8
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.2
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.1
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.0
|
||||
|
||||
- name: Linux, OTP-${OTP_RELEASE}, development, Ubuntu 14.04
|
||||
alias: Linux Development
|
||||
allow_failures: true
|
||||
skip_notifications: true
|
||||
depends_on:
|
||||
- Linux Stable
|
||||
- FreeBSD Stable
|
||||
matrix:
|
||||
- env:
|
||||
OTP_RELEASE: master
|
||||
- env:
|
||||
OTP_RELEASE: maint
|
||||
|
||||
install_script:
|
||||
- wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${OTP_RELEASE}.tar.gz
|
||||
- mkdir -p otp
|
||||
- tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
- otp/Install -minimal ${CIRRUS_WORKING_DIR}/otp
|
||||
- rm -rf .git
|
||||
- make compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- make test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
dialyzer_script: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
|
||||
test_erlang_script: make test_erlang
|
||||
|
||||
test_elixir_script: make test_elixir
|
||||
|
||||
check_posix_compliant_script: |
|
||||
if [ -n "$CHECK_POSIX_COMPLIANT" ]; then
|
||||
apt update
|
||||
apt install -y shellcheck
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||
else
|
||||
echo "The format of the shell scripts is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
|
||||
check_reproducible_script: |
|
||||
if [ -n "$CHECK_REPRODUCIBLE" ]; then
|
||||
make check_reproducible
|
||||
else
|
||||
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
|
||||
|
||||
test_windows_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: Windows, OTP-${OTP_RELEASE}, Windows Server 2019
|
||||
alias: Windows Stable
|
||||
|
||||
matrix:
|
||||
- env:
|
||||
OS_VERSION: 2019
|
||||
OTP_RELEASE: 22.0
|
||||
|
||||
- env:
|
||||
OS_VERSION: 2019
|
||||
OTP_RELEASE: 21.0.1
|
||||
|
||||
windows_container:
|
||||
image: fertapric/elixir-ci:otp-win64-${OTP_RELEASE}
|
||||
os_version: ${OS_VERSION}
|
||||
cpu: 4
|
||||
memory: 3840Mi
|
||||
|
||||
install_script:
|
||||
- rmdir /s /q .git
|
||||
- make compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- make test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
test_erlang_script: make --keep-going test_erlang
|
||||
|
||||
test_elixir_script: make --keep-going test_elixir
|
||||
|
||||
|
||||
test_freebsd_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: FreeBSD 12.0
|
||||
alias: FreeBSD Stable
|
||||
|
||||
freebsd_instance:
|
||||
image_family: freebsd-12-0
|
||||
cpu: 8
|
||||
memory: 7424Mi
|
||||
|
||||
env:
|
||||
CHECK_REPRODUCIBLE: true
|
||||
LC_ALL: en_US.UTF-8
|
||||
|
||||
install_script:
|
||||
- sudo pkg update
|
||||
- pkg install -y erlang git gmake
|
||||
- rm -rf .git
|
||||
- gmake compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- gmake test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
dialyzer_script: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
|
||||
test_erlang_script: gmake test_erlang
|
||||
|
||||
test_elixir_script: gmake test_elixir
|
||||
|
||||
check_reproducible_script: |
|
||||
if [ -n "$CHECK_REPRODUCIBLE" ]; then
|
||||
gmake check_reproducible
|
||||
else
|
||||
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
@@ -0,0 +1,17 @@
|
||||
on: check_suite
|
||||
name: CI email
|
||||
jobs:
|
||||
sendEmail:
|
||||
name: Send email
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send email
|
||||
# Source: https://github.com/elixir-lang/elixir-ci
|
||||
uses: docker://fertapric/elixir-ci-email:latest
|
||||
env:
|
||||
APP_NAME: Cirrus CI
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAIL_FROM: ci@elixir-lang.org
|
||||
MAIL_HOST: smtp.sendgrid.net
|
||||
MAIL_PASSWORD: ${{ secrets.CI_EMAIL_PASSWORD }}
|
||||
MAIL_USERNAME: ${{ secrets.CI_EMAIL_USERNAME }}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
language: bash
|
||||
sudo: false
|
||||
|
||||
env:
|
||||
global:
|
||||
- ELIXIR_ASSERT_TIMEOUT=2000
|
||||
matrix:
|
||||
- OTP_RELEASE=OTP-22.0 CHECK_REPRODUCIBLE=true CHECK_POSIX_COMPLIANT=true
|
||||
- OTP_RELEASE=OTP-21.3.8
|
||||
- OTP_RELEASE=OTP-21.2
|
||||
- OTP_RELEASE=OTP-21.1
|
||||
- OTP_RELEASE=OTP-21.0
|
||||
- OTP_RELEASE=OTP-20.3
|
||||
- OTP_RELEASE=OTP-20.2
|
||||
- OTP_RELEASE=OTP-20.1
|
||||
- OTP_RELEASE=OTP-20.0
|
||||
- OTP_RELEASE=maint
|
||||
- OTP_RELEASE=master
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: OTP_RELEASE=maint
|
||||
- env: OTP_RELEASE=master
|
||||
|
||||
install:
|
||||
- wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${OTP_RELEASE}.tar.gz
|
||||
- mkdir -p otp
|
||||
- tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
- otp/Install -minimal $(pwd)/otp
|
||||
- PATH=$(pwd)/otp/bin:$PATH
|
||||
|
||||
script:
|
||||
- rm -rf .git
|
||||
- ELIXIRC_OPTS="--warnings-as-errors" ERLC_OPTS="+warning_as_errors" make compile
|
||||
- make test
|
||||
- dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
|
||||
# Check for reproducible builds only in the latest OTP release
|
||||
- if [ -n "$CHECK_REPRODUCIBLE" ]; then make check_reproducible; fi
|
||||
|
||||
# Check for POSIX compliant shell scripts
|
||||
- if [ -n "$CHECK_POSIX_COMPLIANT" ]; then
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant";
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant";
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant";
|
||||
fi
|
||||
+246
-111
@@ -1,182 +1,317 @@
|
||||
# Changelog for Elixir v1.9
|
||||
# Changelog for Elixir v1.10
|
||||
|
||||
## Releases
|
||||
## Support for Erlang/OTP 21+
|
||||
|
||||
The main feature in Elixir v1.9 is the addition of releases. A release is a self-contained directory that consists of your application code, all of its dependencies, plus the whole Erlang Virtual Machine (VM) and runtime. Once a release is assembled, it can be packaged and deployed to a target as long as the target runs on the same operating system (OS) distribution and version as the machine running the `mix release` command.
|
||||
Elixir v1.10 requires Erlang/OTP 21+, allowing Elixir to integrate with Erlang/OTP's new logger. Currently, this means that the logger level, logger metadata, as well as all log messages are now shared between Erlang and Elixir APIs.
|
||||
|
||||
You can start a new project and assemble a release for it in three easy steps:
|
||||
We will continue improving the relationship between the logging systems in future releases. In particular, we plan to expose all log levels and runtime filtering functionalities available in Erlang directly into Elixir in the next Elixir version.
|
||||
|
||||
$ mix new my_app
|
||||
$ cd my_app
|
||||
$ MIX_ENV=prod mix release
|
||||
This release also adds two new guards, `is_struct/1` and `is_map_key/2`, thanks to the strict requirement on Erlang/OTP 21+.
|
||||
|
||||
A release will be assembled in `_build/prod/rel/my_app`. Inside the release, there will be a `bin/my_app` file which is the entry point to your system. It supports multiple commands, such as:
|
||||
## Releases improvements
|
||||
|
||||
* `bin/my_app start`, `bin/my_app start_iex`, `bin/my_app restart`, and `bin/my_app stop` - for general management of the release
|
||||
Elixir v1.9 introduced releases as a mechanism to package self-contained applications. Elixir v1.10 further improves releases with bug fixes and new enhancements based on feedback we got from the community. The highlights are:
|
||||
|
||||
* `bin/my_app rpc COMMAND` and `bin/my_app remote` - for running commands on the running system or to connect to the running system
|
||||
* Allow the dual boot system of releases to be disabled on environments that are boot-time sensitive, such as embedded devices
|
||||
|
||||
* `bin/my_app eval COMMAND` - to start a fresh system that runs a single command and then shuts down
|
||||
* Track and raise if compile-time configuration is set or changes at runtime (more in the next section)
|
||||
|
||||
* `bin/my_app daemon` and `bin/my_app daemon_iex` - to start the system as a daemon on Unix-like systems
|
||||
* Support for easily adding extra files to releases via overlays
|
||||
|
||||
* `bin/my_app install` - to install the system as a service on Windows machines
|
||||
* Allow `RELEASE_DISTRIBUTION` to be set to `none` in order to fully disable it
|
||||
|
||||
### Why releases?
|
||||
* Add a built-in `:tar` step that automatically packages releases
|
||||
|
||||
Releases allow developers to precompile and package all of their code and the runtime into a single unit. The benefits of releases are:
|
||||
See the full CHANGELOG for more improvements.
|
||||
|
||||
* Code preloading. The VM has two mechanisms for loading code: interactive and embedded. By default, it runs in the interactive mode which dynamically loads modules when they are used for the first time. The first time your application calls `Enum.map/2`, the VM will find the `Enum` module and load it. There’s a downside. When you start a new server in production, it may need to load many other modules, causing the first requests to have an unusual spike in response time. Releases run in embedded mode, which loads all available modules upfront, guaranteeing your system is ready to handle requests after booting.
|
||||
## Improvements to sort-based APIs in Enum
|
||||
|
||||
* Configuration and customization. Releases give developers fine grained control over system configuration and the VM flags used to start the system.
|
||||
`Enum.sort/1` in Elixir by default sorts from lowest to highest:
|
||||
|
||||
* Self-contained. A release does not require the source code to be included in your production artifacts. All of the code is precompiled and packaged. Releases do not even require Erlang or Elixir in your servers, as they include the Erlang VM and its runtime by default. Furthermore, both Erlang and Elixir standard libraries are stripped to bring only the parts you are actually using.
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"])
|
||||
["apple", "banana", "pineapple"]
|
||||
```
|
||||
|
||||
* Multiple releases. You can assemble different releases with different configuration per application or even with different applications altogether.
|
||||
If you want to sort from highest to lowest, you need to call `Enum.sort/2` with a custom sorting function, such as `Enum.sort(collection, &>=/2)`, which is not immediately obvious to someone reading the code:
|
||||
|
||||
### Hooks and Configuration
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], &>=/2)
|
||||
["pineapple", "banana", "apple"]
|
||||
```
|
||||
|
||||
Releases also provide built-in hooks for configuring almost every need of the production system:
|
||||
Furthermore, comparison operators, such as `<=` and `>=`, perform structural sorting, instead of a semantic one. For example, using `>=` to sort dates descendingly won't yield the correct result:
|
||||
|
||||
* `config/config.exs` (and `config/prod.exs`) - provides build-time application configuration, which is executed when the release is assembled
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]])
|
||||
[~D[2020-01-01], ~D[2019-12-31]]
|
||||
```
|
||||
|
||||
* `config/releases.exs` - provides runtime application configuration. It is executed every time the release boots and is further extensible via config providers
|
||||
To perform proper semantic comparison for dates, one would also need to pass a custom sorting function:
|
||||
|
||||
* `rel/vm.args.eex` - a template file that is copied into every release and provides static configuration of the Erlang Virtual Machine and other runtime flags
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], &(Date.compare(&1, &2) != :lt))
|
||||
[~D[2019-12-31], ~D[2020-01-01]]
|
||||
```
|
||||
|
||||
* `rel/env.sh.eex` and `rel/env.bat.eex` - template files that are copied into every release and executed on every command to set up environment variables, including ones specific to the VM, and the general environment
|
||||
Elixir v1.10 streamlines the sorting functions by introducing both `:asc` and `:desc` shortcuts:
|
||||
|
||||
We have written extensive documentation on releases, so we recommend checking it out for more information.
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], :asc)
|
||||
["apple", "banana", "pineapple"]
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], :desc)
|
||||
["pineapple", "banana", "apple"]
|
||||
```
|
||||
|
||||
## Configuration overhaul
|
||||
As well as adding the possibility to pass a module to perform semantic comparisons. For example, to sort dates, one now only needs to pass the `Date` module or even `{:desc, Date}` for descending semantical sort:
|
||||
|
||||
A new `Config` module has been added to Elixir. The previous configuration API, `Mix.Config`, was part of the Mix build tool. But since releases provide runtime configuration and Mix is not included in releases, we ported the `Mix.Config` API to Elixir. In other words, `use Mix.Config` has been soft-deprecated in favor of `import Config`.
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], Date)
|
||||
[~D[2019-12-31], ~D[2020-01-01]]
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], {:desc, Date})
|
||||
[~D[2020-01-01], ~D[2019-12-31]]
|
||||
```
|
||||
|
||||
Another important change related to configuration is that `mix new` will no longer generate a `config/config.exs` file. [Relying on configuration is undesired for most libraries](https://hexdocs.pm/elixir/library-guidelines.html#avoid-application-configuration) and the generated config files pushed library authors in the wrong direction. Furthermore, `mix new --umbrella` will no longer generate a configuration for each child app, instead all configuration should be declared in the umbrella root. That's how it has always behaved, we are now making it explicit.
|
||||
These API improvements make the code more concise and readable and they have also been added to `Enum.sort_by`, `Enum.min_by`, `Enum.max_by`, and friends.
|
||||
|
||||
## Tracking of compile-time configuration
|
||||
|
||||
In Elixir, we organize our code in applications. Libraries, your dependencies, and your own project are all separate applications. All applications in Elixir also come with an application environment.
|
||||
|
||||
The application environment is a key-value store that allows us to configure said application. While reading the application environment at runtime is the preferred approach, in some rare occasions you may want to use the application environment to configure the compilation of a certain project. This is often done by calling `Application.get_env/3` outside of a function:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.DBClient do
|
||||
@db_host Application.get_env(:my_app, :db_host, "db.local")
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: @db_host)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This approach has one big limitation: if you change the value of the application environment after the code is compiled, the value used at runtime is not going to change! For example, if you are using `mix release` and your `config/releases.exs` has:
|
||||
|
||||
config :my_app, :db_host, "db.production"
|
||||
|
||||
Because `config/releases.exs` is read after the code is compiled, the new value will have no effect as the code was compiled to connect to "db.local".
|
||||
|
||||
Of course, the obvious solution to this mismatch is to not read the application environment at compilation time in the first place, and instead move the code to inside a function:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.DBClient do
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: db_host())
|
||||
end
|
||||
defp db_host() do
|
||||
Application.get_env(:my_app, :db_host, "db.local")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
While this is the preferred approach, there are still two scenarios we need to address:
|
||||
|
||||
1. Not everyone may be aware of this pitfall, so they will mistakenly read the application environemnt at compile-time, until they are bitten by this behaviour
|
||||
|
||||
2. In rare occasions, you trully need to read the application environment at compile-time, and you want to be warned when you try to configure at runtime something that is valid only at compilation time
|
||||
|
||||
Elixir v1.10 aims to solve these two scenarios by introducing a `Application.compile_env/3` function. For example, to read the value at compile time, you can now do:
|
||||
|
||||
```elixir
|
||||
@db_host Application.compile_env(:my_app, :db_host, "db.local")
|
||||
```
|
||||
|
||||
By using `compile_env/3`, Elixir will store the values used during compilation and compare them with the runtime values whenever your system starts, raising an error in case they differ. This helps developers ensure they are running their production systems with the configuration they intend to.
|
||||
|
||||
In future versions, we will deprecate the use `Application.get_env` at compile-time with a clear message pointing users to configuration best practices, effectively addressing the scenario where users read from the application environment at compile time unaware of its pitfalls.
|
||||
|
||||
## Compiler tracing
|
||||
|
||||
This release brings enhancements to the Elixir compiler and adds new capabilities for developers to listen to compilation events.
|
||||
|
||||
In previous Elixir versions, Elixir would compile a database of cross references between modules (such as function calls, references, structs, etc) for each project in order to perform all kinds of checks, such as deprecations and undefined functions.
|
||||
|
||||
Although this database was not public, developers would still use it to run their own checks against their projects. With time, developers would request more data to be included in the database, which was problematic as Elixir itself did not have a use for the additional data, and the database was not meant to be used externally in the first place.
|
||||
|
||||
In Elixir v1.10, we have addressed these problems by introducing compiler tracing. The compiler tracing allows developers to listen to events as they are emitted by the compiler, so they can store all of the information they need - and only the information they need.
|
||||
|
||||
Elixir itself is using the new compiler tracing to provide new functionality. One advantage of this approach is that developers can now disable undefined function warnings directly on the callsite. For example, imagine you have an optional dependency which may not be available in some cases. You can tell the compiler to skip warning on calls to optional modules with:
|
||||
|
||||
@compile {:no_warn_undefined, OptionalDependency}
|
||||
defdelegate my_function_call(arg), to: OptionalDependency
|
||||
|
||||
Previously, this information had to be added to the overall project configuration, which was far away from where the optional call effectively happened.
|
||||
|
||||
## Other enhancements
|
||||
|
||||
There are many other enhancements. The Elixir CLI got a handful of new options in order to best support releases. `Logger` now computes its sync/async/discard thresholds in a decentralized fashion, reducing contention. `EEx` templates support more complex expressions than before. Finally, there is a new `~U` sigil for working with UTC DateTimes as well as new functions in the `File`, `Registry`, and `System` modules.
|
||||
Elixir's calendar data types got many improvements, such as sigil support for third-party calendars, as well as the additions of `DateTime.now!/2`, `DateTime.shift_zone!/3`, and `NaiveDateTime.local_now/0`.
|
||||
|
||||
## v1.9.0-dev
|
||||
There are many improvements related to Elixir's AST in this release too. First of all, `Code.string_to_quoted/2` has two new options, `:token_metadata` and `:literal_encoder`, that give more control over Elixir's parser. This information was already available to the Elixir code formatter and has now been made public. We have also extensively documented all of Elixir's AST metadata. These changes alongside compiler tracing means static analyzers and IDE integrations have a better foundation to analyze the source code.
|
||||
|
||||
### 1. Enhancements
|
||||
ExUnit, our test framework, ships two small but important improvements: `ExUnit.CaptureIO` can now be used by tests that run concurrently and we have added "pattern-matching diffing". To understand the last feature, take this code:
|
||||
|
||||
#### EEx
|
||||
```elixir
|
||||
assert %{"status" => 200, "body" => %{"key" => "foo"}} = json_payload
|
||||
```
|
||||
|
||||
* [EEx] Allow more complex mixed expressions when tokenizing
|
||||
Now imagine that `json_payload` is a large JSON blob and the `"key"` inside the `"body"` did not have value of `"foo"`. In previous Elixir versions, if the assertion failed, Elixir would print the right side and let you up to your own devices to figure out what went wrong. In Elixir v1.10, we diff the data structure against the pattern so you can see exactly which parts of the data matched the pattern and which ones did not. Note ExUnit already performed diffing when comparing data types, this new version adds diffing when matching data against a pattern.
|
||||
|
||||
## v1.10.1 (2020-02-10)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] Allow `Access.at/1` to handle negative index
|
||||
* [CLI] Add support for `--boot`, `--boot-var`, `--erl-config`, `--pipe-to`, `--rpc-eval`, and `--vm-args` options
|
||||
* [Code] Add `static_atom_encoder` option to `Code.string_to_quoted/2`
|
||||
* [Code] Support `:force_do_end_blocks` on `Code.format_string!/2` and `Code.format_file!/2`
|
||||
* [Code] Do not raise on deadlocks on `Code.ensure_compiled/1`
|
||||
* [Config] Add `Config`, `Config.Reader`, and `Config.Provider` modules for working with configuration
|
||||
* [File] Add `File.rename!/2`
|
||||
* [Inspect] Add `:inspect_fun` and `:custom_options` to `Inspect.Opts`
|
||||
* [Kernel] Add `~U` sigil for UTC date times
|
||||
* [Kernel] Optimize `&super/arity` and `&super(&1)`
|
||||
* [Kernel] Optimize generated code for `with` with a catch-all clause
|
||||
* [Kernel] Validate `__struct__` key in map returned by `__struct__/0,1`
|
||||
* [Module] Add `Module.get_attribute/3`
|
||||
* [Protocol] Improve `Protocol.UndefinedError` messages to also include the type that was attempted to dispatch on
|
||||
* [Protocol] Optimize performance of dynamic dispatching for non-consolidated protocols
|
||||
* [Record] Include field names in generated type for records
|
||||
* [Regex] Automatically recompile regexes
|
||||
* [Registry] Add `Registry.select/2`
|
||||
* [System] Add `System.restart/0`, `System.pid/0` and `System.no_halt/1`
|
||||
* [System] Add `System.get_env/2`, `System.fetch_env/1`, and `System.fetch_env!/1`
|
||||
* [System] Support `SOURCE_DATE_EPOCH` for reproducible builds
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Allow multiple `:exclude` on configuration/CLI
|
||||
* [ExUnit.DocTest] No longer wrap doctest errors in custom exceptions. They ended-up hiding more information than showing
|
||||
* [ExUnit.DocTest] Display the actual doctest code when doctest fails
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.CLI] Copy ticktime from remote node on IEx `--remsh`
|
||||
* [IEx.CLI] Automatically add a host on node given to `--remsh`
|
||||
* [Code] Do not emit invalid code when formatting `nil`, `false`, and `true` keys in maps
|
||||
* [Kernel] Ensure `with` clauses properly unpack "implicit guards" (such as matching on the struct name)
|
||||
* [Kernel] Do not warn if commas are used by themselves in `~w`/`~W` sigils
|
||||
* [Kernel] Do not validate the `:line` option in quote (the validation has been moved to v1.11 to give users more time to update their code)
|
||||
* [Module] Ensure the code verifier handles the `:erlang.size/1` guard properly
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Use a decentralized mode computation for Logger which allows overloads to be detected more quickly
|
||||
* [Logger] Use `persistent_term` to store configuration whenever available for performance
|
||||
* [Logger] Properly handle the `report_cb/2` option from Erlang
|
||||
* [Logger] Fix truncation for multi-byte characters
|
||||
* [Logger] Do not rebroadcast messages from remote nodes as this is now taken care by Erlang's logger
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Ensure `assert_receive` produces valid exception messages in case of errors
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Follow XDG base dir specification in Mix for temporary and configuration files
|
||||
* [Mix.Generator] Add `copy_file/3`, `copy_template/4`, and `overwite?/2`
|
||||
* [mix archive.uninstall] Allow `mix archive.uninstall APP` to uninstall any installed version of APP
|
||||
* [mix new] No longer generate a `config/` directory for mix new
|
||||
* [mix release] Add support for releases
|
||||
* [mix release.init] Add templates for release configuration
|
||||
* [mix test] Allow running tests for a given umbrella app from the umbrella root with `mix test apps/APP/test`. Test failures also include the `apps/APP` prefix in the test location
|
||||
* [mix release] Make sure the install command (Window specific) works on paths with spaces in the name
|
||||
* [mix release] Allow using `remote` and `rpc` commands with `Application.compile_env/3`
|
||||
|
||||
## v1.10.0 (2020-01-27)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Application] Add `Application.compile_env/3` and `Application.compile_env!/2` for reading values at compilation time and tracking if they accidentally change during runtime
|
||||
* [Calendar] Allow custom calendar representations in calendar sigils
|
||||
* [Calendar] Add `c:Calendar.parse_time/1`, `c:Calendar.parse_date/1`, `c:Calendar.parse_naive_datetime/1` and `c:Calendar.parse_utc_datetime/1` callbacks to calendar behaviour
|
||||
* [CLI] Add support for `NO_COLOR` environment variable
|
||||
* [Code] Add `:token_metadata` and `:literal_encoder` support to `Code.string_to_quoted/2`
|
||||
* [Code] Add compiler tracing to lift events done by the compiler
|
||||
* [Code] Return `{:error, :unavailable}` in `Code.ensure_compiled/1` if module is in a deadlock
|
||||
* [DateTime] Add `DateTime.now!/2` and `DateTime.shift_zone!/3`
|
||||
* [Enum] Speed up getting one random element from enumerables
|
||||
* [Enum] Add `Enum.frequencies/1`, `Enum.frequencies_by/2`, and `Enum.map_intersperse/2`
|
||||
* [Enum] Allow a sorting function on `Enum.min/max/min_by/max_by`
|
||||
* [Enum] Add `asc/desc` and `compare/1` support to `Enum.sort/2`
|
||||
* [Exception] Add version alongside app names in stacktraces
|
||||
* [Function] Add `Function.identity/1`
|
||||
* [Kernel] Add `Kernel.is_struct/1` and `Kernel.is_map_key/2`
|
||||
* [Kernel] Warn when function head comes immediately after the implementation instead of before the implementation
|
||||
* [Kernel] Warn if duplicate key is found in struct declaration
|
||||
* [Kernel] Print all undefined functions as warnings and then raise. This allows users to see all undefined calls at once, when it would otherwise require them to compile the code multiple times
|
||||
* [Kernel] Allow file, line and context to be dynamically set on `quote`
|
||||
* [Keyword] Add `Keyword.pop!/2` and `Keyword.pop_values/2`
|
||||
* [Map] Add `Map.pop!/2`
|
||||
* [MapSet] Optimize multiple operations
|
||||
* [Module] Add `Module.has_attribute?/2`
|
||||
* [Module] Add `@compile {:no_warn_undefined, mfa_or_module}` to turn off undefined function warnings
|
||||
* [NaiveDateTime] Add `NaiveDateTime.local_now/0`
|
||||
* [Record] Warn if duplicate key is found in record declaration
|
||||
* [String] Update to Unicode 12.1
|
||||
* [StringIO] Add `:encoding` option to StringIO and optimize `get_chars` operation
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Support diffs in pattern matching and in `assert_receive`
|
||||
* [ExUnit.CaptureIO] Supports capturing named devices in asynchronous tests
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Warn on circular file imports when loading default `.iex.exs`
|
||||
* [IEx] Allow customization of the continuation prompt on IEx
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Allow `start_options` to be configured on Logger's GenEvent
|
||||
* [Logger] Integrate Elixir's Logger with Erlang/OTP 21+'s logger. This means setting up the logger level in Elixir will automatically change the logger level for Erlang and vice-versa
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Add `--profile time` flag to profile compilation steps
|
||||
* [mix deps.compile] Add `--skip-umbrella-children` flag. The new flag does not compile umbrella apps. This is useful for building caches in CD/CI pipelines
|
||||
* [mix deps.unlock] Add `--check-unused` flag. The new flag raises if there are any unused dependencies in the lock file
|
||||
* [mix release] Allow `RELEASE_DISTRIBUTION` to be set to `none`
|
||||
* [mix release] Support overlays in `rel/overlays`
|
||||
* [mix release] Allow configuration reboot to be disabled in releases
|
||||
* [mix test] Add support for simple round-robin test partitioning across multiple machines
|
||||
* [Mix.Project] Add `MIX_DEPS_PATH` environment variable for setting `:deps_path`
|
||||
* [Mix.Project] Add `Mix.Project.deps_scms/1` that returns deps with their SCMs
|
||||
* [Mix.Task] Add `Mix.Task.Compiler.after_compiler/2` callback, to simplify compilers that may need to run something at multiple steps
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Consistently trim newlines when you have a single EEx expression per line on multiple lines
|
||||
* [EEx] Ensure multiline do/end with no spaces compile under trim mode
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Quote `::` in `Code.format_string!/1` to avoid ambiguity
|
||||
* [Enum] Ensure the first equal entry is returned by `Enum.min/2` and `Enum.max/2`
|
||||
* [Kernel] Improve error message when string interpolation is used in a guard
|
||||
* [Kernel] Properly merge and handle docs for callbacks with multiple clauses
|
||||
* [Kernel] Guarantee reproducible builds on modules with dozens of specs
|
||||
* [Kernel] Resolve `__MODULE__` accordingly in nested `defmodule` to avoid double nesting
|
||||
* [Kernel] Type variables starting with an underscore (`_foo`) should not raise compile error
|
||||
* [Kernel] Keep order of elements when macro `in/2` is expanded with a literal list on the right-hand side
|
||||
* [Kernel] Print proper location on undefined function error from dynamically generated functions
|
||||
* [System] Make sure `:init.get_status/0` is set to `{:started, :started}` once the system starts
|
||||
* [Path] Do not expand `~` in `Path.expand/2` when not followed by a path separator
|
||||
* [Protocol] Ensure `debug_info` is kept in protocols
|
||||
* [Regex] Ensure inspect returns valid `~r//` expressions when they are manually compiled with backslashes
|
||||
* [Registry] Fix ETS leak in `Registry.register/2` for already registered calls in unique registries while the process is still alive
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Raise error if attempting to run single line tests on multiple files
|
||||
* [ExUnit] Return proper error on duplicate child IDs on `start_supervised`
|
||||
* [Enum] Allow positive range slices on infinite streams given to `Enum.slice/2`
|
||||
* [Kernel] Raise error on functions/guards without implementation
|
||||
* [Kernel] Do not expand expressions inside interpolation twice
|
||||
* [Keyword] Ensure keyword replace and update preserve order
|
||||
* [Module] Raise instead of silently failing when performing a write module operation during after-compile
|
||||
* [Module] Fix `@macrocallback` definitions with a `when` clause
|
||||
* [Path] Fix `Path.absname/1` to correctly handle UNC paths on Windows
|
||||
* [Stream] Close with correct accumulator in `Stream.resource/3` when called for a single-element list
|
||||
* [Stream] Allow `Stream.cycle/1` to be double nested inside `Stream.cycle/1`
|
||||
* [URI] Preserve slashes in URIs without authority
|
||||
* [URI] Require a nil or an absolute path on URIs with host or authority
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Automatically shut down IEx if we receive EOF
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Don't discard Logger messages from other nodes as to leave a trail on both systems
|
||||
* [IEx] Exit IEx session if the group leader exits
|
||||
* [IEx] Allow `pry` to be used in non-tty terminals
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Ensure Erlang-based Mix compilers (erlang, leex, yecc) set valid position on diagnostics
|
||||
* [mix compile] Ensure compilation halts in an umbrella project if one of the siblings fail to compile
|
||||
* [mix deps] Raise an error if the umbrella app's dir name and `mix.exs` app name don't match
|
||||
* [mix test] Do not consider modules that are no longer cover compiled when computing coverage report, which could lead to flawed reports
|
||||
* [mix compile] Do not filter out warning for external files from diagnostics
|
||||
* [Mix.Project] Ensure user given `:manager` to dependencies has higher precedence than the SCM one
|
||||
* [Mix.Project] Recompile umbrella children when config files change and `mix compile` is called from the umbrella root
|
||||
* [Mix.Task] Always recompile before running tasks from dependencies
|
||||
* [Mix.Task] Ensure project's Logger config is used when running Mix tasks
|
||||
|
||||
### 3. Soft-deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] `compiler_options/0` is deprecated in favor of `compiler_option/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix.Config] `Mix.Config` has been deprecated in favor of the `Config` module that now ships as part of Elixir itself. Reading configuration files should now be done by the `Config.Reader` module
|
||||
* [Mix.Config] `Mix.Config.persist/1` has been deprecated. Instead of `Mix.Config.persist(config)` use `Application.put_all_env(config, persistent: true)` (`Application.put_all_env/2` was added in v1.9)
|
||||
* [mix xref] `calls/0` is deprecated in favor of compiler tracer
|
||||
* [mix xref] The `xref.exclude` option has been moved to `elixirc_options.no_warn_undefined` as the `xref` pass has been moved into the compiler
|
||||
|
||||
### 4. Hard-deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [CLI] Deprecate `--detached` option, use `--erl "-detached"` instead
|
||||
* [Map] Deprecate Enumerable keys in `Map.drop/2`, `Map.split/2`, and `Map.take/2`
|
||||
* [String] The `:insert_replaced` option in `String.replace/4` has been deprecated. Instead you may pass a function as a replacement or use `:binary.replace/4` if you need to support earlier Elixir versions
|
||||
* [Code] `Code.load_file/2` has been deprecated in favor of `Code.require_file/2` or `Code.compile_file/2`
|
||||
* [Code] `Code.loaded_files/0` and `Code.unload_file/1` have been deprecated in favor of `Code.required_files/0` and `Code.unrequire_file/1` respectively
|
||||
* [Code] `Code.ensure_compiled?/1` is deprecated in favor of `Code.ensure_compiled/1`
|
||||
* [String] `String.normalize/2` has been deprecated in favor of `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` which ship as part of Erlang/OTP 20+
|
||||
* [Supervisor] `Supervisor.Spec.supervise/2` has been deprecated in favor of the new Supervisor child specification
|
||||
* [Supervisor] The `:simple_one_for_one` strategy in `Supervisor` has been deprecated in favor of `DynamicSupervisor`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] `:compile_time_purge_level` application environment configuration has been deprecated in favor of the more general `:compile_time_purge_matching` config
|
||||
* [Logger] Deprecate logging non-chardata values
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix.Project] Deprecate `Mix.Project.load_paths/1` in favor of `Mix.Project.compile_path/1`
|
||||
* [mix compile.xref] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref deprecations] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref unreachable] This check has been moved into the compiler and has no effect now
|
||||
|
||||
## v1.8
|
||||
## v1.9
|
||||
|
||||
The CHANGELOG for v1.8 releases can be found [in the v1.8 branch](https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md).
|
||||
The CHANGELOG for v1.9 releases can be found [in the v1.9 branch](https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md).
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := master/ # master/ or vMAJOR.MINOR/
|
||||
CANONICAL := v1.10/ # master/ or vMAJOR.MINOR/
|
||||
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include $(ERLC_OPTS)
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
@@ -19,15 +20,15 @@ GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$hea
|
||||
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
|
||||
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
||||
|
||||
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files install_man clean_man docs Docs.zip Precompiled.zip zips
|
||||
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
|
||||
.NOTPARALLEL: compile
|
||||
|
||||
#==> 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 >= 20)])' -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 >= 21)])' -s erlang halt | grep -q '^true'; \
|
||||
if [ $$? != 0 ]; then \
|
||||
echo "At least Erlang/OTP 20.0 is required to build Elixir"; \
|
||||
echo "At least Erlang/OTP 21.0 is required to build Elixir"; \
|
||||
exit 1; \
|
||||
fi
|
||||
endef
|
||||
@@ -45,7 +46,7 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
|
||||
|
||||
test_$(1): compile $(1)
|
||||
@ echo "==> $(1) (ex_unit)"
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/*_test.exs";
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
|
||||
endef
|
||||
|
||||
define WRITE_SOURCE_DATE_EPOCH
|
||||
@@ -89,10 +90,9 @@ $(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
|
||||
echo "==> bootstrap (compile)"; \
|
||||
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
|
||||
fi
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/kernel.ex" -o ebin;
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
|
||||
$(Q) $(MAKE) unicode
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
|
||||
$(Q) $(MAKE) app
|
||||
|
||||
app: $(APP)
|
||||
@@ -133,11 +133,13 @@ check_reproducible: compile
|
||||
$(call WRITE_SOURCE_DATE_EPOCH)
|
||||
$(Q) mkdir -p lib/elixir/tmp/ebin_reproducible/ \
|
||||
lib/eex/tmp/ebin_reproducible/ \
|
||||
lib/ex_unit/tmp/ebin_reproducible/ \
|
||||
lib/iex/tmp/ebin_reproducible/ \
|
||||
lib/logger/tmp/ebin_reproducible/ \
|
||||
lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/elixir/ebin/* lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/eex/ebin/* lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/ex_unit/ebin/* lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
|
||||
@@ -145,6 +147,7 @@ check_reproducible: compile
|
||||
$(Q) echo "Diffing..."
|
||||
$(Q) diff -r lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||
@@ -173,16 +176,16 @@ clean_residual_files:
|
||||
#==> Documentation tasks
|
||||
|
||||
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}\c")
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
DOCS_FORMAT = html
|
||||
COMPILE_DOCS = bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" -m "$(3)" -u "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) -o doc/$(2) -n https://hexdocs.pm/$(2)/$(CANONICAL) -p https://elixir-lang.org/docs.html -f "$(DOCS_FORMAT)" $(4)
|
||||
COMPILE_DOCS = 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)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
docs_elixir: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (elixir)"
|
||||
$(Q) rm -rf doc/elixir
|
||||
$(call COMPILE_DOCS,Elixir,elixir,Kernel,-c lib/elixir/docs.exs)
|
||||
$(call COMPILE_DOCS,Elixir,elixir,Kernel,--config "lib/elixir/docs.exs")
|
||||
|
||||
docs_eex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (eex)"
|
||||
@@ -237,6 +240,7 @@ zips: Precompiled.zip Docs.zip
|
||||
|
||||
#==> Test tasks
|
||||
|
||||
# If you modify this task, please update .cirrus.yml accordingly
|
||||
test: test_formatted test_erlang test_elixir
|
||||
|
||||
test_windows: test test_taskkill
|
||||
@@ -249,8 +253,19 @@ TEST_ERL = lib/elixir/test/erlang
|
||||
TEST_EBIN = lib/elixir/test/ebin
|
||||
TEST_ERLS = $(addprefix $(TEST_EBIN)/, $(addsuffix .beam, $(basename $(notdir $(wildcard $(TEST_ERL)/*.erl)))))
|
||||
|
||||
define FORMAT
|
||||
$(Q) if [ "$(OS)" = "Windows_NT" ]; then \
|
||||
cmd //C call ./bin/mix.bat format $(1); \
|
||||
else \
|
||||
bin/elixir bin/mix format $(1); \
|
||||
fi
|
||||
endef
|
||||
|
||||
format: compile
|
||||
$(call FORMAT)
|
||||
|
||||
test_formatted: compile
|
||||
bin/elixir bin/mix format --check-formatted
|
||||
$(call FORMAT,--check-formatted)
|
||||
|
||||
test_erlang: compile $(TEST_ERLS)
|
||||
@ echo "==> elixir (eunit)"
|
||||
@@ -267,9 +282,9 @@ test_stdlib: compile
|
||||
@ echo "==> elixir (ex_unit)"
|
||||
$(Q) exec epmd & exit
|
||||
$(Q) if [ "$(OS)" = "Windows_NT" ]; then \
|
||||
cd lib/elixir && cmd //C call ../../bin/elixir.bat -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs"; \
|
||||
cd lib/elixir && cmd //C call ../../bin/elixir.bat -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
|
||||
else \
|
||||
cd lib/elixir && ../../bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/*_test.exs"; \
|
||||
cd lib/elixir && ../../bin/elixir -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
|
||||
fi
|
||||
|
||||
#==> Dialyzer tasks
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||

|
||||
=========
|
||||
[](https://travis-ci.org/elixir-lang/elixir)
|
||||
[](https://ci.appveyor.com/project/josevalim/elixir)
|
||||
[](https://cirrus-ci.com/github/elixir-lang/elixir)
|
||||
|
||||
Elixir is a dynamic, functional language designed for building scalable
|
||||
and maintainable applications.
|
||||
@@ -50,10 +48,10 @@ If Elixir fails to build (specifically when pulling in a new version via
|
||||
If tests pass, you can use Interactive Elixir by running `bin/iex` in your terminal.
|
||||
|
||||
However, if tests fail, it is likely that you have an outdated Erlang/OTP version
|
||||
(Elixir requires Erlang/OTP 20.0 or later). You can check your Erlang/OTP version
|
||||
by calling `erl` in the command line. You will see some information as follows:
|
||||
(Elixir requires Erlang/OTP 21.0 or later). You can check your Erlang/OTP version
|
||||
by calling `erl` in the command line. You will see some information similar to:
|
||||
|
||||
Erlang/OTP 20 [erts-9.0] [smp:2:2] [async-threads:10] [kernel-poll:false]
|
||||
Erlang/OTP 21 [erts-9.0] [smp:2:2] [async-threads:10] [kernel-poll:false]
|
||||
|
||||
If you have properly set up your dependencies and tests still fail,
|
||||
you may want to open up a bug report, as explained next.
|
||||
@@ -112,7 +110,7 @@ To recompile (including Erlang modules):
|
||||
make compile
|
||||
```
|
||||
|
||||
After your changes are done, please remember to run `mix format` to guarantee
|
||||
After your changes are done, please remember to run `make format` to guarantee
|
||||
all files are properly formatted and then run the full suite with
|
||||
`make test`.
|
||||
|
||||
@@ -125,7 +123,7 @@ make clean_elixir compile
|
||||
|
||||
Similarly, if you can't get Elixir to compile or the tests to pass after
|
||||
updating an existing checkout, run `make clean compile`. You can check
|
||||
[the official build status on Travis-CI](https://travis-ci.org/elixir-lang/elixir).
|
||||
[the official build status on Cirrus CI](https://cirrus-ci.com/github/elixir-lang/elixir).
|
||||
More tasks can be found by reading the [Makefile](Makefile).
|
||||
|
||||
With tests running and passing, you are ready to contribute to Elixir and
|
||||
@@ -179,8 +177,8 @@ make docs # to generate HTML pages
|
||||
make docs DOCS_FORMAT=epub # to generate EPUB documents
|
||||
```
|
||||
|
||||
This will produce documentation sets for `elixir`, `mix`, etc. under
|
||||
the `doc` directory. If you are planning to contribute documentation,
|
||||
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).
|
||||
|
||||
## Development links
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@
|
||||
|
||||
9. Publish new zips with `make zips`, upload `Precompiled.zip` and `Docs.zip` to GitHub Releases, and include SHAs+CHANGELOG
|
||||
|
||||
10. Add the release to `elixir.csv` (all releases) and `_data/elixir-versions.yml` (except for RCs) files in `elixir-lang/elixir-lang.github.com`
|
||||
10. Add the release to `elixir.csv` (all releases), update `erlang.csv` to the precompiled OTP version, and `_data/elixir-versions.yml` (except for RCs) files in `elixir-lang/elixir-lang.github.com`
|
||||
|
||||
11. Send an e-mail to elixir-lang-ann@googlegroups.com with title "Elixir vVERSION released". The body should be a link to the Release page on GitHub and the checksums. If it is a security release, prefix the title with the `[security]` tag
|
||||
|
||||
@@ -40,6 +40,6 @@
|
||||
|
||||
2. Start new /CHANGELOG.md
|
||||
|
||||
3. Update tables in "Compatibility and Deprecations"
|
||||
3. Update tables in /SECURITY.md in "Compatibility and Deprecations"
|
||||
|
||||
4. Commit "Start vMAJOR.MINOR+1"
|
||||
|
||||
+2
-2
@@ -6,11 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
| Elixir version | Support
|
||||
| -------------- | ------------------------------
|
||||
| 1.9 | Bug fixes and security patches
|
||||
| 1.10 | Bug fixes and security patches
|
||||
| 1.9 | Security patches only
|
||||
| 1.8 | Security patches only
|
||||
| 1.7 | Security patches only
|
||||
| 1.6 | Security patches only
|
||||
| 1.5 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+20
-18
@@ -2,22 +2,23 @@
|
||||
set -e
|
||||
|
||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "Usage: $(basename "$0") [options] [.exs file] [data]
|
||||
cat <<USAGE >&2
|
||||
Usage: $(basename "$0") [options] [.exs file] [data]
|
||||
|
||||
## General options
|
||||
|
||||
-e \"COMMAND\" Evaluates the given command (*)
|
||||
-e "COMMAND" Evaluates the given command (*)
|
||||
-h, --help Prints this message and exits
|
||||
-r \"FILE\" Requires the given files/patterns (*)
|
||||
-r "FILE" Requires the given files/patterns (*)
|
||||
-S SCRIPT Finds and executes the given script in \$PATH
|
||||
-pr \"FILE\" Requires the given files/patterns in parallel (*)
|
||||
-pa \"PATH\" Prepends the given path to Erlang code path (*)
|
||||
-pz \"PATH\" Appends the given path to Erlang code path (*)
|
||||
-pr "FILE" Requires the given files/patterns in parallel (*)
|
||||
-pa "PATH" Prepends the given path to Erlang code path (*)
|
||||
-pz "PATH" Appends the given path to Erlang code path (*)
|
||||
-v, --version Prints Elixir version and exits
|
||||
|
||||
--app APP Starts the given app and its dependencies (*)
|
||||
--erl \"SWITCHES\" Switches to be passed down to Erlang (*)
|
||||
--eval \"COMMAND\" Evaluates the given command, same as -e (*)
|
||||
--erl "SWITCHES" Switches to be passed down to Erlang (*)
|
||||
--eval "COMMAND" Evaluates the given command, same as -e (*)
|
||||
--logger-otp-reports BOOL Enables or disables OTP reporting
|
||||
--logger-sasl-reports BOOL Enables or disables SASL reporting
|
||||
--no-halt Does not halt the Erlang VM after execution
|
||||
@@ -33,24 +34,25 @@ The following options are related to node distribution.
|
||||
--cookie COOKIE Sets a cookie for this distributed node
|
||||
--hidden Makes a hidden node
|
||||
--name NAME Makes and assigns a name to the distributed node
|
||||
--rpc-eval NODE \"COMMAND\" Evaluates the given command on the given remote node (*)
|
||||
--rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*)
|
||||
--sname NAME Makes and assigns a short name to the distributed node
|
||||
|
||||
## Release options
|
||||
|
||||
The following options are generally used under releases.
|
||||
|
||||
--boot \"FILE\" Uses the given FILE.boot to start the system
|
||||
--boot-var VAR \"VALUE\" Makes \$VAR available as VALUE to FILE.boot (*)
|
||||
--erl-config \"FILE\" Loads configuration in FILE.config written in Erlang (*)
|
||||
--pipe-to \"PIPEDIR\" \"LOGDIR\" Starts the Erlang VM as a named PIPEDIR and LOGDIR
|
||||
--vm-args \"FILE\" Passes the contents in file as arguments to the VM
|
||||
--boot "FILE" Uses the given FILE.boot to start the system
|
||||
--boot-var VAR "VALUE" Makes \$VAR available as VALUE to FILE.boot (*)
|
||||
--erl-config "FILE" Loads configuration in FILE.config written in Erlang (*)
|
||||
--pipe-to "PIPEDIR" "LOGDIR" Starts the Erlang VM as a named PIPEDIR and LOGDIR
|
||||
--vm-args "FILE" Passes the contents in file as arguments to the VM
|
||||
|
||||
--pipe-to starts Elixir detached from console (Unix-like only).
|
||||
It will attempt to create PIPEDIR and LOGDIR if they don't exist.
|
||||
See run_erl to learn more. To reattach, run: to_erl PIPEDIR.
|
||||
|
||||
** Options marked with (*) can be given more than once." >&2
|
||||
** Options marked with (*) can be given more than once.
|
||||
USAGE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -64,7 +66,7 @@ readlink_f () {
|
||||
fi
|
||||
}
|
||||
|
||||
# Stores static erlang arguments and --erl (which is passed as is)
|
||||
# Stores static Erlang arguments and --erl (which is passed as is)
|
||||
ERL=""
|
||||
|
||||
# Stores erl arguments preserving spaces/quotes (mimics an array)
|
||||
@@ -204,7 +206,7 @@ SCRIPT_PATH=$(dirname "$SELF")
|
||||
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
|
||||
if [ "$MODE" != "iex" ]; then ERL="-noshell -s elixir start_cli $ERL"; fi
|
||||
|
||||
if [ "$OS" != "Windows_NT" ]; then
|
||||
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
|
||||
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
|
||||
fi
|
||||
|
||||
@@ -226,4 +228,4 @@ if [ -n "$ELIXIR_CLI_DRY_RUN" ]; then
|
||||
echo "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
+2
-2
@@ -156,9 +156,9 @@ if not !runMode! == "iex" (
|
||||
set beforeExtra=-noshell -s elixir start_cli !beforeExtra!
|
||||
)
|
||||
if defined useWerl (
|
||||
start !ERTS_BIN!werl.exe !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
) else (
|
||||
!ERTS_BIN!erl.exe !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
|
||||
)
|
||||
:end
|
||||
endlocal
|
||||
+4
-2
@@ -2,7 +2,8 @@
|
||||
set -e
|
||||
|
||||
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "Usage: $(basename "$0") [elixir switches] [compiler switches] [.ex files]
|
||||
cat <<USAGE >&2
|
||||
Usage: $(basename "$0") [elixir switches] [compiler switches] [.ex files]
|
||||
|
||||
-h, --help Prints this message and exits
|
||||
-o The directory to output compiled files
|
||||
@@ -16,7 +17,8 @@ if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
|
||||
Options given after -- are passed down to the executed code.
|
||||
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS.
|
||||
Options can be passed to the Erlang compiler using \$ERL_COMPILER_OPTIONS." >&2
|
||||
Options can be passed to the Erlang compiler using \$ERL_COMPILER_OPTIONS.
|
||||
USAGE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
set -e
|
||||
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "Usage: $(basename "$0") [options] [.exs file] [data]
|
||||
cat <<USAGE >&2
|
||||
Usage: $(basename "$0") [options] [.exs file] [data]
|
||||
|
||||
The following options are exclusive to IEx:
|
||||
|
||||
--dot-iex \"PATH\" Overrides default .iex.exs file and uses path instead;
|
||||
--dot-iex "PATH" Overrides default .iex.exs file and uses path instead;
|
||||
path can be empty, then no file will be loaded
|
||||
--remsh NAME Connects to a node using a remote shell
|
||||
|
||||
It accepts all other options listed by \"elixir --help\"." >&2
|
||||
It accepts all other options listed by "elixir --help".
|
||||
USAGE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/usr/bin/env elixir
|
||||
Mix.start
|
||||
Mix.CLI.main
|
||||
Mix.start()
|
||||
Mix.CLI.main()
|
||||
|
||||
@@ -76,6 +76,12 @@ defmodule EEx do
|
||||
This will never appear
|
||||
<% end %>
|
||||
|
||||
To escape an EEx expression in EEx use `<%% content %>`. For example:
|
||||
|
||||
<%%= x + 3 %>
|
||||
|
||||
will be rendered as `<%= x + 3 %>`.
|
||||
|
||||
Notice that different engines may have different rules
|
||||
for each tag. Other tags may be added in future versions.
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ defmodule EEx.Compiler do
|
||||
{count, new_state}
|
||||
end
|
||||
|
||||
# Look middle expressions that immediatelly follow a start_expr
|
||||
# Look middle expressions that immediately follow a start_expr
|
||||
|
||||
defp look_ahead_middle(
|
||||
[{:text, text}, {:middle_expr, line, _, chars, _} | rest] = tokens,
|
||||
|
||||
@@ -63,6 +63,7 @@ defmodule EEx.Tokenizer do
|
||||
{:ok, expr, new_line, rest} ->
|
||||
token = token_name(expr)
|
||||
{trimmed?, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
|
||||
expr = pad_if_needed(token, expr, trimmed?)
|
||||
acc = tokenize_text(buffer, acc)
|
||||
final = {token, line, marker, Enum.reverse(expr), trimmed?}
|
||||
tokenize(rest, new_line, opts, [], [final | acc])
|
||||
@@ -239,4 +240,7 @@ defmodule EEx.Tokenizer do
|
||||
defp trim_whitespace(list) do
|
||||
list
|
||||
end
|
||||
|
||||
defp pad_if_needed(:start_expr, [h | _] = expr, true) when h not in @spaces, do: [?\s | expr]
|
||||
defp pad_if_needed(_, expr, _), do: expr
|
||||
end
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
Code.require_file("../test_helper.exs", __DIR__)
|
||||
|
||||
defmodule EEx.SmartEngineTest do
|
||||
# TODO: Make this async: true once capture_io is removed
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "evaluates simple string" do
|
||||
assert_eval("foo bar", "foo bar")
|
||||
|
||||
@@ -64,6 +64,20 @@ defmodule EExTest do
|
||||
assert_eval(" • • •\n Jößé Vâlìm Jößé Vâlìm\n", template)
|
||||
end
|
||||
|
||||
test "no spaces" do
|
||||
string = """
|
||||
<%=cond do%>
|
||||
<%false ->%>
|
||||
this
|
||||
<%true ->%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n that\n\n"
|
||||
assert_eval(expected, string, [])
|
||||
end
|
||||
|
||||
test "trim mode" do
|
||||
string = "<%= 123 %> \n456\n <%= 789 %>"
|
||||
expected = "123456\n789"
|
||||
@@ -96,6 +110,20 @@ defmodule EExTest do
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "trim mode with no spaces" do
|
||||
string = """
|
||||
<%=cond do%>
|
||||
<%false ->%>
|
||||
this
|
||||
<%true ->%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = " that\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "embedded code" do
|
||||
assert_eval("foo bar", "foo <%= :bar %>")
|
||||
end
|
||||
@@ -112,7 +140,7 @@ defmodule EExTest do
|
||||
assert_eval("foo ", "foo <%= if false do %>bar<% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with do preceeded by bracket" do
|
||||
test "embedded code with do preceded by bracket" do
|
||||
assert_eval("foo bar", "foo <%= if {true}do %>bar<% end %>")
|
||||
assert_eval("foo bar", "foo <%= if (true)do %>bar<% end %>")
|
||||
assert_eval("foo bar", "foo <%= if [true]do %>bar<% end %>")
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
Time,
|
||||
Tuple,
|
||||
URI,
|
||||
Version
|
||||
Version,
|
||||
Version.Requirement
|
||||
],
|
||||
"Collections & Enumerables": [
|
||||
Access,
|
||||
|
||||
@@ -10,4 +10,4 @@ main([Source, Target, Version]) ->
|
||||
Props = lists:keyreplace(vsn, 1, Props1, {vsn, Version}),
|
||||
AppDef = io_lib:format("~tp.~n", [{application, Name, Props}]),
|
||||
ok = file:write_file(Target, AppDef),
|
||||
io:format("Generated ~ts.app~n", [Name]).
|
||||
io:format("Generated ~ts app~n", [Name]).
|
||||
|
||||
@@ -236,6 +236,25 @@ defmodule Access do
|
||||
:error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `fetch/2` but returns the value directly,
|
||||
or raises a `KeyError` exception if `key` is not found.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Access.fetch!(%{name: "meg", age: 26}, :name)
|
||||
"meg"
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec fetch!(container, term) :: term
|
||||
def fetch!(container, key) do
|
||||
case fetch(container, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise(KeyError, key: key, term: container)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value for the given key in a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
@@ -310,6 +329,14 @@ defmodule Access do
|
||||
|
||||
The returned value is a two-element tuple with the "get" value returned by
|
||||
`fun` and a new container with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Access.get_and_update([a: 1], :a, fn current_value ->
|
||||
...> {current_value, current_value + 1}
|
||||
...> end)
|
||||
{1, [a: 2]}
|
||||
|
||||
"""
|
||||
@spec get_and_update(data, key, (value -> {get_value, value} | :pop)) :: {get_value, data}
|
||||
when get_value: var, data: container
|
||||
|
||||
+28
-2
@@ -123,7 +123,6 @@ defmodule Agent do
|
||||
The generated `child_spec/1` can be customized with the following options:
|
||||
|
||||
* `:id` - the child specification identifier, defaults to the current module
|
||||
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
|
||||
* `:restart` - when the child should be restarted, defaults to `:permanent`
|
||||
* `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down
|
||||
|
||||
@@ -202,7 +201,7 @@ defmodule Agent do
|
||||
@doc false
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
if Module.get_attribute(__MODULE__, :doc) == nil do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -424,6 +423,15 @@ defmodule Agent do
|
||||
Same as `update/3` but a module, function, and arguments are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.update(pid, Kernel, :+, [12])
|
||||
:ok
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
54
|
||||
|
||||
"""
|
||||
@spec update(agent, module, atom, [term], timeout) :: :ok
|
||||
def update(agent, module, fun, args, timeout \\ 5000) do
|
||||
@@ -439,6 +447,15 @@ defmodule Agent do
|
||||
|
||||
Note that `cast` returns `:ok` immediately, regardless of whether `agent` (or
|
||||
the node it should live on) exists.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.cast(pid, fn state -> state + 1 end)
|
||||
:ok
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
43
|
||||
|
||||
"""
|
||||
@spec cast(agent, (state -> state)) :: :ok
|
||||
def cast(agent, fun) when is_function(fun, 1) do
|
||||
@@ -451,6 +468,15 @@ defmodule Agent do
|
||||
Same as `cast/2` but a module, function, and arguments are expected
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
|
||||
iex> Agent.cast(pid, Kernel, :+, [12])
|
||||
:ok
|
||||
iex> Agent.get(pid, fn state -> state end)
|
||||
54
|
||||
|
||||
"""
|
||||
@spec cast(agent, module, atom, [term]) :: :ok
|
||||
def cast(agent, module, fun, args) do
|
||||
|
||||
+280
-94
@@ -8,78 +8,120 @@ defmodule Application do
|
||||
|
||||
An application is a component implementing some specific functionality, with a
|
||||
standardized directory structure, configuration, and lifecycle. Applications
|
||||
are *loaded*, *started*, and *stopped*.
|
||||
are *loaded*, *started*, and *stopped*. Each application also has its own
|
||||
environment, which provides a unified API for configuring each application.
|
||||
|
||||
## The application resource file
|
||||
|
||||
Applications are specified in their [*resource
|
||||
file*](http://erlang.org/doc/man/app.html), which is a file called `APP.app`,
|
||||
where `APP` is the application name. For example, the application resource
|
||||
file of the OTP application `ex_unit` is called `ex_unit.app`.
|
||||
|
||||
You'll find the resource file of an application in its `ebin` directory, it is
|
||||
generated automatically by Mix. Some of its keys are taken from the keyword
|
||||
lists returned by the `project/0` and `application/0` functions defined in
|
||||
`mix.exs`, and others are generated by Mix itself.
|
||||
|
||||
You can learn more about the generation of application resource files in the
|
||||
documentation of `Mix.Tasks.Compile.App`, available as well by running
|
||||
`mix help compile.app`.
|
||||
Developers typically interact with the application environment and its
|
||||
callback module. Therefore those will be the topics we will cover first
|
||||
before jumping into details about the application resource file and life-cycle.
|
||||
|
||||
## The application environment
|
||||
|
||||
The key `env` of an application resource file has a list of tuples that map
|
||||
atoms to terms, and its contents are known as the application *environment*.
|
||||
Note that this environment is unrelated to the operating system environment.
|
||||
Each application has its own environment. The environment is a keyword list
|
||||
that maps atoms to terms. Note that this environment is unrelated to the
|
||||
operating system environment.
|
||||
|
||||
By default, the environment of an application is an empty list. In a Mix
|
||||
project you can set that key in `application/0`:
|
||||
project's `mix.exs` file, you can set the `:env` key in `application/0`:
|
||||
|
||||
def application do
|
||||
[env: [redis_host: "localhost"]]
|
||||
[env: [db_host: "localhost"]]
|
||||
end
|
||||
|
||||
and the generated application resource file is going to have it included.
|
||||
Now, in your application, you can read this environment by using functions
|
||||
such as `fetch_env!/2` and friends:
|
||||
|
||||
The environment is available after loading the application, which is a process
|
||||
explained later:
|
||||
defmodule MyApp.DBClient do
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: db_host())
|
||||
end
|
||||
|
||||
Application.load(:APP_NAME)
|
||||
#=> :ok
|
||||
|
||||
Application.get_env(:APP_NAME, :redis_host)
|
||||
#=> "localhost"
|
||||
defp db_host do
|
||||
Application.fetch_env!(:my_app, :db_host)
|
||||
end
|
||||
end
|
||||
|
||||
In Mix projects, the environment of the application and its dependencies can
|
||||
be overridden via the `config/config.exs` file. If you start the application
|
||||
with Mix, that configuration is available at compile time, and at runtime too,
|
||||
but take into account it is not included in the generated application resource
|
||||
file, and it is not available if you start the application without Mix.
|
||||
be overridden via the `config/config.exs` file. For example, someone using
|
||||
your application can override its `:db_host` environment variable as follows:
|
||||
|
||||
For example, someone using your application can override its `:redis_host`
|
||||
environment variable as follows:
|
||||
import Config
|
||||
config :my_app, :db_host, "db.local"
|
||||
|
||||
config :APP_NAME, redis_host: "redis.local"
|
||||
You can also change the application environment dynamically by using functions
|
||||
such as `put_env/3` and `delete_env/2`. However, as a rule of thumb, each application
|
||||
is responsible for its own environment. Please do not use the functions in this
|
||||
module for directly accessing or modifying the environment of other applications.
|
||||
|
||||
The function `put_env/3` allows dynamic configuration of the application
|
||||
environment, but as a rule of thumb each application is responsible for its
|
||||
own environment. Please do not use the functions in this module for directly
|
||||
accessing or modifying the environment of other applications.
|
||||
### Compile-time environment
|
||||
|
||||
The application environment can be overridden via the `-config` option of
|
||||
`erl`, as well as command-line options, as we are going to see below.
|
||||
In the previous example, we read the application environment at runtime:
|
||||
|
||||
defmodule MyApp.DBClient do
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: db_host())
|
||||
end
|
||||
|
||||
defp db_host do
|
||||
Application.fetch_env!(:my_app, :db_host)
|
||||
end
|
||||
end
|
||||
|
||||
In other words, the environment key `:db_host` for application `:my_app`
|
||||
will only be read when `MyApp.DBClient` effectively starts. While reading
|
||||
the application environment at runtime is the preferred approach, in some
|
||||
rare occasions you may want to use the application environment to configure
|
||||
the compilation of a certain project. This is often done by calling `get_env/3`
|
||||
outside of a function:
|
||||
|
||||
defmodule MyApp.DBClient do
|
||||
@db_host Application.get_env(:my_app, :db_host, "db.local")
|
||||
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: @db_host)
|
||||
end
|
||||
end
|
||||
|
||||
This approach has one big limitation: if you change the value of the
|
||||
application environment after the code is compiled, the value used at
|
||||
runtime is not going to change! For example, if you are using `mix release`
|
||||
and your `config/releases.exs` has:
|
||||
|
||||
config :my_app, :db_host, "db.production"
|
||||
|
||||
This value will have no effect as the code was compiled to connect to "db.local",
|
||||
which is mostly likely unavailable in the production environment.
|
||||
|
||||
For those reasons, reading the application environment at runtime should be the
|
||||
first choice. However, if you really have to read the application environment
|
||||
during compilation, we recommend you to use `compile_env/3` instead:
|
||||
|
||||
@db_host Application.compile_env(:my_app, :db_host, "db.local")
|
||||
|
||||
By using `compile_env/3`, tools like Mix will store the values used during
|
||||
compilation and compare the compilation values with the runtime values whenever
|
||||
your system starts, raising an error in case they differ.
|
||||
|
||||
## The application callback module
|
||||
|
||||
The `mod` key of an application resource file configures an application
|
||||
callback module and start argument:
|
||||
Applications can be loaded, started, and stopped. Generally, build tools
|
||||
like Mix take care of starting an application and all of its dependencies
|
||||
for you, but you can also do it manually by calling:
|
||||
|
||||
{:ok, _} = Application.ensure_all_started(:some_app)
|
||||
|
||||
When an application starts, developers may configure a callback module
|
||||
that executes custom code. Developers use this callback to start the
|
||||
application supervision tree.
|
||||
|
||||
The first step to do so is to add a `:mod` key to the `application/0`
|
||||
definition in your `mix.exs` file. It expects a tuple, with the application
|
||||
callback module and start argument (commonly an empty list):
|
||||
|
||||
def application do
|
||||
[mod: {MyApp, []}]
|
||||
end
|
||||
|
||||
This key is optional, only needed for applications that start a supervision tree.
|
||||
|
||||
The `MyApp` module given to `:mod` needs to implement the `Application` behaviour.
|
||||
This can be done by putting `use Application` in that module and implementing the
|
||||
`c:start/2` callback, for example:
|
||||
@@ -116,6 +158,19 @@ defmodule Application do
|
||||
tree is terminated. Its argument is the state returned by `c:start/2`, if it did,
|
||||
or `[]` otherwise, and its return value is passed to `c:stop/1`.
|
||||
|
||||
## The application resource file
|
||||
|
||||
In the sections above, we have configured an application in the
|
||||
`application/0` section of the `mix.exs` file. Ultimately, Mix will use
|
||||
this configuration to create an [*application resource
|
||||
file*](http://erlang.org/doc/man/app.html), which is a file called
|
||||
`APP_NAME.app`. For example, the application resource file of the OTP
|
||||
application `ex_unit` is called `ex_unit.app`.
|
||||
|
||||
You can learn more about the generation of application resource files in
|
||||
the documentation of `Mix.Tasks.Compile.App`, available as well by running
|
||||
`mix help compile.app`.
|
||||
|
||||
## The application lifecycle
|
||||
|
||||
### Loading applications
|
||||
@@ -126,16 +181,8 @@ defmodule Application do
|
||||
Application.load(:ex_unit)
|
||||
#=> :ok
|
||||
|
||||
If an application has included applications, they are also loaded. And the
|
||||
procedure recurses if they in turn have included applications. Included
|
||||
applications are unrelated to applications in Mix umbrella projects, they are
|
||||
an Erlang/OTP concept that has to do with coordinated starts.
|
||||
|
||||
When an application is loaded, the environment specified in its resource file
|
||||
is merged with any overrides from config files passed to `erl` via the
|
||||
`-config` option. It is worth highlighting that releases pass `sys.config`
|
||||
this way. The resulting environment can still be overridden again via specific
|
||||
`-Application` options passed to `erl`.
|
||||
is merged with any overrides from config files.
|
||||
|
||||
Loading an application *does not* load its modules.
|
||||
|
||||
@@ -155,14 +202,12 @@ defmodule Application do
|
||||
system. Instead, you start one or more applications, each with their own
|
||||
initialization and termination logic.
|
||||
|
||||
When an application is started, the runtime loads it if it hasn't been loaded
|
||||
yet (in the technical sense described above). Then, it checks if the
|
||||
dependencies listed in the `applications` key of the resource file are already
|
||||
started. Having at least one dependency not started is an error condition, but
|
||||
when you start an application with `mix run`, Mix takes care of starting all
|
||||
the dependencies for you, so in practice you don't need to worry about it
|
||||
unless you are starting applications manually with the API provided by this
|
||||
module.
|
||||
When an application is started, the `Application.load/1` is automatically
|
||||
invoked if it hasn't been done yet. Then, it checks if the dependencies listed
|
||||
in the `applications` key of the resource file are already started. Having at
|
||||
least one dependency not started is an error condition. Functions like
|
||||
`ensure_all_started/1` takes care of starting an application and all of its
|
||||
dependencies for you.
|
||||
|
||||
If the application does not have a callback module configured, starting is
|
||||
done at this point. Otherwise, its `c:start/2` callback if invoked. The PID of
|
||||
@@ -181,9 +226,9 @@ defmodule Application do
|
||||
|
||||
Stopping an application with a callback module has three steps:
|
||||
|
||||
1. If present, invoke the optional callback `c:prep_stop/1`.
|
||||
2. Terminate the top-level supervisor.
|
||||
3. Invoke the required callback `c:stop/1`.
|
||||
1. If present, invoke the optional callback `c:prep_stop/1`.
|
||||
2. Terminate the top-level supervisor.
|
||||
3. Invoke the required callback `c:stop/1`.
|
||||
|
||||
The arguments passed to the callbacks are related to the state optionally
|
||||
returned by `c:start/2`, and are documented in the section about the callback
|
||||
@@ -203,17 +248,16 @@ defmodule Application do
|
||||
|
||||
## Tooling
|
||||
|
||||
The Mix build tool can also be used to start your applications. For example,
|
||||
The Mix build tool automates most of the application management tasks. For example,
|
||||
`mix test` automatically starts your application dependencies and your application
|
||||
itself before your test runs. `mix run --no-halt` boots your current project and
|
||||
can be used to start a long running system. See `mix help run`.
|
||||
|
||||
Developers can also use tools like [Distillery](https://github.com/bitwalker/distillery)
|
||||
that build **releases**. Releases are able to package all of your source code
|
||||
as well as the Erlang VM into a single directory. Releases also give you explicit
|
||||
control over how each application is started and in which order. They also provide
|
||||
a more streamlined mechanism for starting and stopping systems, debugging, logging,
|
||||
as well as system monitoring.
|
||||
Developers can also use `mix release` to build **releases**. Releases are able to
|
||||
package all of your source code as well as the Erlang VM into a single directory.
|
||||
Releases also give you explicit control over how each application is started and in
|
||||
which order. They also provide a more streamlined mechanism for starting and
|
||||
stopping systems, debugging, logging, as well as system monitoring.
|
||||
|
||||
Finally, Elixir provides tools such as escripts and archives, which are
|
||||
different mechanisms for packaging your application. Those are typically used
|
||||
@@ -253,7 +297,7 @@ defmodule Application do
|
||||
application specification key `:start_phases` is not `:undefined`.
|
||||
|
||||
`start_args` are the arguments passed to the application in the `:mod`
|
||||
specification key (e.g., `mod: {MyApp, [:my_args]}`).
|
||||
specification key (for example, `mod: {MyApp, [:my_args]}`).
|
||||
|
||||
This function should either return `{:ok, pid}` or `{:ok, pid, state}` if
|
||||
startup is successful. `pid` should be the PID of the top supervisor. `state`
|
||||
@@ -413,12 +457,135 @@ defmodule Application do
|
||||
:application.get_all_env(app)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the application environment at compilation time.
|
||||
|
||||
Similar to `get_env/3`, except it must be used to read values
|
||||
at compile time. This allows Elixir to track when configuration
|
||||
values change between compile time and runtime.
|
||||
|
||||
The first argument is the application name. The second argument
|
||||
`key_or_path` is either an atom key or a path to traverse in
|
||||
search of the configuration, starting with an atom key.
|
||||
|
||||
For example, imagine the following configuration:
|
||||
|
||||
config :my_app, :key, [foo: [bar: :baz]]
|
||||
|
||||
We can access it during compile time as:
|
||||
|
||||
Application.compile_env(:my_app, :key)
|
||||
#=> [foo: [bar: :baz]]
|
||||
|
||||
Application.compile_env(:my_app, [:key, :foo])
|
||||
#=> [bar: :baz]
|
||||
|
||||
Application.compile_env(:my_app, [:key, :foo, :bar])
|
||||
#=> :baz
|
||||
|
||||
A default value can also be given as third argument. If
|
||||
any of the keys in the path along the way is missing, the
|
||||
default value is used:
|
||||
|
||||
Application.compile_env(:my_app, [:unknown, :foo, :bar], :default)
|
||||
#=> :default
|
||||
|
||||
Application.compile_env(:my_app, [:key, :unknown, :bar], :default)
|
||||
#=> :default
|
||||
|
||||
Application.compile_env(:my_app, [:key, :foo, :unknown], :default)
|
||||
#=> :default
|
||||
|
||||
Giving a path is useful to let Elixir know that only certain paths
|
||||
in a large configuration are compile time dependent.
|
||||
"""
|
||||
# TODO: Warn if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
|
||||
@doc since: "1.10.0"
|
||||
@spec compile_env(app, key | list, value) :: value
|
||||
defmacro compile_env(app, key_or_path, default \\ nil) when is_atom(app) do
|
||||
if __CALLER__.function do
|
||||
raise "Application.compile_env/3 cannot be called inside functions, only in the module body"
|
||||
end
|
||||
|
||||
quote do
|
||||
Application.__compile_env__(unquote(app), unquote(key_or_path), unquote(default), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __compile_env__(app, key_or_path, default, env) do
|
||||
case fetch_compile_env(app, key_or_path, env) do
|
||||
{:ok, value} -> value
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the application environment at compilation time or raises.
|
||||
|
||||
This is the same as `compile_env/3` but it raises an
|
||||
ArgumentError if the configuration is not available.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec compile_env!(app, key | list) :: value
|
||||
defmacro compile_env!(app, key_or_path) when is_atom(app) do
|
||||
if __CALLER__.function do
|
||||
raise "Application.compile_env!/2 cannot be called inside functions, only in the module body"
|
||||
end
|
||||
|
||||
quote do
|
||||
Application.__compile_env__!(unquote(app), unquote(key_or_path), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __compile_env__!(app, key_or_path, env) do
|
||||
case fetch_compile_env(app, key_or_path, env) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
:error ->
|
||||
raise ArgumentError,
|
||||
"could not fetch application environment #{inspect(key_or_path)} for application " <>
|
||||
"#{inspect(app)} #{fetch_env_failed_reason(app, key_or_path)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_compile_env(app, key, env) when is_atom(key),
|
||||
do: fetch_compile_env(app, key, [], env)
|
||||
|
||||
defp fetch_compile_env(app, [key | paths], env) when is_atom(key),
|
||||
do: fetch_compile_env(app, key, paths, env)
|
||||
|
||||
defp fetch_compile_env(app, key, path, env) do
|
||||
return = traverse_env(fetch_env(app, key), path)
|
||||
|
||||
for tracer <- env.tracers do
|
||||
tracer.trace({:compile_env, app, [key | path], return}, env)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
defp traverse_env(return, []), do: return
|
||||
defp traverse_env(:error, _paths), do: :error
|
||||
defp traverse_env({:ok, value}, [key | keys]), do: traverse_env(Access.fetch(value, key), keys)
|
||||
|
||||
@doc """
|
||||
Returns the value for `key` in `app`'s environment.
|
||||
|
||||
If the configuration parameter does not exist, the function returns the
|
||||
`default` value.
|
||||
|
||||
**Important:** if you are reading the application environment at compilation
|
||||
time, for example, inside the module definition instead of inside of a
|
||||
function, see `compile_env/3` instead.
|
||||
|
||||
**Important:** if you are writing a library to be used by other developers,
|
||||
it is generally recommended to avoid the application environment, as the
|
||||
application environment is effectively a global storage. For more information,
|
||||
read our [library guidelines](library-guidelines.html).
|
||||
|
||||
## Examples
|
||||
|
||||
`get_env/3` is commonly used to read the configuration of your OTP applications.
|
||||
@@ -448,11 +615,6 @@ defmodule Application do
|
||||
by module names). Our database engine can then traverse each repository in the
|
||||
list and then call `get_env(:my_app, Databases.RepoOne)` and so forth to retrieve
|
||||
the configuration of each one.
|
||||
|
||||
**Important:** if you are writing a library to be used by other developers,
|
||||
it is generally recommended to avoid the application environment, as the
|
||||
application environment is effectively a global storage. For more information,
|
||||
read our [library guidelines](library-guidelines.html).
|
||||
"""
|
||||
@spec get_env(app, key, value) :: value
|
||||
def get_env(app, key, default \\ nil) when is_atom(app) do
|
||||
@@ -476,6 +638,10 @@ defmodule Application do
|
||||
Returns the value for `key` in `app`'s environment.
|
||||
|
||||
If the configuration parameter does not exist, raises `ArgumentError`.
|
||||
|
||||
**Important:** if you are reading the application environment at compilation
|
||||
time, for example, inside the module definition instead of inside of a
|
||||
function, see `compile_env!/2` instead.
|
||||
"""
|
||||
@spec fetch_env!(app, key) :: value
|
||||
def fetch_env!(app, key) when is_atom(app) do
|
||||
@@ -484,23 +650,23 @@ defmodule Application do
|
||||
value
|
||||
|
||||
:error ->
|
||||
vsn = :application.get_key(app, :vsn)
|
||||
app = inspect(app)
|
||||
key = inspect(key)
|
||||
raise ArgumentError,
|
||||
"could not fetch application environment #{inspect(key)} for application " <>
|
||||
"#{inspect(app)} #{fetch_env_failed_reason(app, key)}"
|
||||
end
|
||||
end
|
||||
|
||||
case vsn do
|
||||
{:ok, _} ->
|
||||
raise ArgumentError,
|
||||
"could not fetch application environment #{key} for application #{app} " <>
|
||||
"because configuration #{key} was not set"
|
||||
defp fetch_env_failed_reason(app, key) do
|
||||
vsn = :application.get_key(app, :vsn)
|
||||
|
||||
:undefined ->
|
||||
raise ArgumentError,
|
||||
"could not fetch application environment #{key} for application #{app} " <>
|
||||
"because the application was not loaded/started. If your application " <>
|
||||
"depends on #{app} at runtime, make sure to load/start it or list it " <>
|
||||
"under :extra_applications in your mix.exs file"
|
||||
end
|
||||
case vsn do
|
||||
{:ok, _} ->
|
||||
"because configuration at #{inspect(key)} was not set"
|
||||
|
||||
:undefined ->
|
||||
"because the application was not loaded/started. If your application " <>
|
||||
"depends on #{inspect(app)} at runtime, make sure to load/start it or " <>
|
||||
"list it under :extra_applications in your mix.exs file"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -526,6 +692,9 @@ defmodule Application do
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
|
||||
# TODO: Remove this once we support Erlang/OTP 22+ exclusively.
|
||||
@compile {:no_warn_undefined, {:application, :set_env, 2}}
|
||||
|
||||
@doc """
|
||||
Puts the environment for multiple apps at the same time.
|
||||
|
||||
@@ -540,6 +709,7 @@ defmodule Application do
|
||||
|
||||
It receives the same options as `put_env/4`. Returns `:ok`.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok
|
||||
def put_all_env(config, opts \\ []) when is_list(config) and is_list(opts) do
|
||||
# TODO: Remove function exported? check when we require Erlang/OTP 22+
|
||||
@@ -582,6 +752,22 @@ defmodule Application do
|
||||
:application.ensure_started(app, type)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` is loaded.
|
||||
|
||||
Same as `load/2` but returns `:ok` if the application was already
|
||||
loaded.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec ensure_loaded(app) :: :ok | {:error, term}
|
||||
def ensure_loaded(app) when is_atom(app) do
|
||||
case :application.load(app) do
|
||||
:ok -> :ok
|
||||
{:error, {:already_loaded, ^app}} -> :ok
|
||||
{:error, _} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` and its applications are started.
|
||||
|
||||
|
||||
+40
-2
@@ -1,8 +1,46 @@
|
||||
defmodule Atom do
|
||||
@moduledoc """
|
||||
Convenience functions for working with atoms.
|
||||
Atoms are constants whose values are their own name.
|
||||
|
||||
They are often useful to enumerate over distinct values, such as:
|
||||
|
||||
iex> :apple
|
||||
:apple
|
||||
iex> :orange
|
||||
:orange
|
||||
iex> :watermelon
|
||||
:watermelon
|
||||
|
||||
Atoms are equal if their names are equal.
|
||||
|
||||
iex> :apple == :apple
|
||||
true
|
||||
iex> :apple == :orange
|
||||
false
|
||||
|
||||
Often they are used to express the state of an operation, by using
|
||||
values such as `:ok` and `:error`.
|
||||
|
||||
The booleans `true` and `false` are also atoms:
|
||||
|
||||
iex> true == :true
|
||||
true
|
||||
iex> is_atom(false)
|
||||
true
|
||||
iex> is_boolean(:false)
|
||||
true
|
||||
|
||||
Elixir allows you to skip the leading `:` for the atoms `false`, `true`,
|
||||
and `nil`.
|
||||
|
||||
Atoms must be composed of Unicode characters such as letters, numbers,
|
||||
underscore, and `@`. If the keyword has a character that does not
|
||||
belong to the category above, such as spaces, you can wrap it in
|
||||
quotes:
|
||||
|
||||
iex> :"this is an atom with spaces"
|
||||
:"this is an atom with spaces"
|
||||
|
||||
See also `Kernel.is_atom/1`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
|
||||
+66
-66
@@ -10,85 +10,85 @@ defmodule Base do
|
||||
|
||||
## Base 16 alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| 0| 4| 4| 8| 8| 12| C|
|
||||
| 1| 1| 5| 5| 9| 9| 13| D|
|
||||
| 2| 2| 6| 6| 10| A| 14| E|
|
||||
| 3| 3| 7| 7| 11| B| 15| F|
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|:---------|------:|:---------|------:|:---------|------:|:---------|
|
||||
| 0 | 0 | 4 | 4 | 8 | 8 | 12 | C |
|
||||
| 1 | 1 | 5 | 5 | 9 | 9 | 13 | D |
|
||||
| 2 | 2 | 6 | 6 | 10 | A | 14 | E |
|
||||
| 3 | 3 | 7 | 7 | 11 | B | 15 | F |
|
||||
|
||||
## Base 32 alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 9| J| 18| S| 27| 3|
|
||||
| 1| B| 10| K| 19| T| 28| 4|
|
||||
| 2| C| 11| L| 20| U| 29| 5|
|
||||
| 3| D| 12| M| 21| V| 30| 6|
|
||||
| 4| E| 13| N| 22| W| 31| 7|
|
||||
| 5| F| 14| O| 23| X| | |
|
||||
| 6| G| 15| P| 24| Y| (pad)| =|
|
||||
| 7| H| 16| Q| 25| Z| | |
|
||||
| 8| I| 17| R| 26| 2| | |
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|:---------|------:|:---------|------:|:---------|------:|:---------|
|
||||
| 0 | A | 9 | J | 18 | S | 27 | 3 |
|
||||
| 1 | B | 10 | K | 19 | T | 28 | 4 |
|
||||
| 2 | C | 11 | L | 20 | U | 29 | 5 |
|
||||
| 3 | D | 12 | M | 21 | V | 30 | 6 |
|
||||
| 4 | E | 13 | N | 22 | W | 31 | 7 |
|
||||
| 5 | F | 14 | O | 23 | X | | |
|
||||
| 6 | G | 15 | P | 24 | Y | (pad) | = |
|
||||
| 7 | H | 16 | Q | 25 | Z | | |
|
||||
| 8 | I | 17 | R | 26 | 2 | | |
|
||||
|
||||
|
||||
## Base 32 (extended hex) alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| 0| 9| 9| 18| I| 27| R|
|
||||
| 1| 1| 10| A| 19| J| 28| S|
|
||||
| 2| 2| 11| B| 20| K| 29| T|
|
||||
| 3| 3| 12| C| 21| L| 30| U|
|
||||
| 4| 4| 13| D| 22| M| 31| V|
|
||||
| 5| 5| 14| E| 23| N| | |
|
||||
| 6| 6| 15| F| 24| O| (pad)| =|
|
||||
| 7| 7| 16| G| 25| P| | |
|
||||
| 8| 8| 17| H| 26| Q| | |
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|:---------|------:|:---------|------:|:---------|------:|:---------|
|
||||
| 0 | 0 | 9 | 9 | 18 | I | 27 | R |
|
||||
| 1 | 1 | 10 | A | 19 | J | 28 | S |
|
||||
| 2 | 2 | 11 | B | 20 | K | 29 | T |
|
||||
| 3 | 3 | 12 | C | 21 | L | 30 | U |
|
||||
| 4 | 4 | 13 | D | 22 | M | 31 | V |
|
||||
| 5 | 5 | 14 | E | 23 | N | | |
|
||||
| 6 | 6 | 15 | F | 24 | O | (pad) | = |
|
||||
| 7 | 7 | 16 | G | 25 | P | | |
|
||||
| 8 | 8 | 17 | H | 26 | Q | | |
|
||||
|
||||
## Base 64 alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 17| R| 34| i| 51| z|
|
||||
| 1| B| 18| S| 35| j| 52| 0|
|
||||
| 2| C| 19| T| 36| k| 53| 1|
|
||||
| 3| D| 20| U| 37| l| 54| 2|
|
||||
| 4| E| 21| V| 38| m| 55| 3|
|
||||
| 5| F| 22| W| 39| n| 56| 4|
|
||||
| 6| G| 23| X| 40| o| 57| 5|
|
||||
| 7| H| 24| Y| 41| p| 58| 6|
|
||||
| 8| I| 25| Z| 42| q| 59| 7|
|
||||
| 9| J| 26| a| 43| r| 60| 8|
|
||||
| 10| K| 27| b| 44| s| 61| 9|
|
||||
| 11| L| 28| c| 45| t| 62| +|
|
||||
| 12| M| 29| d| 46| u| 63| /|
|
||||
| 13| N| 30| e| 47| v| | |
|
||||
| 14| O| 31| f| 48| w| (pad)| =|
|
||||
| 15| P| 32| g| 49| x| | |
|
||||
| 16| Q| 33| h| 50| y| | |
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|:----------|------:|:---------|------:|:---------|------:|:---------|
|
||||
| 0 | A | 17 | R | 34 | i | 51 | z |
|
||||
| 1 | B | 18 | S | 35 | j | 52 | 0 |
|
||||
| 2 | C | 19 | T | 36 | k | 53 | 1 |
|
||||
| 3 | D | 20 | U | 37 | l | 54 | 2 |
|
||||
| 4 | E | 21 | V | 38 | m | 55 | 3 |
|
||||
| 5 | F | 22 | W | 39 | n | 56 | 4 |
|
||||
| 6 | G | 23 | X | 40 | o | 57 | 5 |
|
||||
| 7 | H | 24 | Y | 41 | p | 58 | 6 |
|
||||
| 8 | I | 25 | Z | 42 | q | 59 | 7 |
|
||||
| 9 | J | 26 | a | 43 | r | 60 | 8 |
|
||||
| 10 | K | 27 | b | 44 | s | 61 | 9 |
|
||||
| 11 | L | 28 | c | 45 | t | 62 | + |
|
||||
| 12 | M | 29 | d | 46 | u | 63 | / |
|
||||
| 13 | N | 30 | e | 47 | v | | |
|
||||
| 14 | O | 31 | f | 48 | w | (pad) | = |
|
||||
| 15 | P | 32 | g | 49 | x | | |
|
||||
| 16 | Q | 33 | h | 50 | y | | |
|
||||
|
||||
## Base 64 (URL and filename safe) alphabet
|
||||
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|---------:|------:|---------:|------:|---------:|------:|---------:|
|
||||
| 0| A| 17| R| 34| i| 51| z|
|
||||
| 1| B| 18| S| 35| j| 52| 0|
|
||||
| 2| C| 19| T| 36| k| 53| 1|
|
||||
| 3| D| 20| U| 37| l| 54| 2|
|
||||
| 4| E| 21| V| 38| m| 55| 3|
|
||||
| 5| F| 22| W| 39| n| 56| 4|
|
||||
| 6| G| 23| X| 40| o| 57| 5|
|
||||
| 7| H| 24| Y| 41| p| 58| 6|
|
||||
| 8| I| 25| Z| 42| q| 59| 7|
|
||||
| 9| J| 26| a| 43| r| 60| 8|
|
||||
| 10| K| 27| b| 44| s| 61| 9|
|
||||
| 11| L| 28| c| 45| t| 62| -|
|
||||
| 12| M| 29| d| 46| u| 63| _|
|
||||
| 13| N| 30| e| 47| v| | |
|
||||
| 14| O| 31| f| 48| w| (pad)| =|
|
||||
| 15| P| 32| g| 49| x| | |
|
||||
| 16| Q| 33| h| 50| y| | |
|
||||
| Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
|
||||
|------:|:---------|------:|:---------|------:|:---------|------:|:---------|
|
||||
| 0 | A | 17 | R | 34 | i | 51 | z |
|
||||
| 1 | B | 18 | S | 35 | j | 52 | 0 |
|
||||
| 2 | C | 19 | T | 36 | k | 53 | 1 |
|
||||
| 3 | D | 20 | U | 37 | l | 54 | 2 |
|
||||
| 4 | E | 21 | V | 38 | m | 55 | 3 |
|
||||
| 5 | F | 22 | W | 39 | n | 56 | 4 |
|
||||
| 6 | G | 23 | X | 40 | o | 57 | 5 |
|
||||
| 7 | H | 24 | Y | 41 | p | 58 | 6 |
|
||||
| 8 | I | 25 | Z | 42 | q | 59 | 7 |
|
||||
| 9 | J | 26 | a | 43 | r | 60 | 8 |
|
||||
| 10 | K | 27 | b | 44 | s | 61 | 9 |
|
||||
| 11 | L | 28 | c | 45 | t | 62 | - |
|
||||
| 12 | M | 29 | d | 46 | u | 63 | _ |
|
||||
| 13 | N | 30 | e | 47 | v | | |
|
||||
| 14 | O | 31 | f | 48 | w | (pad) | = |
|
||||
| 15 | P | 32 | g | 49 | x | | |
|
||||
| 16 | Q | 33 | h | 50 | y | | |
|
||||
|
||||
"""
|
||||
|
||||
|
||||
+106
-29
@@ -1,8 +1,11 @@
|
||||
defmodule Bitwise do
|
||||
@moduledoc """
|
||||
A set of macros that perform calculations on bits.
|
||||
A set of functions that perform calculations on bits.
|
||||
|
||||
The macros in this module come in two flavors: named or
|
||||
All bitwise functions work only on integers; otherwise an
|
||||
`ArithmeticError` is raised.
|
||||
|
||||
The functions in this module come in two flavors: named or
|
||||
operators. For example:
|
||||
|
||||
iex> use Bitwise
|
||||
@@ -26,16 +29,16 @@ defmodule Bitwise do
|
||||
When invoked with no options, `use Bitwise` is equivalent
|
||||
to `import Bitwise`.
|
||||
|
||||
All bitwise macros can be used in guards:
|
||||
All bitwise functions can be used in guards:
|
||||
|
||||
iex> use Bitwise
|
||||
iex> odd? = fn
|
||||
...> int when band(int, 1) == 1 -> true
|
||||
...> int when Bitwise.band(int, 1) == 1 -> true
|
||||
...> _ -> false
|
||||
...> end
|
||||
iex> odd?.(1)
|
||||
true
|
||||
|
||||
All functions in this module are inlined by the compiler.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@@ -60,172 +63,246 @@ defmodule Bitwise do
|
||||
@doc """
|
||||
Calculates the bitwise NOT of its argument.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bnot(2)
|
||||
-3
|
||||
|
||||
iex> bnot(2) &&& 3
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro bnot(expr) do
|
||||
quote(do: :erlang.bnot(unquote(expr)))
|
||||
@spec bnot(integer) :: integer
|
||||
def bnot(expr) do
|
||||
:erlang.bnot(expr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prefix (unary) operator; calculates the bitwise NOT of its argument.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> ~~~2
|
||||
-3
|
||||
|
||||
iex> ~~~2 &&& 3
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro ~~~expr do
|
||||
quote(do: :erlang.bnot(unquote(expr)))
|
||||
@spec ~~~integer :: integer
|
||||
def ~~~expr do
|
||||
:erlang.bnot(expr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise AND of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> band(9, 3)
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro band(left, right) do
|
||||
quote(do: :erlang.band(unquote(left), unquote(right)))
|
||||
@spec band(integer, integer) :: integer
|
||||
def band(left, right) do
|
||||
:erlang.band(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the bitwise AND of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 9 &&& 3
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left &&& right do
|
||||
quote(do: :erlang.band(unquote(left), unquote(right)))
|
||||
@spec integer &&& integer :: integer
|
||||
def left &&& right do
|
||||
:erlang.band(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise OR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bor(9, 3)
|
||||
11
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro bor(left, right) do
|
||||
quote(do: :erlang.bor(unquote(left), unquote(right)))
|
||||
@spec bor(integer, integer) :: integer
|
||||
def bor(left, right) do
|
||||
:erlang.bor(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the bitwise OR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 9 ||| 3
|
||||
11
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left ||| right do
|
||||
quote(do: :erlang.bor(unquote(left), unquote(right)))
|
||||
@spec integer ||| integer :: integer
|
||||
def left ||| right do
|
||||
:erlang.bor(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise XOR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bxor(9, 3)
|
||||
10
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro bxor(left, right) do
|
||||
quote(do: :erlang.bxor(unquote(left), unquote(right)))
|
||||
@spec bxor(integer, integer) :: integer
|
||||
def bxor(left, right) do
|
||||
:erlang.bxor(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the bitwise XOR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 9 ^^^ 3
|
||||
10
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left ^^^ right do
|
||||
quote(do: :erlang.bxor(unquote(left), unquote(right)))
|
||||
@spec integer ^^^ integer :: integer
|
||||
def left ^^^ right do
|
||||
:erlang.bxor(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the result of an arithmetic left bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bsl(1, 2)
|
||||
4
|
||||
|
||||
iex> bsl(1, -2)
|
||||
0
|
||||
|
||||
iex> bsl(-1, 2)
|
||||
-4
|
||||
|
||||
iex> bsl(-1, -2)
|
||||
-1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro bsl(left, right) do
|
||||
quote(do: :erlang.bsl(unquote(left), unquote(right)))
|
||||
@spec bsl(integer, integer) :: integer
|
||||
def bsl(left, right) do
|
||||
:erlang.bsl(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the result of an arithmetic left bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 1 <<< 2
|
||||
4
|
||||
|
||||
iex> 1 <<< -2
|
||||
0
|
||||
|
||||
iex> -1 <<< 2
|
||||
-4
|
||||
|
||||
iex> -1 <<< -2
|
||||
-1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left <<< right do
|
||||
quote(do: :erlang.bsl(unquote(left), unquote(right)))
|
||||
@spec integer <<< integer :: integer
|
||||
def left <<< right do
|
||||
:erlang.bsl(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the result of an arithmetic right bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> bsr(1, 2)
|
||||
0
|
||||
|
||||
iex> bsr(1, -2)
|
||||
4
|
||||
|
||||
iex> bsr(-1, 2)
|
||||
-1
|
||||
|
||||
iex> bsr(-1, -2)
|
||||
-4
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro bsr(left, right) do
|
||||
quote(do: :erlang.bsr(unquote(left), unquote(right)))
|
||||
@spec bsr(integer, integer) :: integer
|
||||
def bsr(left, right) do
|
||||
:erlang.bsr(left, right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the result of an arithmetic right bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 1 >>> 2
|
||||
0
|
||||
|
||||
iex> 1 >>> -2
|
||||
4
|
||||
|
||||
iex> -1 >>> 2
|
||||
-1
|
||||
|
||||
iex> -1 >>> -2
|
||||
-4
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left >>> right do
|
||||
quote(do: :erlang.bsr(unquote(left), unquote(right)))
|
||||
@spec integer >>> integer :: integer
|
||||
def left >>> right do
|
||||
:erlang.bsr(left, right)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Calendar do
|
||||
For the actual date, time and datetime structures, see `Date`,
|
||||
`Time`, `NaiveDateTime` and `DateTime`.
|
||||
|
||||
Note the year, month, day, etc. designations are overspecified
|
||||
Note designations for year, month, day, and the like, are overspecified
|
||||
(i.e. an integer instead of `1..12` for months) because different
|
||||
calendars may have a different number of days per month, months per year and so on.
|
||||
"""
|
||||
@@ -23,6 +23,11 @@ defmodule Calendar do
|
||||
@type day_of_week :: non_neg_integer
|
||||
@type era :: non_neg_integer
|
||||
|
||||
@typedoc """
|
||||
A tuple representing the `day` and the `era`.
|
||||
"""
|
||||
@type day_of_era :: {day :: non_neg_integer(), era}
|
||||
|
||||
@type hour :: non_neg_integer
|
||||
@type minute :: non_neg_integer
|
||||
@type second :: non_neg_integer
|
||||
@@ -52,15 +57,15 @@ defmodule Calendar do
|
||||
representing the microseconds to external format. If the precision is 0,
|
||||
it means microseconds must be skipped.
|
||||
"""
|
||||
@type microsecond :: {0..999_999, 0..6}
|
||||
@type microsecond :: {non_neg_integer, non_neg_integer}
|
||||
|
||||
@typedoc "A calendar implementation"
|
||||
@type calendar :: module
|
||||
|
||||
@typedoc "The time zone ID according to the IANA tz database (e.g. Europe/Zurich)"
|
||||
@typedoc "The time zone ID according to the IANA tz database (for example, Europe/Zurich)"
|
||||
@type time_zone :: String.t()
|
||||
|
||||
@typedoc "The time zone abbreviation (e.g. CET or CEST or BST etc.)"
|
||||
@typedoc "The time zone abbreviation (for example, CET or CEST or BST, and such)"
|
||||
@type zone_abbr :: String.t()
|
||||
|
||||
@typedoc "The time zone UTC offset in seconds"
|
||||
@@ -122,7 +127,7 @@ defmodule Calendar do
|
||||
for any other time zone.
|
||||
|
||||
Other time zone databases (including ones provided by packages)
|
||||
can be configure as default either via configuration:
|
||||
can be configured as default either via configuration:
|
||||
|
||||
config :elixir, :time_zone_database, CustomTimeZoneDatabase
|
||||
|
||||
@@ -175,7 +180,7 @@ defmodule Calendar do
|
||||
@doc """
|
||||
Calculates the day and era from the given `year`, `month`, and `day`.
|
||||
"""
|
||||
@callback day_of_era(year, month, day) :: {non_neg_integer(), era}
|
||||
@callback day_of_era(year, month, day) :: day_of_era()
|
||||
|
||||
@doc """
|
||||
Converts the date into a string according to the calendar.
|
||||
@@ -265,6 +270,47 @@ defmodule Calendar do
|
||||
"""
|
||||
@callback valid_time?(hour, minute, second, microsecond) :: boolean
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a time returned by `c:time_to_string/4`
|
||||
into a time-tuple.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_time(String.t()) ::
|
||||
{:ok, {hour, minute, second, microsecond}}
|
||||
| {:error, atom}
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a date returned by `c:date_to_string/3`
|
||||
into a date-tuple.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_date(String.t()) ::
|
||||
{:ok, {year, month, day}}
|
||||
| {:error, atom}
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a naive datetime returned by
|
||||
`c:naive_datetime_to_string/7` into a naive-datetime-tuple.
|
||||
|
||||
The given string may contain a timezone offset but it is ignored.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_naive_datetime(String.t()) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}}
|
||||
| {:error, atom}
|
||||
|
||||
@doc """
|
||||
Parses the string representation for a datetime returned by
|
||||
`c:datetime_to_string/11` into a datetime-tuple.
|
||||
|
||||
The returned datetime must be in UTC. The original `utc_offset`
|
||||
it was written in must be returned in the result.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@callback parse_utc_datetime(String.t()) ::
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, utc_offset}
|
||||
| {:error, atom}
|
||||
|
||||
# General Helpers
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -263,30 +263,9 @@ defmodule Date do
|
||||
|
||||
"""
|
||||
@spec from_iso8601(String.t(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO)
|
||||
|
||||
def from_iso8601(<<?-, rest::binary>>, calendar) do
|
||||
with {:ok, %{year: year} = date} <- raw_from_iso8601(rest, calendar) do
|
||||
{:ok, %{date | year: -year}}
|
||||
end
|
||||
end
|
||||
|
||||
def from_iso8601(<<rest::binary>>, calendar) do
|
||||
raw_from_iso8601(rest, calendar)
|
||||
end
|
||||
|
||||
[match_date, guard_date, read_date] = Calendar.ISO.__match_date__()
|
||||
|
||||
defp raw_from_iso8601(string, calendar) do
|
||||
with unquote(match_date) <- string,
|
||||
true <- unquote(guard_date) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
|
||||
with {:ok, date} <- new(year, month, day, Calendar.ISO) do
|
||||
convert(date, calendar)
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_format}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO) do
|
||||
with {:ok, {year, month, day}} <- Calendar.ISO.parse_date(string) do
|
||||
convert(%Date{year: year, month: month, day: day}, calendar)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -769,12 +748,11 @@ defmodule Date do
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
|
||||
"~D[" <> Calendar.ISO.date_to_string(year, month, day) <> "]"
|
||||
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
|
||||
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
|
||||
end
|
||||
|
||||
def inspect(date, opts) do
|
||||
Inspect.Any.inspect(date, opts)
|
||||
end
|
||||
defp suffix(Calendar.ISO), do: ""
|
||||
defp suffix(calendar), do: " " <> inspect(calendar)
|
||||
end
|
||||
end
|
||||
|
||||
+107
-114
@@ -25,12 +25,17 @@ defmodule DateTime do
|
||||
datetimes and returns `{:error, :utc_only_time_zone_database}`
|
||||
for any other time zone.
|
||||
|
||||
Other time zone databases (including ones provided by packages)
|
||||
can be configure as default either via configuration:
|
||||
Other time zone databases can also be configured. For example, to use the
|
||||
[tzdata](https://hexdocs.pm/tzdata/) database, first make sure it is added as
|
||||
a dependency in `mix.exs`. It can then be configured either via
|
||||
configuration:
|
||||
|
||||
config :elixir, :time_zone_database, CustomTimeZoneDatabase
|
||||
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
||||
|
||||
or by calling `Calendar.put_time_zone_database/1`:
|
||||
|
||||
Calendar.put_time_zone_database(Tzdata.TimeZoneDatabase)
|
||||
|
||||
or by calling `Calendar.put_time_zone_database/1`.
|
||||
"""
|
||||
|
||||
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
|
||||
@@ -412,11 +417,13 @@ defmodule DateTime do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> cph_datetime = DateTime.from_naive!(~N[2018-07-16 12:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> {:ok, pacific_datetime} = DateTime.shift_zone(cph_datetime, "America/Los_Angeles", FakeTimeZoneDatabase)
|
||||
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
|
||||
iex> pacific_datetime
|
||||
#DateTime<2018-07-16 03:00:00-07:00 PDT America/Los_Angeles>
|
||||
|
||||
iex> DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "bad timezone", FakeTimeZoneDatabase)
|
||||
{:error, :time_zone_not_found}
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec shift_zone(t, Calendar.time_zone(), Calendar.time_zone_database()) ::
|
||||
@@ -471,6 +478,34 @@ defmodule DateTime do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changes the time zone of a `DateTime` or raises on errors.
|
||||
|
||||
See `shift_zone/3` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.shift_zone!(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
|
||||
#DateTime<2018-07-16 03:00:00-07:00 PDT America/Los_Angeles>
|
||||
|
||||
iex> DateTime.shift_zone!(~U[2018-07-16 10:00:00Z], "bad timezone", FakeTimeZoneDatabase)
|
||||
** (ArgumentError) cannot shift ~U[2018-07-16 10:00:00Z] to "bad timezone" time zone, reason: :time_zone_not_found
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec shift_zone!(t, Calendar.time_zone(), Calendar.time_zone_database()) :: t
|
||||
def shift_zone!(datetime, time_zone, time_zone_database \\ Calendar.get_time_zone_database()) do
|
||||
case shift_zone(datetime, time_zone, time_zone_database) do
|
||||
{:ok, datetime} ->
|
||||
datetime
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot shift #{inspect(datetime)} to #{inspect(time_zone)} time zone" <>
|
||||
", reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current datetime in the provided time zone.
|
||||
|
||||
@@ -485,9 +520,11 @@ defmodule DateTime do
|
||||
iex> {:ok, datetime} = DateTime.now("Etc/UTC")
|
||||
iex> datetime.time_zone
|
||||
"Etc/UTC"
|
||||
|
||||
iex> DateTime.now("Europe/Copenhagen")
|
||||
{:error, :utc_only_time_zone_database}
|
||||
iex> DateTime.now("not a real time zone name", FakeTimeZoneDatabase)
|
||||
|
||||
iex> DateTime.now("bad timezone", FakeTimeZoneDatabase)
|
||||
{:error, :time_zone_not_found}
|
||||
|
||||
"""
|
||||
@@ -504,6 +541,38 @@ defmodule DateTime do
|
||||
shift_zone(utc_now(), time_zone, time_zone_database)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current datetime in the provided time zone or raises on errors
|
||||
|
||||
See `now/2` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> datetime = DateTime.now!("Etc/UTC")
|
||||
iex> datetime.time_zone
|
||||
"Etc/UTC"
|
||||
|
||||
iex> DateTime.now!("Europe/Copenhagen")
|
||||
** (ArgumentError) cannot get current datetime in "Europe/Copenhagen" time zone, reason: :utc_only_time_zone_database
|
||||
|
||||
iex> DateTime.now!("bad timezone", FakeTimeZoneDatabase)
|
||||
** (ArgumentError) cannot get current datetime in "bad timezone" time zone, reason: :time_zone_not_found
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec now!(Calendar.time_zone(), Calendar.time_zone_database()) :: t
|
||||
def now!(time_zone, time_zone_database \\ Calendar.get_time_zone_database()) do
|
||||
case now(time_zone, time_zone_database) do
|
||||
{:ok, datetime} ->
|
||||
datetime
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot get current datetime in #{inspect(time_zone)} time zone, reason: " <>
|
||||
inspect(reason)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `datetime` to Unix time.
|
||||
|
||||
@@ -701,25 +770,14 @@ defmodule DateTime do
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
time_zone: time_zone,
|
||||
zone_abbr: zone_abbr,
|
||||
utc_offset: utc_offset,
|
||||
std_offset: std_offset
|
||||
} = datetime
|
||||
|
||||
Calendar.ISO.datetime_to_iso8601(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
time_zone,
|
||||
zone_abbr,
|
||||
utc_offset,
|
||||
std_offset,
|
||||
format
|
||||
)
|
||||
Calendar.ISO.date_to_string(year, month, day, format) <>
|
||||
"T" <>
|
||||
Calendar.ISO.time_to_string(hour, minute, second, microsecond, format) <>
|
||||
Calendar.ISO.offset_to_string(utc_offset, std_offset, time_zone, format)
|
||||
end
|
||||
|
||||
def to_iso8601(%{calendar: _} = datetime, format) when format in [:extended, :basic] do
|
||||
@@ -782,91 +840,26 @@ defmodule DateTime do
|
||||
@doc since: "1.4.0"
|
||||
@spec from_iso8601(String.t(), Calendar.calendar()) ::
|
||||
{:ok, t, Calendar.utc_offset()} | {:error, atom}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO)
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO) do
|
||||
with {:ok, {year, month, day, hour, minute, second, microsecond}, offset} <-
|
||||
Calendar.ISO.parse_utc_datetime(string) do
|
||||
datetime = %DateTime{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
def from_iso8601(<<?-, rest::binary>>, calendar) do
|
||||
raw_from_iso8601(rest, calendar, true)
|
||||
end
|
||||
|
||||
def from_iso8601(<<rest::binary>>, calendar) do
|
||||
raw_from_iso8601(rest, calendar, false)
|
||||
end
|
||||
|
||||
@sep [?\s, ?T]
|
||||
[match_date, guard_date, read_date] = Calendar.ISO.__match_date__()
|
||||
[match_time, guard_time, read_time] = Calendar.ISO.__match_time__()
|
||||
|
||||
defp raw_from_iso8601(string, calendar, is_year_negative) do
|
||||
with <<unquote(match_date), sep, unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_date) and sep in @sep and unquote(guard_time),
|
||||
{microsecond, rest} <- Calendar.ISO.parse_microsecond(rest),
|
||||
{offset, ""} <- Calendar.ISO.parse_offset(rest) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
year = if is_year_negative, do: -year, else: year
|
||||
|
||||
cond do
|
||||
not calendar.valid_date?(year, month, day) ->
|
||||
{:error, :invalid_date}
|
||||
|
||||
not calendar.valid_time?(hour, minute, second, microsecond) ->
|
||||
{:error, :invalid_time}
|
||||
|
||||
offset == 0 ->
|
||||
datetime = %DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
{:ok, datetime, 0}
|
||||
|
||||
is_nil(offset) ->
|
||||
{:error, :missing_offset}
|
||||
|
||||
true ->
|
||||
day_fraction = Calendar.ISO.time_to_day_fraction(hour, minute, second, {0, 0})
|
||||
|
||||
{{year, month, day}, {hour, minute, second, _}} =
|
||||
case apply_tz_offset({0, day_fraction}, offset) do
|
||||
{0, day_fraction} ->
|
||||
{{year, month, day}, Calendar.ISO.time_from_day_fraction(day_fraction)}
|
||||
|
||||
{extra_days, day_fraction} ->
|
||||
base_days = Calendar.ISO.date_to_iso_days(year, month, day)
|
||||
|
||||
{Calendar.ISO.date_from_iso_days(base_days + extra_days),
|
||||
Calendar.ISO.time_from_day_fraction(day_fraction)}
|
||||
end
|
||||
|
||||
datetime = %DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
{:ok, datetime, offset}
|
||||
with {:ok, converted} <- convert(datetime, calendar) do
|
||||
{:ok, converted, offset}
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1088,7 +1081,7 @@ defmodule DateTime do
|
||||
|
||||
@doc """
|
||||
Returns the given datetime with the microsecond field truncated to the given
|
||||
precision (`:microsecond`, `millisecond` or `:second`).
|
||||
precision (`:microsecond`, `:millisecond` or `:second`).
|
||||
|
||||
The given datetime is returned unchanged if it already has lower precision than
|
||||
the given precision.
|
||||
@@ -1304,7 +1297,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(%{calendar: Calendar.ISO} = datetime, _) do
|
||||
def inspect(datetime, _) do
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
@@ -1316,11 +1309,12 @@ defmodule DateTime do
|
||||
time_zone: time_zone,
|
||||
zone_abbr: zone_abbr,
|
||||
utc_offset: utc_offset,
|
||||
std_offset: std_offset
|
||||
std_offset: std_offset,
|
||||
calendar: calendar
|
||||
} = datetime
|
||||
|
||||
formatted =
|
||||
Calendar.ISO.datetime_to_string(
|
||||
calendar.datetime_to_string(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
@@ -1336,15 +1330,14 @@ defmodule DateTime do
|
||||
|
||||
case datetime do
|
||||
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
|
||||
"~U[" <> formatted <> "]"
|
||||
"~U[" <> formatted <> suffix(calendar) <> "]"
|
||||
|
||||
_ ->
|
||||
"#DateTime<" <> formatted <> ">"
|
||||
"#DateTime<" <> formatted <> suffix(calendar) <> ">"
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(datetime, opts) do
|
||||
Inspect.Any.inspect(datetime, opts)
|
||||
end
|
||||
defp suffix(Calendar.ISO), do: ""
|
||||
defp suffix(calendar), do: " " <> inspect(calendar)
|
||||
end
|
||||
end
|
||||
|
||||
+414
-125
@@ -20,9 +20,48 @@ defmodule Calendar.ISO do
|
||||
unix_end = 315_569_519_999_999_999 - @unix_epoch * 1_000_000
|
||||
@unix_range_microseconds unix_start..unix_end
|
||||
|
||||
@typedoc """
|
||||
"Before the Current Era" or "Before the Common Era" (BCE), for those years less than `1`.
|
||||
"""
|
||||
@type bce :: 0
|
||||
|
||||
@typedoc """
|
||||
The "Current Era" or the "Common Era" (CE) which starts in year `1`.
|
||||
"""
|
||||
@type ce :: 1
|
||||
|
||||
@typedoc """
|
||||
The calendar era.
|
||||
|
||||
The ISO calendar has two eras:
|
||||
* [CE](`t:ce/0`) - which starts in year `1` and is defined as era `1`.
|
||||
* [BCE](`t:bce/0`) - for those years less than `1` and is defined as era `0`.
|
||||
"""
|
||||
@type era :: bce | ce
|
||||
@type year :: -9999..9999
|
||||
@type month :: 1..12
|
||||
@type day :: 1..31
|
||||
@type hour :: 0..23
|
||||
@type minute :: 0..59
|
||||
@type second :: 0..59
|
||||
|
||||
@typedoc """
|
||||
Microseconds with stored precision.
|
||||
|
||||
The precision represents the number of digits that must be used when
|
||||
representing the microseconds to external format. If the precision is 0,
|
||||
it means microseconds must be skipped.
|
||||
"""
|
||||
@type microsecond :: {0..999_999, 0..6}
|
||||
|
||||
@typedoc """
|
||||
Integer that represents the day of the week, where 1 is Monday and 7 is Sunday.
|
||||
"""
|
||||
@type day_of_week :: 1..7
|
||||
|
||||
@type day_of_year :: 1..366
|
||||
@type quarter_of_year :: 1..4
|
||||
@type year_of_era :: {1..10000, era}
|
||||
|
||||
@seconds_per_minute 60
|
||||
@seconds_per_hour 60 * 60
|
||||
@@ -32,18 +71,16 @@ defmodule Calendar.ISO do
|
||||
@microseconds_per_second 1_000_000
|
||||
@parts_per_day @seconds_per_day * @microseconds_per_second
|
||||
|
||||
@sep [?\s, ?T]
|
||||
@days_per_nonleap_year 365
|
||||
@days_per_leap_year 366
|
||||
|
||||
@months_in_year 12
|
||||
|
||||
# The ISO epoch starts, in this implementation,
|
||||
# with ~D[0000-01-01]. Era "1" starts
|
||||
# on ~D[0001-01-01] which is 366 days later.
|
||||
@iso_epoch 366
|
||||
|
||||
@doc false
|
||||
def __match_date__ do
|
||||
[match_date, guard_date, read_date] =
|
||||
quote do
|
||||
[
|
||||
<<y1, y2, y3, y4, ?-, m1, m2, ?-, d1, d2>>,
|
||||
@@ -57,10 +94,8 @@ defmodule Calendar.ISO do
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __match_time__ do
|
||||
[match_time, guard_time, read_time] =
|
||||
quote do
|
||||
[
|
||||
<<h1, h2, ?:, i1, i2, ?:, s1, s2>>,
|
||||
@@ -73,6 +108,276 @@ defmodule Calendar.ISO do
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defguardp is_year(year) when year in -9999..9999
|
||||
defguardp is_year_BCE(year) when year in -9999..0
|
||||
defguardp is_year_CE(year) when year in 1..9999
|
||||
defguardp is_month(month) when month in 1..12
|
||||
defguardp is_day(day) when day in 1..31
|
||||
defguardp is_hour(hour) when hour in 0..23
|
||||
defguardp is_minute(minute) when minute in 0..59
|
||||
defguardp is_second(second) when second in 0..59
|
||||
|
||||
defguardp is_microsecond(microsecond, precision)
|
||||
when microsecond in 0..999_999 and precision in 0..6
|
||||
|
||||
defguardp is_time_zone(term) when is_binary(term)
|
||||
defguardp is_zone_abbr(term) when is_binary(term)
|
||||
defguardp is_utc_offset(offset) when is_integer(offset)
|
||||
defguardp is_std_offset(offset) when is_integer(offset)
|
||||
|
||||
@doc """
|
||||
Parses a time string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_time("23:50:07")
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_time("23:50:07Z")
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_time("T23:50:07Z")
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
|
||||
iex> Calendar.ISO.parse_time("23:50:07,0123456")
|
||||
{:ok, {23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_time("23:50:07.0123456")
|
||||
{:ok, {23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_time("23:50:07.123Z")
|
||||
{:ok, {23, 50, 7, {123000, 3}}}
|
||||
|
||||
iex> Calendar.ISO.parse_time("2015:01:23 23-50-07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_time("23:50:07A")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_time("23:50:07.")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_time("23:50:61")
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_time("T" <> string) when is_binary(string),
|
||||
do: do_parse_time(string)
|
||||
|
||||
def parse_time(string) when is_binary(string),
|
||||
do: do_parse_time(string)
|
||||
|
||||
defp do_parse_time(string) do
|
||||
with <<unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_time),
|
||||
{microsecond, rest} <- parse_microsecond(rest),
|
||||
{_offset, ""} <- parse_offset(rest) do
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
|
||||
if valid_time?(hour, minute, second, microsecond) do
|
||||
{:ok, {hour, minute, second, microsecond}}
|
||||
else
|
||||
{:error, :invalid_time}
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a date string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_date("2015-01-23")
|
||||
{:ok, {2015, 1, 23}}
|
||||
iex> Calendar.ISO.parse_date("-2015-01-23")
|
||||
{:ok, {-2015, 1, 23}}
|
||||
iex> Calendar.ISO.parse_date("2015:01:23")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_date("2015-01-32")
|
||||
{:error, :invalid_date}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_date("-" <> string) when is_binary(string),
|
||||
do: parse_date(string, -1)
|
||||
|
||||
def parse_date(string) when is_binary(string),
|
||||
do: parse_date(string, 1)
|
||||
|
||||
defp parse_date(string, multiplier) do
|
||||
with unquote(match_date) <- string, true <- unquote(guard_date) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
year = multiplier * year
|
||||
|
||||
if valid_date?(year, month, day) do
|
||||
{:ok, {year, month, day}}
|
||||
else
|
||||
{:error, :invalid_date}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a naive datetime string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07.0")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 1}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07,0123456")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07.0123456")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23P23:50:07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015:01:23 23-50-07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07A")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:61")
|
||||
{:error, :invalid_time}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-32 23:50:07")
|
||||
{:error, :invalid_date}
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123+02:30")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123+00:00")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-02:30")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-00:00")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-00:60")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-24:00")
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_naive_datetime("-" <> string) when is_binary(string),
|
||||
do: parse_naive_datetime(string, -1)
|
||||
|
||||
def parse_naive_datetime(string) when is_binary(string),
|
||||
do: parse_naive_datetime(string, 1)
|
||||
|
||||
defp parse_naive_datetime(string, multiplier) do
|
||||
with <<unquote(match_date), sep, unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_date) and sep in @sep and unquote(guard_time),
|
||||
{microsecond, rest} <- parse_microsecond(rest),
|
||||
{_offset, ""} <- parse_offset(rest) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
year = multiplier * year
|
||||
|
||||
cond do
|
||||
not valid_date?(year, month, day) ->
|
||||
{:error, :invalid_date}
|
||||
|
||||
not valid_time?(hour, minute, second, microsecond) ->
|
||||
{:error, :invalid_time}
|
||||
|
||||
true ->
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}}
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a UTC datetime string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}, 0}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07.123+02:30")
|
||||
{:ok, {2015, 1, 23, 21, 20, 7, {123000, 3}}, 9000}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07,123+02:30")
|
||||
{:ok, {2015, 1, 23, 21, 20, 7, {123000, 3}}, 9000}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("-2015-01-23T23:50:07Z")
|
||||
{:ok, {-2015, 1, 23, 23, 50, 7, {0, 0}}, 0}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("-2015-01-23T23:50:07,123+02:30")
|
||||
{:ok, {-2015, 1, 23, 21, 20, 7, {123000, 3}}, 9000}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23P23:50:07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07")
|
||||
{:error, :missing_offset}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23 23:50:61")
|
||||
{:error, :invalid_time}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-32 23:50:07")
|
||||
{:error, :invalid_date}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07.123-00:00")
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_utc_datetime("-" <> string) when is_binary(string),
|
||||
do: parse_utc_datetime(string, -1)
|
||||
|
||||
def parse_utc_datetime(string) when is_binary(string),
|
||||
do: parse_utc_datetime(string, 1)
|
||||
|
||||
defp parse_utc_datetime(string, multiplier) do
|
||||
with <<unquote(match_date), sep, unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_date) and sep in @sep and unquote(guard_time),
|
||||
{microsecond, rest} <- parse_microsecond(rest),
|
||||
{offset, ""} <- parse_offset(rest) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
year = multiplier * year
|
||||
|
||||
cond do
|
||||
not valid_date?(year, month, day) ->
|
||||
{:error, :invalid_date}
|
||||
|
||||
not valid_time?(hour, minute, second, microsecond) ->
|
||||
{:error, :invalid_time}
|
||||
|
||||
offset == 0 ->
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, offset}
|
||||
|
||||
is_nil(offset) ->
|
||||
{:error, :missing_offset}
|
||||
|
||||
true ->
|
||||
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
|
||||
|
||||
{{year, month, day}, {hour, minute, second, _}} =
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
|
||||
{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)
|
||||
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
|
||||
end
|
||||
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, offset}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -179,7 +484,7 @@ defmodule Calendar.ISO do
|
||||
@doc since: "1.5.0"
|
||||
@impl true
|
||||
@spec time_from_day_fraction(Calendar.day_fraction()) ::
|
||||
{Calendar.hour(), Calendar.minute(), Calendar.second(), Calendar.microsecond()}
|
||||
{hour(), minute(), second(), microsecond()}
|
||||
def time_from_day_fraction({0, _}) do
|
||||
{0, 0, 0, {0, 6}}
|
||||
end
|
||||
@@ -212,7 +517,7 @@ defmodule Calendar.ISO do
|
||||
719_528
|
||||
end
|
||||
|
||||
def date_to_iso_days(year, month, day) when year in -9999..9999 do
|
||||
def date_to_iso_days(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
|
||||
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
|
||||
@@ -263,14 +568,16 @@ defmodule Calendar.ISO do
|
||||
@doc since: "1.4.0"
|
||||
@spec days_in_month(year, month) :: 28..31
|
||||
@impl true
|
||||
def days_in_month(year, month)
|
||||
def days_in_month(year, month) when is_year(year) and is_month(month) do
|
||||
days_in_month_guarded(year, month)
|
||||
end
|
||||
|
||||
def days_in_month(year, 2) do
|
||||
defp days_in_month_guarded(year, 2) do
|
||||
if leap_year?(year), do: 29, else: 28
|
||||
end
|
||||
|
||||
def days_in_month(_, month) when month in [4, 6, 9, 11], do: 30
|
||||
def days_in_month(_, month) when month in 1..12, do: 31
|
||||
defp days_in_month_guarded(_, month) when month in [4, 6, 9, 11], do: 30
|
||||
defp days_in_month_guarded(_, _), do: 31
|
||||
|
||||
@doc """
|
||||
Returns how many months there are in the given year.
|
||||
@@ -284,8 +591,8 @@ defmodule Calendar.ISO do
|
||||
@doc since: "1.7.0"
|
||||
@impl true
|
||||
@spec months_in_year(year) :: 12
|
||||
def months_in_year(_year) do
|
||||
@months_in_year
|
||||
def months_in_year(year) when is_year(year) do
|
||||
12
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -308,7 +615,7 @@ defmodule Calendar.ISO do
|
||||
@doc since: "1.3.0"
|
||||
@spec leap_year?(year) :: boolean()
|
||||
@impl true
|
||||
def leap_year?(year) when is_integer(year) do
|
||||
def leap_year?(year) when is_year(year) do
|
||||
rem(year, 4) === 0 and (rem(year, 100) !== 0 or rem(year, 400) === 0)
|
||||
end
|
||||
|
||||
@@ -338,11 +645,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec day_of_week(year, month, day) :: 1..7
|
||||
@spec day_of_week(year, month, day) :: day_of_week()
|
||||
@impl true
|
||||
def day_of_week(year, month, day)
|
||||
when is_integer(year) and is_integer(month) and is_integer(day) do
|
||||
iso_days_to_day_of_week(date_to_iso_days(year, month, day))
|
||||
def day_of_week(year, month, day) do
|
||||
date_to_iso_days(year, month, day)
|
||||
|> iso_days_to_day_of_week()
|
||||
end
|
||||
|
||||
defp iso_days_to_day_of_week(iso_days) do
|
||||
@@ -365,10 +672,9 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec day_of_year(year, month, day) :: 1..366
|
||||
@spec day_of_year(year, month, day) :: day_of_year()
|
||||
@impl true
|
||||
def day_of_year(year, month, day)
|
||||
when is_integer(year) and is_integer(month) and is_integer(day) do
|
||||
def day_of_year(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
days_before_month(month) + leap_day_offset(year, month) + day
|
||||
end
|
||||
@@ -391,20 +697,19 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec quarter_of_year(year, month, day) :: 1..4
|
||||
@spec quarter_of_year(year, month, day) :: quarter_of_year()
|
||||
@impl true
|
||||
def quarter_of_year(year, month, day)
|
||||
when is_integer(year) and is_integer(month) and is_integer(day) do
|
||||
when is_year(year) and is_month(month) and is_day(day) do
|
||||
div(month - 1, 3) + 1
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the year and era from the given `year`.
|
||||
|
||||
The ISO calendar has two eras: the current era which
|
||||
starts in year 1 and is defined as era "1". And a
|
||||
second era for those years less than 1 defined as
|
||||
era "0".
|
||||
The ISO calendar has two eras: the "current era" (CE) which
|
||||
starts in year `1` and is defined as era `1`. And "before the current
|
||||
era" (BCE) for those years less than `1`, defined as era `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -419,13 +724,13 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec year_of_era(year) :: {year, era :: 0..1}
|
||||
@spec year_of_era(year) :: {1..10000, era}
|
||||
@impl true
|
||||
def year_of_era(year) when is_integer(year) and year > 0 do
|
||||
def year_of_era(year) when is_year_CE(year) do
|
||||
{year, 1}
|
||||
end
|
||||
|
||||
def year_of_era(year) when is_integer(year) and year < 1 do
|
||||
def year_of_era(year) when is_year_BCE(year) do
|
||||
{abs(year) + 1, 0}
|
||||
end
|
||||
|
||||
@@ -447,16 +752,14 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec day_of_era(year, month, day) :: {day :: pos_integer(), era :: 0..1}
|
||||
@spec day_of_era(year, month, day) :: Calendar.day_of_era()
|
||||
@impl true
|
||||
def day_of_era(year, month, day)
|
||||
when is_integer(year) and is_integer(month) and is_integer(day) and year > 0 do
|
||||
def day_of_era(year, month, day) when is_year_CE(year) do
|
||||
day = date_to_iso_days(year, month, day) - @iso_epoch + 1
|
||||
{day, 1}
|
||||
end
|
||||
|
||||
def day_of_era(year, month, day)
|
||||
when is_integer(year) and is_integer(month) and is_integer(day) and year < 1 do
|
||||
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, 0}
|
||||
end
|
||||
@@ -492,14 +795,23 @@ defmodule Calendar.ISO do
|
||||
Calendar.microsecond(),
|
||||
:basic | :extended
|
||||
) :: String.t()
|
||||
def time_to_string(hour, minute, second, microsecond, format \\ :extended)
|
||||
def time_to_string(
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
{ms_value, ms_precision} = microsecond,
|
||||
format \\ :extended
|
||||
)
|
||||
when is_hour(hour) and is_minute(minute) and is_second(second) and
|
||||
is_microsecond(ms_value, ms_precision) and format in [:basic, :extended] do
|
||||
time_to_string_guarded(hour, minute, second, microsecond, format)
|
||||
end
|
||||
|
||||
def time_to_string(hour, minute, second, {_, 0}, format) when format in [:basic, :extended] do
|
||||
defp time_to_string_guarded(hour, minute, second, {_, 0}, format) do
|
||||
time_to_string_format(hour, minute, second, format)
|
||||
end
|
||||
|
||||
def time_to_string(hour, minute, second, {microsecond, precision}, format)
|
||||
when format in [:basic, :extended] do
|
||||
defp time_to_string_guarded(hour, minute, second, {microsecond, precision}, format) do
|
||||
time_to_string_format(hour, minute, second, format) <>
|
||||
"." <> (microsecond |> zero_pad(6) |> binary_part(0, precision))
|
||||
end
|
||||
@@ -538,12 +850,16 @@ defmodule Calendar.ISO do
|
||||
@spec date_to_string(year, month, day, :basic | :extended) :: String.t()
|
||||
@impl true
|
||||
def date_to_string(year, month, day, format \\ :extended)
|
||||
when is_integer(year) and is_integer(month) and is_integer(day) and
|
||||
format in [:basic, :extended] do
|
||||
date_to_string_guarded(year, month, day, format)
|
||||
end
|
||||
|
||||
def date_to_string(year, month, day, :extended) do
|
||||
defp date_to_string_guarded(year, month, day, :extended) do
|
||||
zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2)
|
||||
end
|
||||
|
||||
def date_to_string(year, month, day, :basic) do
|
||||
defp date_to_string_guarded(year, month, day, :basic) do
|
||||
zero_pad(year, 4) <> zero_pad(month, 2) <> zero_pad(day, 2)
|
||||
end
|
||||
|
||||
@@ -586,8 +902,7 @@ defmodule Calendar.ISO do
|
||||
second,
|
||||
microsecond,
|
||||
format \\ :extended
|
||||
)
|
||||
when format in [:basic, :extended] do
|
||||
) do
|
||||
date_to_string(year, month, day, format) <>
|
||||
" " <> time_to_string(hour, minute, second, microsecond, format)
|
||||
end
|
||||
@@ -601,6 +916,14 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> time_zone = "Etc/UTC"
|
||||
iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, time_zone, "UTC", 0, 0)
|
||||
"2017-08-01 01:02:03.00000Z"
|
||||
iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, time_zone, "UTC", 3600, 0)
|
||||
"2017-08-01 01:02:03.00000+01:00"
|
||||
iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, time_zone, "UTC", 3600, 3600)
|
||||
"2017-08-01 01:02:03.00000+02:00"
|
||||
|
||||
iex> time_zone = "Europe/Berlin"
|
||||
iex> Calendar.ISO.datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5}, time_zone, "CET", 3600, 0)
|
||||
"2017-08-01 01:02:03.00000+01:00 CET Europe/Berlin"
|
||||
@@ -648,7 +971,8 @@ defmodule Calendar.ISO do
|
||||
std_offset,
|
||||
format \\ :extended
|
||||
)
|
||||
when format in [:basic, :extended] do
|
||||
when is_time_zone(time_zone) and is_zone_abbr(zone_abbr) and is_utc_offset(utc_offset) and
|
||||
is_std_offset(std_offset) do
|
||||
date_to_string(year, month, day, format) <>
|
||||
" " <>
|
||||
time_to_string(hour, minute, second, microsecond, format) <>
|
||||
@@ -656,6 +980,28 @@ defmodule Calendar.ISO do
|
||||
zone_to_string(utc_offset, std_offset, zone_abbr, time_zone)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def offset_to_string(0, 0, "Etc/UTC", _format), do: "Z"
|
||||
|
||||
def offset_to_string(utc, std, _zone, format) do
|
||||
total = utc + std
|
||||
second = abs(total)
|
||||
minute = second |> rem(3600) |> div(60)
|
||||
hour = div(second, 3600)
|
||||
format_offset(total, hour, minute, format)
|
||||
end
|
||||
|
||||
defp format_offset(total, hour, minute, :extended) do
|
||||
sign(total) <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2)
|
||||
end
|
||||
|
||||
defp format_offset(total, hour, minute, :basic) do
|
||||
sign(total) <> zero_pad(hour, 2) <> zero_pad(minute, 2)
|
||||
end
|
||||
|
||||
defp zone_to_string(_, _, _, "Etc/UTC"), do: ""
|
||||
defp zone_to_string(_, _, abbr, zone), do: " " <> abbr <> " " <> zone
|
||||
|
||||
@doc """
|
||||
Determines if the date given is valid according to the proleptic Gregorian calendar.
|
||||
|
||||
@@ -674,9 +1020,9 @@ defmodule Calendar.ISO do
|
||||
@doc since: "1.5.0"
|
||||
@impl true
|
||||
@spec valid_date?(year, month, day) :: boolean
|
||||
def valid_date?(year, month, day) do
|
||||
month in 1..12 and year in -9999..9999 and
|
||||
(is_integer(day) and day >= 1 and day <= days_in_month(year, month))
|
||||
def valid_date?(year, month, day)
|
||||
when is_integer(year) and is_integer(month) and is_integer(day) do
|
||||
is_year(year) and is_month(month) and day in 1..days_in_month(year, month)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -700,9 +1046,11 @@ defmodule Calendar.ISO do
|
||||
@impl true
|
||||
@spec valid_time?(Calendar.hour(), Calendar.minute(), Calendar.second(), Calendar.microsecond()) ::
|
||||
boolean
|
||||
def valid_time?(hour, minute, second, {microsecond, precision}) do
|
||||
hour in 0..23 and minute in 0..59 and second in 0..59 and microsecond in 0..999_999 and
|
||||
precision in 0..6
|
||||
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_hour(hour) and is_minute(minute) and is_second(second) and
|
||||
is_microsecond(ms_value, ms_precision)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -715,27 +1063,6 @@ defmodule Calendar.ISO do
|
||||
{0, 1}
|
||||
end
|
||||
|
||||
defp offset_to_string(0, 0, "Etc/UTC", _format), do: "Z"
|
||||
|
||||
defp offset_to_string(utc, std, _zone, format) do
|
||||
total = utc + std
|
||||
second = abs(total)
|
||||
minute = second |> rem(3600) |> div(60)
|
||||
hour = div(second, 3600)
|
||||
format_offset(total, hour, minute, format)
|
||||
end
|
||||
|
||||
defp format_offset(total, hour, minute, :extended) do
|
||||
sign(total) <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2)
|
||||
end
|
||||
|
||||
defp format_offset(total, hour, minute, :basic) do
|
||||
sign(total) <> zero_pad(hour, 2) <> zero_pad(minute, 2)
|
||||
end
|
||||
|
||||
defp zone_to_string(0, 0, _abbr, "Etc/UTC"), do: ""
|
||||
defp zone_to_string(_, _, abbr, zone), do: " " <> abbr <> " " <> zone
|
||||
|
||||
defp sign(total) when total < 0, do: "-"
|
||||
defp sign(_), do: "+"
|
||||
|
||||
@@ -777,44 +1104,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def naive_datetime_to_iso8601(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
format \\ :extended
|
||||
) do
|
||||
date_to_string(year, month, day, format) <>
|
||||
"T" <> time_to_string(hour, minute, second, microsecond, format)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def datetime_to_iso8601(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
time_zone,
|
||||
_zone_abbr,
|
||||
utc_offset,
|
||||
std_offset,
|
||||
format \\ :extended
|
||||
) do
|
||||
date_to_string(year, month, day, format) <>
|
||||
"T" <>
|
||||
time_to_string(hour, minute, second, microsecond, format) <>
|
||||
offset_to_string(utc_offset, std_offset, time_zone, format)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def parse_microsecond("." <> rest) do
|
||||
defp parse_microsecond("." <> rest) do
|
||||
case parse_microsecond(rest, 0, "") do
|
||||
{"", 0, _} ->
|
||||
:error
|
||||
@@ -828,11 +1118,11 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
def parse_microsecond("," <> rest) do
|
||||
defp parse_microsecond("," <> rest) do
|
||||
parse_microsecond("." <> rest)
|
||||
end
|
||||
|
||||
def parse_microsecond(rest) do
|
||||
defp parse_microsecond(rest) do
|
||||
{{0, 0}, rest}
|
||||
end
|
||||
|
||||
@@ -841,26 +1131,25 @@ defmodule Calendar.ISO do
|
||||
|
||||
defp parse_microsecond(rest, precision, acc), do: {acc, precision, rest}
|
||||
|
||||
@doc false
|
||||
def parse_offset(""), do: {nil, ""}
|
||||
def parse_offset("Z"), do: {0, ""}
|
||||
def parse_offset("-00:00"), do: :error
|
||||
defp parse_offset(""), do: {nil, ""}
|
||||
defp parse_offset("Z"), do: {0, ""}
|
||||
defp parse_offset("-00:00"), do: :error
|
||||
|
||||
def parse_offset(<<?+, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
|
||||
defp parse_offset(<<?+, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
|
||||
do: parse_offset(1, hour, min, rest)
|
||||
|
||||
def parse_offset(<<?-, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
|
||||
defp parse_offset(<<?-, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
|
||||
do: parse_offset(-1, hour, min, rest)
|
||||
|
||||
def parse_offset(<<?+, hour::2-bytes, min::2-bytes, rest::binary>>),
|
||||
defp parse_offset(<<?+, hour::2-bytes, min::2-bytes, rest::binary>>),
|
||||
do: parse_offset(1, hour, min, rest)
|
||||
|
||||
def parse_offset(<<?-, hour::2-bytes, min::2-bytes, rest::binary>>),
|
||||
defp parse_offset(<<?-, hour::2-bytes, min::2-bytes, rest::binary>>),
|
||||
do: parse_offset(-1, hour, min, rest)
|
||||
|
||||
def parse_offset(<<?+, hour::2-bytes, rest::binary>>), do: parse_offset(1, hour, "00", rest)
|
||||
def parse_offset(<<?-, hour::2-bytes, rest::binary>>), do: parse_offset(-1, hour, "00", rest)
|
||||
def parse_offset(_), do: :error
|
||||
defp parse_offset(<<?+, hour::2-bytes, rest::binary>>), do: parse_offset(1, hour, "00", rest)
|
||||
defp parse_offset(<<?-, hour::2-bytes, rest::binary>>), do: parse_offset(-1, hour, "00", rest)
|
||||
defp parse_offset(_), do: :error
|
||||
|
||||
defp parse_offset(sign, hour, min, rest) do
|
||||
with {hour, ""} when hour < 24 <- Integer.parse(hour),
|
||||
@@ -1037,7 +1326,7 @@ defmodule Calendar.ISO do
|
||||
{hour, minute, second}
|
||||
end
|
||||
|
||||
defp ensure_day_in_month!(year, month, day) do
|
||||
defp ensure_day_in_month!(year, month, day) when is_integer(day) do
|
||||
if day < 1 or day > days_in_month(year, month) do
|
||||
raise ArgumentError, "invalid date: #{date_to_string(year, month, day)}"
|
||||
end
|
||||
|
||||
@@ -117,6 +117,53 @@ defmodule NaiveDateTime do
|
||||
|> DateTime.to_naive()
|
||||
end
|
||||
|
||||
@doc """
|
||||
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
|
||||
daylight saving jumps (spring forward or fall back).
|
||||
|
||||
This function can be used to display what the time is right now for the time
|
||||
zone configuration that the machine happens to have. An example would be a
|
||||
desktop program displaying a clock to the user. For any other uses it is
|
||||
probably a bad idea to use this function.
|
||||
|
||||
For most cases, use `DateTime.now/2` or `DateTime.utc_now/1` instead.
|
||||
|
||||
Does not include fractional seconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> naive_datetime = NaiveDateTime.local_now()
|
||||
iex> naive_datetime.year >= 2019
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec local_now(Calendar.calendar()) :: t
|
||||
def local_now(calendar \\ Calendar.ISO)
|
||||
|
||||
def local_now(Calendar.ISO) do
|
||||
{{year, month, day}, {hour, minute, second}} = :erlang.localtime()
|
||||
{:ok, ndt} = NaiveDateTime.new(year, month, day, hour, minute, second)
|
||||
ndt
|
||||
end
|
||||
|
||||
def local_now(calendar) do
|
||||
naive_datetime = local_now()
|
||||
|
||||
case convert(naive_datetime, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, :incompatible_calendars} ->
|
||||
raise ArgumentError,
|
||||
~s(cannot get "local now" in target calendar #{inspect(calendar)}, ) <>
|
||||
"reason: cannot convert from Calendar.ISO to #{inspect(calendar)}."
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new ISO naive datetime.
|
||||
|
||||
@@ -161,7 +208,7 @@ defmodule NaiveDateTime do
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: {:ok, t} | {:error, atom}
|
||||
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
@@ -534,35 +581,21 @@ defmodule NaiveDateTime do
|
||||
|
||||
"""
|
||||
@spec from_iso8601(String.t(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO)
|
||||
|
||||
def from_iso8601(<<?-, rest::binary>>, calendar) do
|
||||
with {:ok, %{year: year} = naive_datetime} <- raw_from_iso8601(rest, calendar) do
|
||||
{:ok, %{naive_datetime | year: -year}}
|
||||
end
|
||||
end
|
||||
|
||||
def from_iso8601(<<rest::binary>>, calendar) do
|
||||
raw_from_iso8601(rest, calendar)
|
||||
end
|
||||
|
||||
@sep [?\s, ?T]
|
||||
[match_date, guard_date, read_date] = Calendar.ISO.__match_date__()
|
||||
[match_time, guard_time, read_time] = Calendar.ISO.__match_time__()
|
||||
|
||||
defp raw_from_iso8601(string, calendar) do
|
||||
with <<unquote(match_date), sep, unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_date) and sep in @sep and unquote(guard_time),
|
||||
{microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
|
||||
{_offset, ""} <- Calendar.ISO.parse_offset(rest) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, min, sec} = unquote(read_time)
|
||||
|
||||
with {:ok, iso_naive_dt} <- new(year, month, day, hour, min, sec, microsec, Calendar.ISO) do
|
||||
convert(iso_naive_dt, calendar)
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_format}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO) do
|
||||
with {:ok, {year, month, day, hour, minute, second, microsecond}} <-
|
||||
Calendar.ISO.parse_naive_datetime(string) do
|
||||
convert(
|
||||
%NaiveDateTime{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
},
|
||||
calendar
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -640,16 +673,8 @@ defmodule NaiveDateTime do
|
||||
microsecond: microsecond
|
||||
} = naive_datetime
|
||||
|
||||
Calendar.ISO.naive_datetime_to_iso8601(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
format
|
||||
)
|
||||
Calendar.ISO.date_to_string(year, month, day, format) <>
|
||||
"T" <> Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
|
||||
end
|
||||
|
||||
def to_iso8601(%{calendar: _} = naive_datetime, format) when format in [:basic, :extended] do
|
||||
@@ -672,8 +697,8 @@ defmodule NaiveDateTime do
|
||||
iex> NaiveDateTime.to_erl(~N[2000-01-01 13:30:15])
|
||||
{{2000, 1, 1}, {13, 30, 15}}
|
||||
|
||||
This function can also be used to convert a DateTime to a erl format
|
||||
without the time zone information:
|
||||
This function can also be used to convert a DateTime to an Erlang
|
||||
datetime tuple without the time zone information:
|
||||
|
||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||
@@ -936,7 +961,7 @@ defmodule NaiveDateTime do
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(%{calendar: Calendar.ISO} = naive_datetime, _) do
|
||||
def inspect(naive_datetime, _) do
|
||||
%{
|
||||
year: year,
|
||||
month: month,
|
||||
@@ -944,17 +969,17 @@ defmodule NaiveDateTime do
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
microsecond: microsecond,
|
||||
calendar: calendar
|
||||
} = naive_datetime
|
||||
|
||||
formatted =
|
||||
Calendar.ISO.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
|
||||
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
|
||||
|
||||
"~N[" <> formatted <> "]"
|
||||
"~N[" <> formatted <> suffix(calendar) <> "]"
|
||||
end
|
||||
|
||||
def inspect(naive, opts) do
|
||||
Inspect.Any.inspect(naive, opts)
|
||||
end
|
||||
defp suffix(Calendar.ISO), do: ""
|
||||
defp suffix(calendar), do: " " <> inspect(calendar)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -110,7 +110,7 @@ defmodule Time do
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | integer,
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: {:ok, t} | {:error, atom}
|
||||
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
@@ -213,30 +213,12 @@ defmodule Time do
|
||||
|
||||
"""
|
||||
@spec from_iso8601(String.t(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO)
|
||||
|
||||
def from_iso8601(<<?T, rest::binary>>, calendar) do
|
||||
raw_from_iso8601(rest, calendar)
|
||||
end
|
||||
|
||||
def from_iso8601(<<rest::binary>>, calendar) do
|
||||
raw_from_iso8601(rest, calendar)
|
||||
end
|
||||
|
||||
[match_time, guard_time, read_time] = Calendar.ISO.__match_time__()
|
||||
|
||||
defp raw_from_iso8601(string, calendar) do
|
||||
with <<unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_time),
|
||||
{microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
|
||||
{_offset, ""} <- Calendar.ISO.parse_offset(rest) do
|
||||
{hour, min, sec} = unquote(read_time)
|
||||
|
||||
with {:ok, utc_time} <- new(hour, min, sec, microsec, Calendar.ISO) do
|
||||
convert(utc_time, calendar)
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_format}
|
||||
def from_iso8601(string, calendar \\ Calendar.ISO) do
|
||||
with {:ok, {hour, minute, second, microsecond}} <- Calendar.ISO.parse_time(string) do
|
||||
convert(
|
||||
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond},
|
||||
calendar
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -693,20 +675,20 @@ defmodule Time do
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(%{calendar: Calendar.ISO} = time, _) do
|
||||
def inspect(time, _) do
|
||||
%{
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
calendar: Calendar.ISO
|
||||
calendar: calendar
|
||||
} = time
|
||||
|
||||
"~T[" <> Calendar.ISO.time_to_string(hour, minute, second, microsecond) <> "]"
|
||||
"~T[" <>
|
||||
calendar.time_to_string(hour, minute, second, microsecond) <> suffix(calendar) <> "]"
|
||||
end
|
||||
|
||||
def inspect(time, opts) do
|
||||
Inspect.Any.inspect(time, opts)
|
||||
end
|
||||
defp suffix(Calendar.ISO), do: ""
|
||||
defp suffix(calendar), do: " " <> inspect(calendar)
|
||||
end
|
||||
end
|
||||
|
||||
+308
-106
@@ -1,5 +1,5 @@
|
||||
defmodule Code do
|
||||
@moduledoc """
|
||||
@moduledoc ~S"""
|
||||
Utilities for managing code compilation, code evaluation, and code loading.
|
||||
|
||||
This module complements Erlang's [`:code` module](http://www.erlang.org/doc/man/code.html)
|
||||
@@ -19,7 +19,8 @@ defmodule Code do
|
||||
|
||||
* `eval_file/2` - evaluates the file contents without tracking its name. It
|
||||
returns the result of the last expression in the file, instead of the modules
|
||||
defined in it.
|
||||
defined in it. Evaluated files do not trigger the compilation tracers described
|
||||
in the next section.
|
||||
|
||||
In a nutshell, the first must be used when you want to keep track of the files
|
||||
handled by the system, to avoid the same file from being compiled multiple
|
||||
@@ -28,9 +29,91 @@ defmodule Code do
|
||||
`compile_file/2` must be used when you are interested in the modules defined in a
|
||||
file, without tracking. `eval_file/2` should be used when you are interested in
|
||||
the result of evaluating the file rather than the modules it defines.
|
||||
|
||||
## Compilation tracers
|
||||
|
||||
Elixir supports compilation tracers, which allows modules to observe constructs
|
||||
handled by the Elixir compiler when compiling files. A tracer is a module
|
||||
that implements the `trace/2` function. The function receives the event name
|
||||
as first argument and `Macro.Env` as second and it must return `:ok`. It is
|
||||
very important for a tracer to do as little work as possible synchronously
|
||||
and dispatch the bulk of the work to a separate process. **Slow tracers will
|
||||
slow down compilation**.
|
||||
|
||||
You can configure your list of tracers via `put_compiler_option/2`. The
|
||||
following events are available to tracers:
|
||||
|
||||
* `{:import, meta, module, opts}` - traced whenever `module` is imported.
|
||||
`meta` is the import AST metadata and `opts` are the import options.
|
||||
|
||||
* `{:imported_function, meta, module, name, arity}` and
|
||||
`{:imported_macro, meta, module, name, arity}` - traced whenever an
|
||||
imported function or macro is invoked. `meta` is the call AST metadata,
|
||||
`module` is the module the import is from, followed by the `name` and `arity`
|
||||
of the imported function/macro.
|
||||
|
||||
* `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
|
||||
to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
|
||||
|
||||
* `{:alias_expansion, meta, as, alias}` traced whenever there is an alias
|
||||
expansion for a previously defined `alias`, i.e. when the user writes `as`
|
||||
which is expanded to `alias`. `meta` is the alias expansion AST metadata.
|
||||
|
||||
* `{:alias_reference, meta, module}` - traced whenever there is an alias
|
||||
in the code, i.e. whenever the user writes `MyModule.Foo.Bar` in the code,
|
||||
regardless if it was expanded or not.
|
||||
|
||||
* `{:require, meta, module, opts}` - traced whenever `module` is required.
|
||||
`meta` is the require AST metadata and `opts` are the require options.
|
||||
|
||||
* `{:struct_expansion, meta, module, keys}` - traced whenever `module`'s struct
|
||||
is expanded. `meta` is the struct AST metadata and `keys` are the keys being
|
||||
used by expansion
|
||||
|
||||
* `{:remote_function, meta, module, name, arity}` and
|
||||
`{:remote_macro, meta, module, name, arity}` - traced whenever a remote
|
||||
function or macro is referenced. `meta` is the call AST metadata, `module`
|
||||
is the invoked module, followed by the `name` and `arity`.
|
||||
|
||||
* `{:local_function, meta, name, arity}` and
|
||||
`{:local_macro, meta, name, arity}` - traced whenever a local
|
||||
function or macro is referenced. `meta` is the call AST metadata, `module`
|
||||
is the invoked module, followed by the `name` and `arity`.
|
||||
|
||||
* `{:compile_env, app, path, return}` - traced whenever `Application.compile_env/3`
|
||||
or `Application.compile_env!/2` are called. `app` is an atom, `path` is a list
|
||||
of keys to traverse in the application environemnt and `return` is either
|
||||
`{:ok, value}` or `:error`.
|
||||
|
||||
The `:tracers` compiler option can be combined with the `:parser_options`
|
||||
compiler option to enrich the metadata of the traced events above.
|
||||
|
||||
New events may be added at any time in the future, therefore it is advised
|
||||
for the `trace/2` function to have a "catch-all" clause.
|
||||
|
||||
Below is an example tracer that prints all remote function invocations:
|
||||
|
||||
defmodule MyTracer do
|
||||
def trace({:remote_function, _meta, module, name, arity}, env) do
|
||||
IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
|
||||
:ok
|
||||
end
|
||||
|
||||
def trace(_event, _env) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
@available_compiler_options [
|
||||
@typedoc """
|
||||
A list with all variable bindings.
|
||||
|
||||
The binding keys are usually atoms, but they may be a tuple for variables
|
||||
defined in a different context.
|
||||
"""
|
||||
@type binding :: [{atom() | tuple(), any}]
|
||||
|
||||
@boolean_compiler_options [
|
||||
:docs,
|
||||
:debug_info,
|
||||
:ignore_module_conflict,
|
||||
@@ -38,6 +121,10 @@ defmodule Code do
|
||||
:warnings_as_errors
|
||||
]
|
||||
|
||||
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
|
||||
|
||||
@available_compiler_options @boolean_compiler_options ++ @list_compiler_options
|
||||
|
||||
@doc """
|
||||
Lists all required files.
|
||||
|
||||
@@ -54,7 +141,7 @@ defmodule Code do
|
||||
:elixir_code_server.call(:required)
|
||||
end
|
||||
|
||||
# TODO: Deprecate on v1.9
|
||||
@deprecated "Use Code.required_files/0 instead"
|
||||
@doc false
|
||||
def loaded_files do
|
||||
required_files()
|
||||
@@ -86,7 +173,7 @@ defmodule Code do
|
||||
:elixir_code_server.cast({:unrequire_files, files})
|
||||
end
|
||||
|
||||
# TODO: Deprecate on v1.9
|
||||
@deprecated "Use Code.unrequire_files/1 instead"
|
||||
@doc false
|
||||
def unload_files(files) do
|
||||
unrequire_files(files)
|
||||
@@ -163,7 +250,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Evaluates the contents given by `string`.
|
||||
|
||||
The `binding` argument is a keyword list of variable bindings.
|
||||
The `binding` argument is a list of variable bindings.
|
||||
The `opts` argument is a keyword list of environment options.
|
||||
|
||||
**Warning**: `string` can be any Elixir code and will be executed with
|
||||
@@ -204,8 +291,8 @@ defmodule Code do
|
||||
where `value` is the value returned from evaluating `string`.
|
||||
If an error occurs while evaluating `string` an exception will be raised.
|
||||
|
||||
`binding` is a keyword list with the value of all variable bindings
|
||||
after evaluating `string`. The binding key is usually an atom, but it
|
||||
`binding` is a list with all variable bindings
|
||||
after evaluating `string`. The binding keys are usually atoms, but they
|
||||
may be a tuple for variables defined in a different context.
|
||||
|
||||
## Examples
|
||||
@@ -227,17 +314,22 @@ defmodule Code do
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
@spec eval_string(List.Chars.t(), list, Macro.Env.t() | keyword) :: {term, binding :: list}
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
|
||||
def eval_string(string, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_string(string, binding, %Macro.Env{} = env) do
|
||||
{value, binding, _env, _scope} = :elixir.eval(to_charlist(string), binding, Map.to_list(env))
|
||||
{value, binding}
|
||||
eval_string_with_error_handling(string, binding, Map.to_list(env))
|
||||
end
|
||||
|
||||
def eval_string(string, binding, opts) when is_list(opts) do
|
||||
validate_eval_opts(opts)
|
||||
{value, binding, _env, _scope} = :elixir.eval(to_charlist(string), binding, opts)
|
||||
eval_string_with_error_handling(string, binding, opts)
|
||||
end
|
||||
|
||||
defp eval_string_with_error_handling(string, binding, opts) do
|
||||
%{line: line, file: file} = env = :elixir.env_for_eval(opts)
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, file, [])
|
||||
{value, binding, _env} = :elixir.eval_forms(forms, binding, env)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -294,7 +386,7 @@ defmodule Code do
|
||||
a whole.
|
||||
|
||||
The formatter does not hard code names. The formatter will not behave
|
||||
specially because a function is named `defmodule`, `def`, etc. This
|
||||
specially because a function is named `defmodule`, `def`, or the like. This
|
||||
principle mirrors Elixir's goal of being an extensible language where
|
||||
developers can extend the language with new constructs as if they were
|
||||
part of the language. When it is absolutely necessary to change behaviour
|
||||
@@ -431,7 +523,7 @@ defmodule Code do
|
||||
rules in the future. The goal of documenting them is to provide better
|
||||
understanding on what to expect from the formatter.
|
||||
|
||||
### Multi-line lists, maps, tuples, etc.
|
||||
### Multi-line lists, maps, tuples, and the like
|
||||
|
||||
You can force lists, tuples, bitstrings, maps, structs and function
|
||||
calls to have one entry per line by adding a newline after the opening
|
||||
@@ -576,7 +668,7 @@ defmodule Code do
|
||||
Macro arguments are typically transformed by unquoting them into the
|
||||
returned quoted expressions (instead of evaluated).
|
||||
|
||||
See `eval_string/3` for a description of bindings and options.
|
||||
See `eval_string/3` for a description of `binding` and options.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -592,17 +684,17 @@ defmodule Code do
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), list, Macro.Env.t() | keyword) :: {term, binding :: list}
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
|
||||
{value, binding, _env, _scope} = :elixir.eval_quoted(quoted, binding, Map.to_list(env))
|
||||
{value, binding, _env} = :elixir.eval_quoted(quoted, binding, Map.to_list(env))
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
||||
validate_eval_opts(opts)
|
||||
{value, binding, _env, _scope} = :elixir.eval_quoted(quoted, binding, opts)
|
||||
{value, binding, _env} = :elixir.eval_quoted(quoted, binding, opts)
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -644,7 +736,7 @@ defmodule Code do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@doc ~S"""
|
||||
Converts the given string to its quoted form.
|
||||
|
||||
Returns `{:ok, quoted_form}` if it succeeds,
|
||||
@@ -665,11 +757,23 @@ defmodule Code do
|
||||
when non-existing atoms are found by the tokenizer.
|
||||
Defaults to `false`.
|
||||
|
||||
* `:static_atom_encoder` - The static atom encoder function, see
|
||||
"The `:static_atom_encoder` function" section below. This option
|
||||
overrides the `:existing_atoms_only` behaviour for static atoms
|
||||
but `:existing_atoms_only` is still used for dynamic atoms, such
|
||||
as atoms with interpolations.
|
||||
* `:token_metadata` (since v1.10.0) - when `true`, includes token-related
|
||||
metadata in the expression AST, such as metadata for `do` and `end`
|
||||
tokens, for closing tokens, end of expressions, as well as delimiters
|
||||
for sigils. See `t:Macro.metadata/0`. Defaults to `false`.
|
||||
|
||||
* `: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
|
||||
itself as the `term`, then the AST is no longer valid. This option
|
||||
may still 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
|
||||
option overrides the `:existing_atoms_only` behaviour for static
|
||||
atoms but `:existing_atoms_only` is still used for dynamic atoms,
|
||||
such as atoms with interpolations.
|
||||
|
||||
* `:warn_on_unnecessary_quotes` - when `false`, does not warn
|
||||
when atoms, keywords or calls have unnecessary quotes on
|
||||
@@ -681,9 +785,9 @@ defmodule Code do
|
||||
`Macro.to_string/2`, which converts a quoted form to a string/binary
|
||||
representation.
|
||||
|
||||
## The `:static_atom_encoder` function
|
||||
## The `:static_atoms_encoder` function
|
||||
|
||||
When `static_atom_encoder: &my_encoder/2` is passed as an argument,
|
||||
When `static_atoms_encoder: &my_encoder/2` is passed as an argument,
|
||||
`my_encoder/2` is called every time the tokenizer needs to create a
|
||||
"static" atom. Static atoms are atoms in the AST that function as
|
||||
aliases, remote calls, local calls, variable names, regular atoms
|
||||
@@ -694,8 +798,8 @@ defmodule Code do
|
||||
`{:ok, token :: term} | {:error, reason :: binary}`.
|
||||
|
||||
The encoder function is supposed to create an atom from the given
|
||||
string. It is required to return either `{:ok, term}`, where term is
|
||||
an atom. It is possible to return something else than an atom,
|
||||
string. To produce a valid AST, it is required to return `{:ok, term}`,
|
||||
where `term` is an atom. It is possible to return something other than an atom,
|
||||
however, in that case the AST is no longer "valid" in that it cannot
|
||||
be used to compile or evaluate Elixir code. A use case for this is
|
||||
if you want to use the Elixir parser in a user-facing situation, but
|
||||
@@ -708,7 +812,7 @@ defmodule Code do
|
||||
|
||||
* syntax keywords (`fn`, `do`, `else`, and so on)
|
||||
|
||||
* atoms containing interpolation (`:"\#{1 + 1} is two"`), as these
|
||||
* atoms containing interpolation (`:"#{1 + 1} is two"`), as these
|
||||
atoms are constructed at runtime.
|
||||
|
||||
"""
|
||||
@@ -751,22 +855,22 @@ defmodule Code do
|
||||
|
||||
While `require_file/2` and `compile_file/2` return the loaded modules and their
|
||||
bytecode, `eval_file/2` simply evaluates the file contents and returns the
|
||||
evaluation result and its bindings (exactly the same return value as `eval_string/3`).
|
||||
evaluation result and its binding (exactly the same return value as `eval_string/3`).
|
||||
"""
|
||||
@spec eval_file(binary, nil | binary) :: {term, binding :: list}
|
||||
@spec eval_file(binary, nil | binary) :: {term, binding}
|
||||
def eval_file(file, relative_to \\ nil) when is_binary(file) do
|
||||
file = find_file(file, relative_to)
|
||||
eval_string(File.read!(file), [], file: file, line: 1)
|
||||
end
|
||||
|
||||
# TODO: Deprecate on v1.9
|
||||
@deprecated "Use Code.require_file/2 or Code.compile_file/2 instead"
|
||||
@doc false
|
||||
def load_file(file, relative_to \\ nil) when is_binary(file) do
|
||||
file = find_file(file, relative_to)
|
||||
:elixir_code_server.call({:acquire, file})
|
||||
loaded = :elixir_compiler.file(file, fn _, _ -> :ok end)
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
verify_loaded(loaded)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -812,14 +916,15 @@ defmodule Code do
|
||||
:proceed ->
|
||||
loaded = :elixir_compiler.file(file, fn _, _ -> :ok end)
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
verify_loaded(loaded)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the compilation options from the code server.
|
||||
Gets all compilation options from the code server.
|
||||
|
||||
Check `compiler_options/1` for more information.
|
||||
To get invidual options, see `get_compiler_option/1`.
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -827,15 +932,54 @@ defmodule Code do
|
||||
#=> %{debug_info: true, docs: true, ...}
|
||||
|
||||
"""
|
||||
@spec compiler_options() :: %{optional(atom) => boolean}
|
||||
@spec compiler_options :: map
|
||||
def compiler_options do
|
||||
:elixir_config.get(:compiler_options)
|
||||
for key <- @available_compiler_options, into: %{} do
|
||||
{key, :elixir_config.get(key)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with the available compiler options.
|
||||
Stores all given compilation options.
|
||||
|
||||
See `compiler_options/1` for more information.
|
||||
To store invidual options, see `put_compiler_option/2`.
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
Code.compiler_options()
|
||||
#=> %{debug_info: true, docs: true, ...}
|
||||
|
||||
"""
|
||||
@spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
|
||||
def compiler_options(opts) do
|
||||
for {key, value} <- opts, into: %{} do
|
||||
put_compiler_option(key, value)
|
||||
{key, value}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the value of a given compiler option.
|
||||
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
Code.get_compiler_option(:debug_info)
|
||||
#=> true
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec get_compiler_option(atom) :: term
|
||||
def get_compiler_option(key) when key in @available_compiler_options do
|
||||
:elixir_config.get(key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with all available compiler options.
|
||||
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -848,11 +992,101 @@ defmodule Code do
|
||||
@available_compiler_options
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores a compilation option.
|
||||
|
||||
These options are global since they are stored by Elixir's code server.
|
||||
|
||||
Available options are:
|
||||
|
||||
* `:docs` - when `true`, retain documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:debug_info` - when `true`, retain debug information in the compiled
|
||||
module. This allows a developer to reconstruct the original source
|
||||
code. Defaults to `true`.
|
||||
|
||||
* `:ignore_module_conflict` - when `true`, override modules that were
|
||||
already defined without raising errors. Defaults to `false`.
|
||||
|
||||
* `:relative_paths` - when `true`, use 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`.
|
||||
|
||||
* `:warnings_as_errors` - causes compilation to fail when warnings are
|
||||
generated. Defaults to `false`.
|
||||
|
||||
* `: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 `[]`.
|
||||
|
||||
* `: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
|
||||
`string_to_quoted/2` (except by the options that change the AST itself).
|
||||
This can be used in combination with the tracer to retrieve localized
|
||||
information about events happening during compilation. Defaults to `[]`.
|
||||
|
||||
It always returns `:ok`. Raises an error for invalid options.
|
||||
|
||||
## Examples
|
||||
|
||||
Code.put_compiler_option(:debug_info, true)
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec put_compiler_option(atom, term) :: :ok
|
||||
def put_compiler_option(key, value) when key in @boolean_compiler_options do
|
||||
if not is_boolean(value) do
|
||||
raise "compiler option #{inspect(key)} should be a boolean, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
:elixir_config.put(key, value)
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(:no_warn_undefined, value) do
|
||||
if value != :all and not is_list(value) do
|
||||
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
|
||||
"got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
:elixir_config.put(:no_warn_undefined, value)
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(key, value) when key in @list_compiler_options do
|
||||
if not is_list(value) do
|
||||
raise "compiler option #{inspect(key)} should be a list, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
if key == :parser_options and not Keyword.keyword?(value) do
|
||||
raise "compiler option #{inspect(key)} should be a keyword list, " <>
|
||||
"got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
if key == :tracers and not Enum.all?(value, &is_atom/1) do
|
||||
raise "compiler option #{inspect(key)} should be a list of modules, " <>
|
||||
"got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
:elixir_config.put(key, value)
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(key, _value) do
|
||||
raise "unknown compiler option: #{inspect(key)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Purge compiler modules.
|
||||
|
||||
The compiler utilizes temporary modules to compile code. For example,
|
||||
`elixir_compiler_1`, `elixir_compiler_2`, etc. In case the compiled code
|
||||
`elixir_compiler_1`, `elixir_compiler_2`, and so on. In case the compiled code
|
||||
stores references to anonymous functions or similar, the Elixir compiler
|
||||
may be unable to reclaim those modules, keeping an unnecessary amount of
|
||||
code in memory and eventually leading to modules such as `elixir_compiler_12345`.
|
||||
@@ -869,54 +1103,6 @@ defmodule Code do
|
||||
:elixir_code_server.call(:purge_compiler_modules)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets compilation options.
|
||||
|
||||
These options are global since they are stored by Elixir's Code Server.
|
||||
|
||||
Available options are:
|
||||
|
||||
* `:docs` - when `true`, retain documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:debug_info` - when `true`, retain debug information in the compiled
|
||||
module. This allows a developer to reconstruct the original source
|
||||
code. Defaults to `false`.
|
||||
|
||||
* `:ignore_module_conflict` - when `true`, override modules that were
|
||||
already defined without raising errors. Defaults to `false`.
|
||||
|
||||
* `:relative_paths` - when `true`, use 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`.
|
||||
|
||||
* `:warnings_as_errors` - causes compilation to fail when warnings are
|
||||
generated. Defaults to `false`.
|
||||
|
||||
It returns the new map of compiler options.
|
||||
|
||||
## Examples
|
||||
|
||||
Code.compiler_options(debug_info: true)
|
||||
#=> %{debug_info: true, docs: true,
|
||||
#=> warnings_as_errors: false, ignore_module_conflict: false}
|
||||
|
||||
"""
|
||||
@spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
|
||||
def compiler_options(opts) do
|
||||
Enum.each(opts, fn
|
||||
{key, value} when key in @available_compiler_options ->
|
||||
if not is_boolean(value) do
|
||||
raise "compiler option #{inspect(key)} should be a boolean, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
{key, _} ->
|
||||
raise "unknown compiler option: #{inspect(key)}"
|
||||
end)
|
||||
|
||||
:elixir_config.update(:compiler_options, &Enum.into(opts, &1))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given string.
|
||||
|
||||
@@ -933,7 +1119,8 @@ defmodule Code do
|
||||
"""
|
||||
@spec compile_string(List.Chars.t(), binary) :: [{module, binary}]
|
||||
def compile_string(string, file \\ "nofile") when is_binary(file) do
|
||||
:elixir_compiler.string(to_charlist(string), file, fn _, _ -> :ok end)
|
||||
loaded = :elixir_compiler.string(to_charlist(string), file, fn _, _ -> :ok end)
|
||||
Enum.map(loaded, fn {module, _map, binary} -> {module, binary} end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -946,7 +1133,8 @@ defmodule Code do
|
||||
"""
|
||||
@spec compile_quoted(Macro.t(), binary) :: [{module, binary}]
|
||||
def compile_quoted(quoted, file \\ "nofile") when is_binary(file) do
|
||||
:elixir_compiler.quoted(quoted, file, fn _, _ -> :ok end)
|
||||
loaded = :elixir_compiler.quoted(quoted, file, fn _, _ -> :ok end)
|
||||
Enum.map(loaded, fn {module, _map, binary} -> {module, binary} end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -963,9 +1151,11 @@ defmodule Code do
|
||||
|
||||
For compiling many files concurrently, see `Kernel.ParallelCompiler.compile/2`.
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec compile_file(binary, nil | binary) :: [{module, binary}]
|
||||
def compile_file(file, relative_to \\ nil) when is_binary(file) do
|
||||
:elixir_compiler.file(find_file(file, relative_to), fn _, _ -> :ok end)
|
||||
loaded = :elixir_compiler.file(find_file(file, relative_to), fn _, _ -> :ok end)
|
||||
verify_loaded(loaded)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1048,27 +1238,39 @@ defmodule Code do
|
||||
Ensures the given module is compiled and loaded.
|
||||
|
||||
If the module is already loaded, it works as no-op. If the module was
|
||||
not loaded yet, it checks if it needs to be compiled first and then
|
||||
tries to load it.
|
||||
not compiled yet, `ensure_compiled/1` halts the compilation of the caller
|
||||
until the module given to `ensure_compiled/1` becomes available or
|
||||
all files for the current project have been compiled. If compilation
|
||||
finishes and the module is not available, an error tuple is returned.
|
||||
|
||||
Given this function halts compilation, use it carefully. In particular,
|
||||
avoid using it to guess which modules are in the system. Overuse of this
|
||||
function can also lead to deadlocks, where two modules check at the same time
|
||||
if the other is compiled. This returns a specific unavailable error code,
|
||||
where we cannot successfully verify a module is available or not.
|
||||
|
||||
If it succeeds in loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
|
||||
If the module being checked is currently in a compiler deadlock,
|
||||
this functions returns `{:error, :nofile}`.
|
||||
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.
|
||||
|
||||
Check `ensure_loaded/1` for more information on module loading
|
||||
and when to use `ensure_loaded/1` or `ensure_compiled/1`.
|
||||
"""
|
||||
@spec ensure_compiled(module) ::
|
||||
{:module, module} | {:error, :embedded | :badfile | :nofile | :on_load_failure}
|
||||
{:module, module}
|
||||
| {:error, :embedded | :badfile | :nofile | :on_load_failure | :unavailable}
|
||||
def ensure_compiled(module) when is_atom(module) do
|
||||
case :code.ensure_loaded(module) do
|
||||
{:error, :nofile} = error ->
|
||||
if is_pid(:erlang.get(:elixir_compiler_pid)) do
|
||||
case Kernel.ErrorHandler.ensure_compiled(module, :module, :soft) do
|
||||
:found -> {:module, module}
|
||||
:not_found -> error
|
||||
:deadlock -> {:error, :unavailable}
|
||||
:not_found -> {:error, :nofile}
|
||||
end
|
||||
else
|
||||
error
|
||||
@@ -1079,14 +1281,8 @@ defmodule Code do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is compiled and loaded.
|
||||
|
||||
Similar to `ensure_compiled/1`, but returns `true` if the module
|
||||
is already loaded or was successfully loaded and compiled.
|
||||
Returns `false` otherwise.
|
||||
"""
|
||||
@spec ensure_compiled?(module) :: boolean
|
||||
@doc false
|
||||
@deprecated "Use Code.ensure_compiled/1 instead (see the proper disclaimers in its docs)"
|
||||
def ensure_compiled?(module) when is_atom(module) do
|
||||
match?({:module, ^module}, ensure_compiled(module))
|
||||
end
|
||||
@@ -1108,7 +1304,7 @@ defmodule Code do
|
||||
# Module documentation of an existing module
|
||||
iex> {:docs_v1, _, :elixir, _, %{"en" => module_doc}, _, _} = Code.fetch_docs(Atom)
|
||||
iex> module_doc |> String.split("\n") |> Enum.at(0)
|
||||
"Convenience functions for working with atoms."
|
||||
"Atoms are constants whose values are their own name."
|
||||
|
||||
# A module that doesn't exist
|
||||
iex> Code.fetch_docs(ModuleNotGood)
|
||||
@@ -1192,4 +1388,10 @@ defmodule Code do
|
||||
raise Code.LoadError, file: file
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_loaded(loaded) do
|
||||
maps_binaries = Enum.map(loaded, fn {_module, map, binary} -> {map, binary} end)
|
||||
Module.ParallelChecker.verify(maps_binaries, [])
|
||||
Enum.map(loaded, fn {module, map, _binary} -> {module, map} end)
|
||||
end
|
||||
end
|
||||
|
||||
+190
-165
@@ -205,8 +205,13 @@ defmodule Code.Formatter do
|
||||
warn_on_unnecessary_quotes: false
|
||||
]
|
||||
|
||||
parser_options = [
|
||||
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
|
||||
token_metadata: true
|
||||
]
|
||||
|
||||
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, file, tokenizer_options),
|
||||
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, formatter_metadata: true) do
|
||||
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, parser_options) do
|
||||
state =
|
||||
Process.get(:code_formatter_comments)
|
||||
|> Enum.reverse()
|
||||
@@ -348,7 +353,7 @@ defmodule Code.Formatter do
|
||||
not interpolated?(entries) ->
|
||||
bitstring_to_algebra(meta, entries, state)
|
||||
|
||||
meta[:format] == :bin_heredoc ->
|
||||
meta[:delimiter] == ~s["""] ->
|
||||
{doc, state} =
|
||||
entries
|
||||
|> prepend_heredoc_line()
|
||||
@@ -370,7 +375,7 @@ defmodule Code.Formatter do
|
||||
not list_interpolated?(entries) ->
|
||||
remote_to_algebra(quoted, context, state)
|
||||
|
||||
meta[:format] == :list_heredoc ->
|
||||
meta[:delimiter] == ~s['''] ->
|
||||
{doc, state} =
|
||||
entries
|
||||
|> prepend_heredoc_line()
|
||||
@@ -428,12 +433,12 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
|
||||
case meta[:format] do
|
||||
:list_heredoc ->
|
||||
case meta[:delimiter] do
|
||||
~s['''] ->
|
||||
string = list |> List.to_string() |> escape_heredoc()
|
||||
{@single_heredoc |> concat(string) |> concat(@single_heredoc) |> force_unfit(), state}
|
||||
|
||||
:charlist ->
|
||||
~s['] ->
|
||||
string = list |> List.to_string() |> escape_string(@single_quote)
|
||||
{@single_quote |> concat(string) |> concat(@single_quote), state}
|
||||
|
||||
@@ -443,7 +448,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, [string]}, _context, state) when is_binary(string) do
|
||||
if meta[:format] == :bin_heredoc do
|
||||
if meta[:delimiter] == ~s["""] do
|
||||
string = escape_heredoc(string)
|
||||
{@double_heredoc |> concat(string) |> concat(@double_heredoc) |> force_unfit(), state}
|
||||
else
|
||||
@@ -458,11 +463,11 @@ defmodule Code.Formatter do
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, [integer]}, _context, state)
|
||||
when is_integer(integer) do
|
||||
{integer_to_algebra(Keyword.fetch!(meta, :original)), state}
|
||||
{integer_to_algebra(Keyword.fetch!(meta, :token)), state}
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, [float]}, _context, state) when is_float(float) do
|
||||
{float_to_algebra(Keyword.fetch!(meta, :original)), state}
|
||||
{float_to_algebra(Keyword.fetch!(meta, :token)), state}
|
||||
end
|
||||
|
||||
defp quoted_to_algebra(
|
||||
@@ -479,7 +484,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, _} = block, _context, state) do
|
||||
{block, state} = block_to_algebra(block, line(meta), end_line(meta), state)
|
||||
{block, state} = block_to_algebra(block, line(meta), closing_line(meta), state)
|
||||
{surround("(", block, ")"), state}
|
||||
end
|
||||
|
||||
@@ -513,7 +518,7 @@ defmodule Code.Formatter do
|
||||
defp quoted_to_algebra({:not, meta, [{:in, _, [left, right]} = arg]}, context, state) do
|
||||
%{rename_deprecated_at: since} = state
|
||||
|
||||
# TODO: Remove since check on Elixir v2.0 and the OP arrangement is removed.
|
||||
# TODO: Remove metadata and always rewrite to left not in right in Elixir v2.0.
|
||||
if meta[:operator] == :"not in" || (since && Version.match?(since, "~> 1.5")) do
|
||||
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
|
||||
else
|
||||
@@ -522,7 +527,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:fn, meta, [_ | _] = clauses}, _context, state) do
|
||||
anon_fun_to_algebra(clauses, line(meta), end_line(meta), state, eol?(meta))
|
||||
anon_fun_to_algebra(clauses, line(meta), closing_line(meta), state, eol?(meta))
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({fun, meta, args}, context, state) when is_atom(fun) and is_list(args) do
|
||||
@@ -606,14 +611,14 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp block_args_to_algebra(args, min_line, max_line, state) do
|
||||
quoted_to_algebra = fn {kind, meta, _} = arg, _args, doc_newlines, state ->
|
||||
doc_newlines = Keyword.get(meta, :newlines, doc_newlines)
|
||||
quoted_to_algebra = fn {kind, meta, _} = arg, _args, state ->
|
||||
newlines = meta[:end_of_expression][:newlines] || 1
|
||||
{doc, state} = quoted_to_algebra(arg, :block, state)
|
||||
{doc, block_next_line(kind), doc_newlines, state}
|
||||
{{doc, block_next_line(kind), newlines}, state}
|
||||
end
|
||||
|
||||
{args_docs, _comments?, state} =
|
||||
quoted_to_algebra_with_comments(args, [], min_line, max_line, 2, state, quoted_to_algebra)
|
||||
quoted_to_algebra_with_comments(args, [], min_line, max_line, state, quoted_to_algebra)
|
||||
|
||||
case args_docs do
|
||||
[] -> {@empty, state}
|
||||
@@ -668,11 +673,11 @@ defmodule Code.Formatter do
|
||||
|
||||
# There are five kinds of operators.
|
||||
#
|
||||
# 1. no space binary operators, e.g. 1..2
|
||||
# 2. no newline binary operators, e.g. left in right
|
||||
# 3. strict newlines before a left precedent operator, e.g. foo |> bar |> baz
|
||||
# 4. strict newlines before a right precedent operator, e.g. foo when bar when baz
|
||||
# 5. flex newlines after the operator, e.g. foo ++ bar ++ baz
|
||||
# 1. no space binary operators, for example, 1..2
|
||||
# 2. no newline binary operators, for example, left in right
|
||||
# 3. strict newlines before a left precedent operator, for example, foo |> bar |> baz
|
||||
# 4. strict newlines before a right precedent operator, for example, foo when bar when baz
|
||||
# 5. flex newlines after the operator, for example, foo ++ bar ++ baz
|
||||
#
|
||||
# Cases 1, 2 and 5 are handled fairly easily by relying on the
|
||||
# operator precedence and making sure nesting is applied only once.
|
||||
@@ -701,14 +706,14 @@ defmodule Code.Formatter do
|
||||
unwrap_right(right_arg, op, meta, right_context, [{{:root, left_context}, left_arg}])
|
||||
|
||||
operand_to_algebra = fn
|
||||
{{:root, context}, arg}, _args, newlines, state ->
|
||||
{{:root, context}, arg}, _args, state ->
|
||||
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2)
|
||||
{doc, @empty, newlines, state}
|
||||
{{doc, @empty, 1}, state}
|
||||
|
||||
{{kind, context}, arg}, _args, newlines, state ->
|
||||
{{kind, context}, arg}, _args, state ->
|
||||
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, kind, 0)
|
||||
doc = doc |> nest_by_length(op_string) |> force_keyword(arg)
|
||||
{concat(op_string, doc), @empty, newlines, state}
|
||||
{{concat(op_string, doc), @empty, 1}, state}
|
||||
end
|
||||
|
||||
{doc, state} =
|
||||
@@ -739,15 +744,15 @@ defmodule Code.Formatter do
|
||||
unwrap_pipes(left_arg, meta, left_context, [{{op, right_context}, right_arg}])
|
||||
|
||||
operand_to_algebra = fn
|
||||
{{:root, context}, arg}, _args, newlines, state ->
|
||||
{{:root, context}, arg}, _args, state ->
|
||||
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2)
|
||||
{doc, @empty, newlines, state}
|
||||
{{doc, @empty, 1}, state}
|
||||
|
||||
{{op, context}, arg}, _args, newlines, state ->
|
||||
{{op, context}, arg}, _args, state ->
|
||||
op_info = Code.Identifier.binary_op(op)
|
||||
op_string = Atom.to_string(op) <> " "
|
||||
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0)
|
||||
{concat(op_string, doc), @empty, newlines, state}
|
||||
{{concat(op_string, doc), @empty, 1}, state}
|
||||
end
|
||||
|
||||
operand_to_algebra_with_comments(pipes, meta, min_line, max_line, state, operand_to_algebra)
|
||||
@@ -880,7 +885,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp operand_to_algebra_with_comments(operands, meta, min_line, max_line, state, fun) do
|
||||
{docs, comments?, state} =
|
||||
quoted_to_algebra_with_comments(operands, [], min_line, max_line, 1, state, fun)
|
||||
quoted_to_algebra_with_comments(operands, [], min_line, max_line, state, fun)
|
||||
|
||||
if comments? or eol?(meta) do
|
||||
{docs |> Enum.reduce(&line(&2, &1)) |> force_unfit(), state}
|
||||
@@ -900,11 +905,11 @@ defmodule Code.Formatter do
|
||||
|
||||
# @foo bar
|
||||
# @foo(bar)
|
||||
defp module_attribute_to_algebra(meta, {name, _, [_] = args} = expr, context, state)
|
||||
defp module_attribute_to_algebra(meta, {name, call_meta, [_] = args} = expr, context, state)
|
||||
when is_atom(name) and name not in [:__block__, :__aliases__] do
|
||||
if Code.Identifier.classify(name) == :callable_local do
|
||||
{{call_doc, state}, wrap_in_parens?} =
|
||||
call_args_to_algebra(args, meta, context, :skip_unless_many_args, false, state)
|
||||
call_args_to_algebra(args, call_meta, context, :skip_unless_many_args, false, state)
|
||||
|
||||
doc =
|
||||
"@#{name}"
|
||||
@@ -995,8 +1000,7 @@ defmodule Code.Formatter do
|
||||
fun = remote_fun_to_algebra(target, fun, length(args), state)
|
||||
remote_doc = target_doc |> concat(".") |> concat(string(fun))
|
||||
|
||||
if args == [] and not remote_target_is_a_module?(target) and
|
||||
Keyword.get(meta, :no_parens, false) do
|
||||
if args == [] and not remote_target_is_a_module?(target) and not meta?(meta, :closing) do
|
||||
{remote_doc, state}
|
||||
else
|
||||
{{call_doc, state}, wrap_in_parens?} =
|
||||
@@ -1075,7 +1079,7 @@ defmodule Code.Formatter do
|
||||
defp local_to_algebra(fun, meta, args, context, state) when is_atom(fun) do
|
||||
skip_parens =
|
||||
cond do
|
||||
not Keyword.get(meta, :no_parens, false) -> :required
|
||||
meta?(meta, :closing) -> :skip_if_only_do_end
|
||||
local_without_parens?(fun, args, state) -> :skip_unless_many_args
|
||||
true -> :skip_if_do_end
|
||||
end
|
||||
@@ -1093,9 +1097,10 @@ defmodule Code.Formatter do
|
||||
{doc, state}
|
||||
end
|
||||
|
||||
# parens may one of:
|
||||
# parens may be one of:
|
||||
#
|
||||
# * :skip_unless_many_args - skips parens unless we are the argument context
|
||||
# * :skip_if_only_do_end - skip parens if we are do-end and the only arg
|
||||
# * :skip_if_do_end - skip parens if we are do-end
|
||||
# * :required - never skip parens
|
||||
#
|
||||
@@ -1109,13 +1114,18 @@ defmodule Code.Formatter do
|
||||
defp call_args_to_algebra(args, meta, context, parens, list_to_keyword?, state) do
|
||||
{rest, last} = split_last(args)
|
||||
|
||||
if blocks = do_end_blocks(last, state) do
|
||||
if blocks = do_end_blocks(meta, last, state) do
|
||||
{call_doc, state} =
|
||||
if rest == [] do
|
||||
{" do", state}
|
||||
else
|
||||
no_parens? = parens != :required
|
||||
call_args_to_algebra_no_blocks(meta, rest, no_parens?, list_to_keyword?, " do", state)
|
||||
case rest do
|
||||
[] when parens == :required ->
|
||||
{"() do", state}
|
||||
|
||||
[] ->
|
||||
{" do", state}
|
||||
|
||||
_ ->
|
||||
no_parens? = parens not in [:required, :skip_if_only_do_end]
|
||||
call_args_to_algebra_no_blocks(meta, rest, no_parens?, list_to_keyword?, " do", state)
|
||||
end
|
||||
|
||||
{blocks_doc, state} = do_end_blocks_to_algebra(blocks, state)
|
||||
@@ -1135,7 +1145,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp call_args_to_algebra_no_blocks(meta, args, skip_parens?, list_to_keyword?, extra, state) do
|
||||
{left, right} = split_last(args)
|
||||
{keyword?, right} = last_arg_to_keyword(right, list_to_keyword?)
|
||||
{keyword?, right} = last_arg_to_keyword(right, list_to_keyword?, skip_parens?, state.comments)
|
||||
|
||||
context =
|
||||
if left == [] and not keyword? do
|
||||
@@ -1156,7 +1166,7 @@ defmodule Code.Formatter do
|
||||
{left_doc, _join, state} =
|
||||
args_to_algebra_with_comments(
|
||||
left,
|
||||
Keyword.delete(meta, :end),
|
||||
Keyword.delete(meta, :closing),
|
||||
skip_parens?,
|
||||
:force_comma,
|
||||
join,
|
||||
@@ -1263,15 +1273,15 @@ defmodule Code.Formatter do
|
||||
not Enum.any?(args, &match?({:<-, _, [_, _]}, &1))
|
||||
end
|
||||
|
||||
defp do_end_blocks([{{:__block__, meta, [:do]}, _} | rest] = blocks, state) do
|
||||
if meta[:format] == :block or can_force_do_end_blocks?(rest, state) do
|
||||
defp do_end_blocks(meta, [{{:__block__, _, [:do]}, _} | rest] = blocks, state) do
|
||||
if meta?(meta, :do) or can_force_do_end_blocks?(rest, state) do
|
||||
blocks
|
||||
|> Enum.map(fn {{:__block__, meta, [key]}, value} -> {key, line(meta), value} end)
|
||||
|> do_end_blocks_with_range(end_line(meta))
|
||||
end
|
||||
end
|
||||
|
||||
defp do_end_blocks(_, _), do: nil
|
||||
defp do_end_blocks(_, _, _), do: nil
|
||||
|
||||
defp can_force_do_end_blocks?(rest, state) do
|
||||
state.force_do_end_blocks and
|
||||
@@ -1336,7 +1346,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp list_interpolation_to_algebra([entry | entries], escape, state, acc, last) do
|
||||
{{:., _, [Kernel, :to_string]}, meta, [quoted]} = entry
|
||||
{doc, state} = block_to_algebra(quoted, line(meta), end_line(meta), state)
|
||||
{doc, state} = block_to_algebra(quoted, line(meta), closing_line(meta), state)
|
||||
doc = surround("\#{", doc, "}")
|
||||
list_interpolation_to_algebra(entries, escape, state, concat(acc, doc), last)
|
||||
end
|
||||
@@ -1353,7 +1363,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp interpolation_to_algebra([entry | entries], escape, state, acc, last) do
|
||||
{:"::", _, [{{:., _, [Kernel, :to_string]}, meta, [quoted]}, {:binary, _, _}]} = entry
|
||||
{doc, state} = block_to_algebra(quoted, line(meta), end_line(meta), state)
|
||||
{doc, state} = block_to_algebra(quoted, line(meta), closing_line(meta), state)
|
||||
doc = surround("\#{", doc, "}")
|
||||
interpolation_to_algebra(entries, escape, state, concat(acc, doc), last)
|
||||
end
|
||||
@@ -1365,36 +1375,36 @@ defmodule Code.Formatter do
|
||||
## Sigils
|
||||
|
||||
defp maybe_sigil_to_algebra(fun, meta, args, state) do
|
||||
case {Atom.to_string(fun), args} do
|
||||
{<<"sigil_", name>>, [{:<<>>, _, entries}, modifiers]} ->
|
||||
opening_terminator = Keyword.fetch!(meta, :terminator)
|
||||
doc = <<?~, name, opening_terminator::binary>>
|
||||
with <<"sigil_", name>> <- Atom.to_string(fun),
|
||||
[{:<<>>, _, entries}, modifiers] when is_list(modifiers) <- args,
|
||||
opening_delimiter when not is_nil(opening_delimiter) <- meta[:delimiter] do
|
||||
doc = <<?~, name, opening_delimiter::binary>>
|
||||
|
||||
if opening_terminator in [@double_heredoc, @single_heredoc] do
|
||||
closing_terminator = concat(opening_terminator, List.to_string(modifiers))
|
||||
if opening_delimiter in [@double_heredoc, @single_heredoc] do
|
||||
closing_delimiter = concat(opening_delimiter, List.to_string(modifiers))
|
||||
|
||||
{doc, state} =
|
||||
entries
|
||||
|> prepend_heredoc_line()
|
||||
|> interpolation_to_algebra(:heredoc, state, doc, closing_terminator)
|
||||
|
||||
{force_unfit(doc), state}
|
||||
else
|
||||
escape = closing_sigil_terminator(opening_terminator)
|
||||
closing_terminator = concat(escape, List.to_string(modifiers))
|
||||
interpolation_to_algebra(entries, escape, state, doc, closing_terminator)
|
||||
end
|
||||
{doc, state} =
|
||||
entries
|
||||
|> prepend_heredoc_line()
|
||||
|> interpolation_to_algebra(:heredoc, state, doc, closing_delimiter)
|
||||
|
||||
{force_unfit(doc), state}
|
||||
else
|
||||
escape = closing_sigil_delimiter(opening_delimiter)
|
||||
closing_delimiter = concat(escape, List.to_string(modifiers))
|
||||
interpolation_to_algebra(entries, escape, state, doc, closing_delimiter)
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp closing_sigil_terminator("("), do: ")"
|
||||
defp closing_sigil_terminator("["), do: "]"
|
||||
defp closing_sigil_terminator("{"), do: "}"
|
||||
defp closing_sigil_terminator("<"), do: ">"
|
||||
defp closing_sigil_terminator(other) when other in ["\"", "'", "|", "/"], do: other
|
||||
defp closing_sigil_delimiter("("), do: ")"
|
||||
defp closing_sigil_delimiter("["), do: "]"
|
||||
defp closing_sigil_delimiter("{"), do: "}"
|
||||
defp closing_sigil_delimiter("<"), do: ">"
|
||||
defp closing_sigil_delimiter(other) when other in ["\"", "'", "|", "/"], do: other
|
||||
|
||||
## Bitstrings
|
||||
|
||||
@@ -1540,43 +1550,40 @@ defmodule Code.Formatter do
|
||||
|
||||
defp integer_to_algebra(text) do
|
||||
case text do
|
||||
[?0, ?x | rest] ->
|
||||
"0x" <> String.upcase(List.to_string(rest))
|
||||
<<?0, ?x, rest::binary>> ->
|
||||
"0x" <> String.upcase(rest)
|
||||
|
||||
[?0, base | _rest] = digits when base in [?b, ?o] ->
|
||||
List.to_string(digits)
|
||||
<<?0, base, _::binary>> = digits when base in [?b, ?o] ->
|
||||
digits
|
||||
|
||||
[?? | _rest] = char ->
|
||||
List.to_string(char)
|
||||
<<??, _::binary>> = char ->
|
||||
char
|
||||
|
||||
decimal ->
|
||||
List.to_string(insert_underscores(decimal))
|
||||
insert_underscores(decimal)
|
||||
end
|
||||
end
|
||||
|
||||
defp float_to_algebra(text) do
|
||||
{int_part, [?. | decimal_part]} = Enum.split_while(text, &(&1 != ?.))
|
||||
|
||||
decimal_part =
|
||||
decimal_part
|
||||
|> List.to_string()
|
||||
|> String.downcase()
|
||||
|
||||
List.to_string(insert_underscores(int_part)) <> "." <> decimal_part
|
||||
[int_part, decimal_part] = :binary.split(text, ".")
|
||||
decimal_part = String.downcase(decimal_part)
|
||||
insert_underscores(int_part) <> "." <> decimal_part
|
||||
end
|
||||
|
||||
defp insert_underscores(digits) do
|
||||
cond do
|
||||
?_ in digits ->
|
||||
digits =~ "_" ->
|
||||
digits
|
||||
|
||||
length(digits) >= 6 ->
|
||||
byte_size(digits) >= 6 ->
|
||||
digits
|
||||
|> String.to_charlist()
|
||||
|> Enum.reverse()
|
||||
|> Enum.chunk_every(3)
|
||||
|> Enum.intersperse('_')
|
||||
|> List.flatten()
|
||||
|> Enum.reverse()
|
||||
|> List.to_string()
|
||||
|
||||
true ->
|
||||
digits
|
||||
@@ -1622,9 +1629,9 @@ defmodule Code.Formatter do
|
||||
|
||||
defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do
|
||||
min_line = line(meta)
|
||||
max_line = end_line(meta)
|
||||
max_line = closing_line(meta)
|
||||
|
||||
arg_to_algebra = fn arg, args, newlines, state ->
|
||||
arg_to_algebra = fn arg, args, state ->
|
||||
{doc, state} = fun.(arg, state)
|
||||
|
||||
doc =
|
||||
@@ -1635,7 +1642,7 @@ defmodule Code.Formatter do
|
||||
[] when last_arg_mode == :none -> doc
|
||||
end
|
||||
|
||||
{doc, @empty, newlines, state}
|
||||
{{doc, @empty, 1}, state}
|
||||
end
|
||||
|
||||
# If skipping parens, we cannot extract the comments of the first
|
||||
@@ -1643,15 +1650,15 @@ defmodule Code.Formatter do
|
||||
{args, acc, state} =
|
||||
case args do
|
||||
[head | tail] when skip_parens? ->
|
||||
{head, next_line, newlines, state} = arg_to_algebra.(head, tail, 1, state)
|
||||
{tail, [{head, next_line, newlines}], state}
|
||||
{doc_triplet, state} = arg_to_algebra.(head, tail, state)
|
||||
{tail, [doc_triplet], state}
|
||||
|
||||
_ ->
|
||||
{args, [], state}
|
||||
end
|
||||
|
||||
{args_docs, comments?, state} =
|
||||
quoted_to_algebra_with_comments(args, acc, min_line, max_line, 1, state, arg_to_algebra)
|
||||
quoted_to_algebra_with_comments(args, acc, min_line, max_line, state, arg_to_algebra)
|
||||
|
||||
cond do
|
||||
args_docs == [] ->
|
||||
@@ -1823,7 +1830,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp clause_to_algebra({:->, meta, [[], body]}, _min_line, state) do
|
||||
{body_doc, state} = block_to_algebra(body, line(meta), end_line(meta), state)
|
||||
{body_doc, state} = block_to_algebra(body, line(meta), closing_line(meta), state)
|
||||
{"() ->" |> glue(body_doc) |> nest(2), state}
|
||||
end
|
||||
|
||||
@@ -1834,7 +1841,7 @@ defmodule Code.Formatter do
|
||||
{args_doc, state} = clause_args_to_algebra(args, min_line, state)
|
||||
|
||||
state = %{state | operand_nesting: nesting}
|
||||
{body_doc, state} = block_to_algebra(body, min_line, end_line(meta), state)
|
||||
{body_doc, state} = block_to_algebra(body, min_line, closing_line(meta), state)
|
||||
|
||||
doc =
|
||||
args_doc
|
||||
@@ -1847,7 +1854,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp add_max_line_to_last_clause([{op, meta, args}], max_line) do
|
||||
[{op, [end: [line: max_line]] ++ meta, args}]
|
||||
[{op, [closing: [line: max_line]] ++ meta, args}]
|
||||
end
|
||||
|
||||
defp add_max_line_to_last_clause([clause | clauses], max_line) do
|
||||
@@ -1855,13 +1862,13 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp clause_args_to_algebra(args, min_line, state) do
|
||||
arg_to_algebra = fn arg, _args, newlines, state ->
|
||||
arg_to_algebra = fn arg, _args, state ->
|
||||
{doc, state} = clause_args_to_algebra(arg, state)
|
||||
{doc, @empty, newlines, state}
|
||||
{{doc, @empty, 1}, state}
|
||||
end
|
||||
|
||||
{args_docs, comments?, state} =
|
||||
quoted_to_algebra_with_comments([args], [], min_line, @min_line, 1, state, arg_to_algebra)
|
||||
quoted_to_algebra_with_comments([args], [], min_line, @min_line, state, arg_to_algebra)
|
||||
|
||||
if comments? do
|
||||
{Enum.reduce(args_docs, &line(&2, &1)), state}
|
||||
@@ -1889,63 +1896,65 @@ defmodule Code.Formatter do
|
||||
|
||||
## Quoted helpers for comments
|
||||
|
||||
defp quoted_to_algebra_with_comments(args, acc, min_line, max_line, newlines, state, fun) do
|
||||
defp quoted_to_algebra_with_comments(args, acc, min_line, max_line, state, fun) do
|
||||
{pre_comments, state} =
|
||||
get_and_update_in(state.comments, fn comments ->
|
||||
Enum.split_while(comments, fn {line, _, _} -> line <= min_line end)
|
||||
end)
|
||||
|
||||
{docs, comments?, state} =
|
||||
each_quoted_to_algebra_with_comments(args, acc, max_line, newlines, state, false, fun)
|
||||
each_quoted_to_algebra_with_comments(args, acc, max_line, state, false, fun)
|
||||
|
||||
{docs, comments?, update_in(state.comments, &(pre_comments ++ &1))}
|
||||
end
|
||||
|
||||
defp each_quoted_to_algebra_with_comments([], acc, max_line, _newlines, state, comments?, _fun) do
|
||||
%{comments: comments} = state
|
||||
{current, comments} = Enum.split_with(comments, fn {line, _, _} -> line < max_line end)
|
||||
|
||||
extra = for {_, {previous, _}, doc} <- current, do: {doc, @empty, previous}
|
||||
args_docs = merge_algebra_with_comments(Enum.reverse(acc, extra), @empty)
|
||||
{args_docs, comments? or extra != [], %{state | comments: comments}}
|
||||
defp each_quoted_to_algebra_with_comments([], acc, max_line, state, comments?, _fun) do
|
||||
{acc, comments, comments?} = extract_comments_before(max_line, acc, state.comments, comments?)
|
||||
args_docs = merge_algebra_with_comments(Enum.reverse(acc), @empty)
|
||||
{args_docs, comments?, %{state | comments: comments}}
|
||||
end
|
||||
|
||||
defp each_quoted_to_algebra_with_comments(args, acc, max_line, newlines, state, comments?, fun) do
|
||||
defp each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun) do
|
||||
[arg | args] = args
|
||||
{doc_start, doc_end} = traverse_line(arg, {@max_line, @min_line})
|
||||
|
||||
{doc_newlines, acc, comments, comments?} =
|
||||
extract_comments_before(doc_start, newlines, acc, state.comments, comments?)
|
||||
{acc, comments, comments?} =
|
||||
extract_comments_before(doc_start, acc, state.comments, comments?)
|
||||
|
||||
{doc, next_line, doc_newlines, state} =
|
||||
fun.(arg, args, doc_newlines, %{state | comments: comments})
|
||||
{doc_triplet, state} = fun.(arg, args, %{state | comments: comments})
|
||||
|
||||
{doc_newlines, acc, comments, comments?} =
|
||||
extract_comments_trailing(doc_start, doc_end, doc_newlines, acc, state.comments, comments?)
|
||||
{acc, comments, comments?} =
|
||||
extract_comments_trailing(doc_start, doc_end, acc, state.comments, comments?)
|
||||
|
||||
acc = [{doc, next_line, doc_newlines} | acc]
|
||||
acc = [doc_triplet | acc]
|
||||
state = %{state | comments: comments}
|
||||
each_quoted_to_algebra_with_comments(args, acc, max_line, newlines, state, comments?, fun)
|
||||
each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun)
|
||||
end
|
||||
|
||||
defp extract_comments_before(max, _, acc, [{line, _, _} = comment | rest], _) when line < max do
|
||||
defp extract_comments_before(max, acc, [{line, _, _} = comment | rest], _) when line < max do
|
||||
{_, {previous, next}, doc} = comment
|
||||
acc = [{doc, @empty, previous} | acc]
|
||||
extract_comments_before(max, next, acc, rest, true)
|
||||
acc = [{doc, @empty, next} | add_previous_to_acc(acc, previous)]
|
||||
extract_comments_before(max, acc, rest, true)
|
||||
end
|
||||
|
||||
defp extract_comments_before(_max, newlines, acc, rest, comments?) do
|
||||
{newlines, acc, rest, comments?}
|
||||
defp extract_comments_before(_max, acc, rest, comments?) do
|
||||
{acc, rest, comments?}
|
||||
end
|
||||
|
||||
defp extract_comments_trailing(min, max, newlines, acc, [{line, _, doc_comment} | rest], _)
|
||||
defp add_previous_to_acc([{doc, next_line, newlines} | acc], previous) when newlines < previous,
|
||||
do: [{doc, next_line, previous} | acc]
|
||||
|
||||
defp add_previous_to_acc(acc, _previous),
|
||||
do: acc
|
||||
|
||||
defp extract_comments_trailing(min, max, acc, [{line, _, doc_comment} | rest], _)
|
||||
when line >= min and line <= max do
|
||||
acc = [{doc_comment, @empty, newlines} | acc]
|
||||
extract_comments_trailing(min, max, 1, acc, rest, true)
|
||||
acc = [{doc_comment, @empty, 1} | acc]
|
||||
extract_comments_trailing(min, max, acc, rest, true)
|
||||
end
|
||||
|
||||
defp extract_comments_trailing(_min, _max, newlines, acc, rest, comments?) do
|
||||
{newlines, acc, rest, comments?}
|
||||
defp extract_comments_trailing(_min, _max, acc, rest, comments?) do
|
||||
{acc, rest, comments?}
|
||||
end
|
||||
|
||||
defp traverse_line({expr, meta, args}, {min, max}) do
|
||||
@@ -1977,8 +1986,8 @@ defmodule Code.Formatter do
|
||||
# (except for module attributes)
|
||||
# 3. empty lines are collapsed as to not exceed more than one
|
||||
#
|
||||
defp merge_algebra_with_comments([{doc, next_line, _newlines} | docs], left) do
|
||||
right = next_line_separator(docs, next_line)
|
||||
defp merge_algebra_with_comments([{doc, next_line, newlines} | docs], left) do
|
||||
right = if newlines >= @newlines, do: line(), else: next_line
|
||||
|
||||
doc =
|
||||
if left != @empty do
|
||||
@@ -2059,14 +2068,6 @@ defmodule Code.Formatter do
|
||||
end)
|
||||
end
|
||||
|
||||
defp next_line_separator([{_doc, _next_line, newlines} | _], next_line) do
|
||||
if newlines >= @newlines, do: line(), else: next_line
|
||||
end
|
||||
|
||||
defp next_line_separator([], _) do
|
||||
line()
|
||||
end
|
||||
|
||||
defp module_attribute_read?({:@, _, [{var, _, var_context}]})
|
||||
when is_atom(var) and is_atom(var_context) do
|
||||
Code.Identifier.classify(var) == :callable_local
|
||||
@@ -2121,12 +2122,12 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp next_break_fits?({:<<>>, meta, [_ | _] = entries}, state) do
|
||||
meta[:format] == :bin_heredoc or
|
||||
meta[:delimiter] == ~s["""] or
|
||||
(not interpolated?(entries) and eol_or_comments?(meta, state))
|
||||
end
|
||||
|
||||
defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do
|
||||
meta[:format] == :list_heredoc
|
||||
meta[:delimiter] == ~s[''']
|
||||
end
|
||||
|
||||
defp next_break_fits?({{:., _, [_left, :{}]}, _, _}, _state) do
|
||||
@@ -2134,11 +2135,11 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp next_break_fits?({:__block__, meta, [string]}, _state) when is_binary(string) do
|
||||
meta[:format] == :bin_heredoc
|
||||
meta[:delimiter] == ~s["""]
|
||||
end
|
||||
|
||||
defp next_break_fits?({:__block__, meta, [list]}, _state) when is_list(list) do
|
||||
meta[:format] != :charlist
|
||||
meta[:delimeter] != ~s[']
|
||||
end
|
||||
|
||||
defp next_break_fits?({form, _, [_ | _]}, _state) when form in [:fn, :%{}, :%] do
|
||||
@@ -2146,7 +2147,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp next_break_fits?({fun, meta, args}, _state) when is_atom(fun) and is_list(args) do
|
||||
meta[:terminator] in [@double_heredoc, @single_heredoc] and
|
||||
meta[:delimiter] in [@double_heredoc, @single_heredoc] and
|
||||
fun |> Atom.to_string() |> String.starts_with?("sigil_")
|
||||
end
|
||||
|
||||
@@ -2162,23 +2163,47 @@ defmodule Code.Formatter do
|
||||
eol?(meta) or
|
||||
(
|
||||
min_line = line(meta)
|
||||
max_line = end_line(meta)
|
||||
max_line = closing_line(meta)
|
||||
Enum.any?(comments, fn {line, _, _} -> line > min_line and line < max_line end)
|
||||
)
|
||||
end
|
||||
|
||||
# A literal list is a keyword or (... -> ...)
|
||||
defp last_arg_to_keyword([_ | _] = arg, _list_to_keyword?) do
|
||||
defp last_arg_to_keyword([_ | _] = arg, _list_to_keyword?, _skip_parens?, _comments) do
|
||||
{keyword?(arg), arg}
|
||||
end
|
||||
|
||||
# This is a list of tuples, it can be converted to keywords.
|
||||
defp last_arg_to_keyword({:__block__, _, [[_ | _] = arg]} = block, true) do
|
||||
if keyword?(arg), do: {true, arg}, else: {false, block}
|
||||
defp last_arg_to_keyword(
|
||||
{:__block__, meta, [[_ | _] = arg]} = block,
|
||||
true,
|
||||
skip_parens?,
|
||||
comments
|
||||
) do
|
||||
cond do
|
||||
not keyword?(arg) ->
|
||||
{false, block}
|
||||
|
||||
skip_parens? ->
|
||||
block_line = line(meta)
|
||||
{{_, arg_meta, _}, _} = hd(arg)
|
||||
first_line = line(arg_meta)
|
||||
|
||||
case Enum.drop_while(comments, fn {line, _, _} -> line <= block_line end) do
|
||||
[{line, _, _} | _] when line <= first_line ->
|
||||
{false, block}
|
||||
|
||||
_ ->
|
||||
{true, arg}
|
||||
end
|
||||
|
||||
true ->
|
||||
{true, arg}
|
||||
end
|
||||
end
|
||||
|
||||
# Otherwise we don't have a keyword.
|
||||
defp last_arg_to_keyword(arg, _list_to_keyword?) do
|
||||
defp last_arg_to_keyword(arg, _list_to_keyword?, _skip_parens?, _comments) do
|
||||
{false, arg}
|
||||
end
|
||||
|
||||
@@ -2210,28 +2235,24 @@ defmodule Code.Formatter do
|
||||
if force_args?(arg), do: force_unfit(doc), else: doc
|
||||
end
|
||||
|
||||
defp keyword?([{key, _} | list]) do
|
||||
keyword_key?(key) and keyword?(list)
|
||||
end
|
||||
defp keyword?([{_, _} | list]), do: keyword?(list)
|
||||
defp keyword?(rest), do: rest == []
|
||||
|
||||
defp keyword?(rest) do
|
||||
rest == []
|
||||
end
|
||||
defp keyword_key?({:__block__, meta, [atom]}) when is_atom(atom),
|
||||
do: meta[:format] == :keyword
|
||||
|
||||
defp keyword_key?({:__block__, meta, [_]}) do
|
||||
meta[:format] == :keyword
|
||||
end
|
||||
defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, meta, [{:<<>>, _, _}, :utf8]}),
|
||||
do: meta[:format] == :keyword
|
||||
|
||||
defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, meta, _}, :utf8]}) do
|
||||
meta[:format] == :keyword
|
||||
end
|
||||
|
||||
defp keyword_key?(_) do
|
||||
false
|
||||
end
|
||||
defp keyword_key?(_),
|
||||
do: false
|
||||
|
||||
defp eol?(meta) do
|
||||
Keyword.get(meta, :eol, false)
|
||||
Keyword.get(meta, :newlines, 0) > 0
|
||||
end
|
||||
|
||||
defp meta?(meta, key) do
|
||||
is_list(meta[key])
|
||||
end
|
||||
|
||||
defp line(meta) do
|
||||
@@ -2242,6 +2263,10 @@ defmodule Code.Formatter do
|
||||
meta[:end][:line] || @min_line
|
||||
end
|
||||
|
||||
defp closing_line(meta) do
|
||||
meta[:closing][:line] || @min_line
|
||||
end
|
||||
|
||||
## Algebra helpers
|
||||
|
||||
# Relying on the inner document is brittle and error prone.
|
||||
|
||||
@@ -37,7 +37,7 @@ defprotocol Collectable do
|
||||
iex> collector_fun.(updated_acc, :done)
|
||||
#MapSet<[1, 2, 3]>
|
||||
|
||||
To show how the protocol can be implemented, we can take again a look at the
|
||||
To show how the protocol can be implemented, we can again look at the
|
||||
implementation for `MapSet`. In this implementation "collecting" elements
|
||||
simply means inserting them in the set through `MapSet.put/2`.
|
||||
|
||||
@@ -143,7 +143,7 @@ end
|
||||
defimpl Collectable, for: Map do
|
||||
def into(original) do
|
||||
fun = fn
|
||||
map, {:cont, {k, v}} -> :maps.put(k, v, map)
|
||||
map, {:cont, {k, v}} -> Map.put(map, k, v)
|
||||
map, :done -> map
|
||||
_, :halt -> :ok
|
||||
end
|
||||
|
||||
+33
-19
@@ -4,8 +4,8 @@ defmodule Config do
|
||||
|
||||
## Example
|
||||
|
||||
This module is most commonly used to define application
|
||||
configuration, typically in `config/config.exs`:
|
||||
This module is most commonly used to define application configuration,
|
||||
typically in `config/config.exs`:
|
||||
|
||||
import Config
|
||||
|
||||
@@ -18,14 +18,20 @@ defmodule Config do
|
||||
`import Config` will import the functions `config/2`, `config/3`
|
||||
and `import_config/1` to help you manage your configuration.
|
||||
|
||||
Once Mix starts, it will automatically evaluate the configuration
|
||||
file and persist it into `:some_app`'s application environment, which
|
||||
can be accessed in as follows:
|
||||
`config/2` and `config/3` are used to define key-value configuration
|
||||
for a given application. Once Mix starts, it will automatically
|
||||
evaluate the configuration file and persist the configuration above
|
||||
into `:some_app`'s application environment, which can be accessed in
|
||||
as follows:
|
||||
|
||||
"value1" = Application.fetch_env!(:some_app, :key1)
|
||||
|
||||
See `Config.Reader` for evaluating and reading configuration
|
||||
files.
|
||||
Finally, the line `import_config "#{Mix.env()}.exs"` will import other
|
||||
config files, based on the current Mix environment, such as
|
||||
`config/dev.exs` and `config/test.exs`.
|
||||
|
||||
`Config` also provides a low-level API for evaluating and reading
|
||||
configuration, under the `Config.Reader` module.
|
||||
|
||||
**Important:** if you are writing a library to be used by other developers,
|
||||
it is generally recommended to avoid the application environment, as the
|
||||
@@ -34,26 +40,34 @@ defmodule Config do
|
||||
|
||||
## Migrating from `use Mix.Config`
|
||||
|
||||
The `Config` module in Elixir was introduced in v1.9 as a
|
||||
replacement to `Mix.Config`, which was specific to Mix and
|
||||
has been deprecated.
|
||||
The `Config` module in Elixir was introduced in v1.9 as a replacement to
|
||||
`Mix.Config`, which was specific to Mix and has been deprecated.
|
||||
|
||||
You can leverage `Config` instead of `Mix.Config` in two steps.
|
||||
The first step is to replace `use Mix.Config` at the top of
|
||||
your config files by `import Config`.
|
||||
You can leverage `Config` instead of `Mix.Config` in two steps. The first
|
||||
step is to replace `use Mix.Config` at the top of your config files by
|
||||
`import Config`.
|
||||
|
||||
The second is to make sure your `import_config/1` calls do
|
||||
not have a wildcard character. If so, you need to perform
|
||||
the wildcard lookup manually. For example, if you did:
|
||||
The second is to make sure your `import_config/1` calls do not have a
|
||||
wildcard character. If so, you need to perform the wildcard lookup
|
||||
manually. For example, if you did:
|
||||
|
||||
import_config "../apps/*/config/config.exs"
|
||||
|
||||
It has to be replaced by:
|
||||
|
||||
for config <- "apps/*/config/config.exs" |> Path.expand() |> Path.wildcard() do
|
||||
for config <- "../apps/*/config/config.exs" |> Path.expand(__DIR__) |> Path.wildcard() do
|
||||
import_config config
|
||||
end
|
||||
|
||||
## config/releases.exs
|
||||
|
||||
If you are using releases, see `mix release`, there is another configuration
|
||||
file called `config/releases.exs`. While `config/config.exs` and friends
|
||||
mentioned in the previous section are executed whenever you run a Mix
|
||||
command, including when you assemble a release, `config/releases.exs` is
|
||||
executed every time your production system boots. Since Mix is not available
|
||||
in a production system, `config/releases.exs` must not use any of the
|
||||
functions from Mix.
|
||||
"""
|
||||
|
||||
@config_key {__MODULE__, :config}
|
||||
@@ -152,7 +166,7 @@ defmodule Config do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
def config(root_key, key, opts) when is_atom(root_key) do
|
||||
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
|
||||
get_config!()
|
||||
|> __merge__([{root_key, [{key, opts}]}])
|
||||
|> put_config()
|
||||
@@ -182,7 +196,7 @@ defmodule Config do
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __import__!(Path.t()) :: keyword()
|
||||
@spec __import__!(Path.t()) :: {term, Code.binding()}
|
||||
def __import__!(file) when is_binary(file) do
|
||||
current_files = get_files!()
|
||||
|
||||
|
||||
@@ -39,9 +39,15 @@ defmodule Config.Provider do
|
||||
end
|
||||
end
|
||||
|
||||
Then when specifying your release, you can specify the provider:
|
||||
Then when specifying your release, you can specify the provider in
|
||||
the release configuration:
|
||||
|
||||
config_providers: [{JSONConfigProvider, "/etc/config.json"}]
|
||||
releases: [
|
||||
demo: [
|
||||
# ...,
|
||||
config_providers: [{JSONConfigProvider, "/etc/config.json"}]
|
||||
]
|
||||
]
|
||||
|
||||
Now once the system boots, it will invoke the provider early in
|
||||
the boot process, save the merged configuration to the disk, and
|
||||
@@ -59,8 +65,9 @@ defmodule Config.Provider do
|
||||
|
||||
* a binary representing an absolute path
|
||||
|
||||
* a tuple {:system, system_var, path} where the config is the
|
||||
concatenation of the `system_var` with the given `path`
|
||||
* a `{:system, system_var, path}` tuple where the config is the
|
||||
concatenation of the environment variable `system_var` with
|
||||
the given `path`
|
||||
|
||||
"""
|
||||
@type config_path :: {:system, binary(), binary()} | binary()
|
||||
@@ -98,7 +105,14 @@ defmodule Config.Provider do
|
||||
@callback load(config, state) :: config
|
||||
|
||||
@doc false
|
||||
defstruct [:providers, :config_path, extra_config: [], prune_after_boot: false]
|
||||
defstruct [
|
||||
:providers,
|
||||
:config_path,
|
||||
extra_config: [],
|
||||
prune_after_boot: false,
|
||||
reboot_after_config: true,
|
||||
validate_compile_env: false
|
||||
]
|
||||
|
||||
@doc """
|
||||
Validates a `t:config_path/0`.
|
||||
@@ -141,7 +155,7 @@ defmodule Config.Provider do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def boot(app, key, restart_fun \\ &System.restart/0) do
|
||||
def boot(app, key, restart_fun \\ &restart_and_sleep/0) do
|
||||
# The app with the config provider settings may not
|
||||
# have been loaded at this point, so make sure we load
|
||||
# its environment before querying it.
|
||||
@@ -152,32 +166,146 @@ defmodule Config.Provider do
|
||||
# before we go around running Elixir code.
|
||||
{:ok, _} = :application.ensure_all_started(:elixir)
|
||||
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
validate_no_cyclic_boot!(path)
|
||||
|
||||
read_config!(path)
|
||||
|> Config.__merge__([{app, [{key, booted_key(provider, path)}]} | provider.extra_config])
|
||||
|> run_providers(provider)
|
||||
|> write_config!(path)
|
||||
|
||||
restart_fun.()
|
||||
# The key we store if the system already booted
|
||||
booted_key = :"#{key}_booted"
|
||||
|
||||
case :application.get_env(app, booted_key) do
|
||||
{:ok, {:booted, path}} ->
|
||||
File.rm(path)
|
||||
:booted
|
||||
path && File.rm(path)
|
||||
|
||||
with {:ok, %Config.Provider{} = provider} <- :application.get_env(app, key) do
|
||||
maybe_validate_compile_env(provider)
|
||||
end
|
||||
|
||||
{:ok, :booted} ->
|
||||
:booted
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
reboot_config = [{app, [{booted_key, booted_value(provider, path)}]}]
|
||||
boot_providers(path, provider, reboot_config, restart_fun)
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp booted_key(%{prune_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_key(%{prune_after_boot: false}, _path), do: :booted
|
||||
defp boot_providers(path, provider, reboot_config, restart_fun) do
|
||||
validate_no_cyclic_boot!(path)
|
||||
loaded_applications = :application.loaded_applications()
|
||||
original_config = read_config!(path)
|
||||
|
||||
config =
|
||||
original_config
|
||||
|> Config.__merge__(provider.extra_config)
|
||||
|> run_providers(provider)
|
||||
|
||||
if provider.reboot_after_config do
|
||||
config
|
||||
|> Config.__merge__(reboot_config)
|
||||
|> write_config!(path)
|
||||
|
||||
restart_fun.()
|
||||
else
|
||||
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
|
||||
abort("""
|
||||
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
|
||||
to false and #{inspect(app)} has already been loaded, meaning any further \
|
||||
configuration won't have an effect.
|
||||
|
||||
The configuration for #{inspect(app)} before config providers was:
|
||||
|
||||
#{inspect(original_config[app])}
|
||||
|
||||
The configuration for #{inspect(app)} after config providers was:
|
||||
|
||||
#{inspect(config[app])}
|
||||
""")
|
||||
end
|
||||
|
||||
_ = Application.put_all_env(config, persistent: true)
|
||||
maybe_validate_compile_env(provider)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_validate_compile_env(provider) do
|
||||
with [_ | _] = compile_env <- provider.validate_compile_env do
|
||||
validate_compile_env(compile_env)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_compile_env(compile_env) do
|
||||
for {app, [key | path], compile_return} <- compile_env,
|
||||
Application.ensure_loaded(app) == :ok do
|
||||
try do
|
||||
traverse_env(Application.fetch_env(app, key), path)
|
||||
rescue
|
||||
e ->
|
||||
abort("""
|
||||
application #{inspect(app)} failed reading its compile environment #{path(key, path)}:
|
||||
|
||||
#{Exception.format(:error, e, __STACKTRACE__)}
|
||||
|
||||
Expected it to match the compile time value of #{return_to_text(compile_return)}.
|
||||
|
||||
#{compile_env_tips(app)}
|
||||
""")
|
||||
else
|
||||
^compile_return ->
|
||||
:ok
|
||||
|
||||
runtime_return ->
|
||||
abort("""
|
||||
the application #{inspect(app)} has a different value set #{path(key, path)} \
|
||||
during runtime compared to compile time. Since this application environment entry was \
|
||||
marked as compile time, this difference can lead to different behaviour than expected:
|
||||
|
||||
* Compile time value #{return_to_text(compile_return)}
|
||||
* Runtime value #{return_to_text(runtime_return)}
|
||||
|
||||
#{compile_env_tips(app)}
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp path(key, []), do: "for key #{inspect(key)}"
|
||||
defp path(key, path), do: "for path #{inspect(path)} inside key #{inspect(key)}"
|
||||
|
||||
defp compile_env_tips(app),
|
||||
do: """
|
||||
To fix this error, you might:
|
||||
|
||||
* Make the runtime value match the compile time one
|
||||
|
||||
* Recompile your project. If the misconfigured application is a dependency, \
|
||||
you may need to run "mix deps.compile #{app} --force"
|
||||
|
||||
* Alternatively, you can disable this check. If you are using releases, you can \
|
||||
set :validate_compile_env to false in your release configuration. If you are \
|
||||
using Mix to start your system, you can pass the --no-validate-compile-env flag
|
||||
"""
|
||||
|
||||
defp return_to_text({:ok, value}), do: "was set to: #{inspect(value)}"
|
||||
defp return_to_text(:error), do: "was not set"
|
||||
|
||||
defp traverse_env(return, []), do: return
|
||||
defp traverse_env(:error, _paths), do: :error
|
||||
defp traverse_env({:ok, value}, [key | keys]), do: traverse_env(Access.fetch(value, key), keys)
|
||||
|
||||
defp restart_and_sleep do
|
||||
:init.restart()
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
|
||||
defp booted_value(%{prune_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_value(%{prune_after_boot: false}, _path), do: {:booted, nil}
|
||||
|
||||
defp validate_no_cyclic_boot!(path) do
|
||||
if System.get_env("ELIXIR_CONFIG_PROVIDER_BOOTED") do
|
||||
@@ -220,11 +348,9 @@ defmodule Config.Provider do
|
||||
end
|
||||
|
||||
defp write_config!(config, path) do
|
||||
{date, time} = :erlang.localtime()
|
||||
args = [date, time, config]
|
||||
contents = :io_lib.format("%% coding: utf-8~n%% config generated at ~p ~p~n~p.~n", args)
|
||||
contents = :io_lib.format("%% coding: utf-8~n~tw.~n", [config])
|
||||
|
||||
case File.write(path, contents) do
|
||||
case File.write(path, contents, [:utf8]) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -246,6 +372,6 @@ defmodule Config.Provider do
|
||||
|
||||
defp abort(msg) do
|
||||
IO.puts(:stderr, "ERROR! " <> msg)
|
||||
raise(msg)
|
||||
:erlang.raise(:error, "aborting boot", [{Config.Provider, :boot, 2, []}])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,11 +4,24 @@ defmodule Config.Reader do
|
||||
|
||||
## As a provider
|
||||
|
||||
`Config.Reader` can also be used as a `Config.Provider`.
|
||||
When used as a provider, it expects a single argument:
|
||||
which the configuration path (as outlined in
|
||||
`t:Config.Provider.config_path/0`) for the configuration
|
||||
to be read and loaded during the system boot.
|
||||
`Config.Reader` can also be used as a `Config.Provider`. When used
|
||||
as a provider, it expects a single argument: the configuration path
|
||||
(as outlined in `t:Config.Provider.config_path/0`) for the file to
|
||||
be read and loaded during the system boot.
|
||||
|
||||
For example, if you expect the target system to have a config file
|
||||
in an absolute path, you can configure your `mix release` as:
|
||||
|
||||
config_providers: [{Config.Reader, "/etc/config.exs"}]
|
||||
|
||||
Or if you want to read a custom path inside the release:
|
||||
|
||||
config_provider: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]
|
||||
|
||||
Note by default Mix releases supports runtime configuration via
|
||||
a `config/releases.exs`. If a `config/releases.exs` exists in your
|
||||
application, it is automatically copied inside the release and
|
||||
automatically set as a config provider.
|
||||
"""
|
||||
|
||||
@behaviour Config.Provider
|
||||
|
||||
@@ -11,9 +11,8 @@ defmodule DynamicSupervisor do
|
||||
|
||||
## Examples
|
||||
|
||||
A dynamic supervisor is started with no children, often under a
|
||||
supervisor with the supervision strategy (the only strategy currently
|
||||
supported is `:one_for_one`) and a name:
|
||||
A dynamic supervisor is started with no children, a supervision strategy
|
||||
(the only strategy currently supported is `:one_for_one`), and a name:
|
||||
|
||||
children = [
|
||||
{DynamicSupervisor, strategy: :one_for_one, name: MyApp.DynamicSupervisor}
|
||||
@@ -149,12 +148,9 @@ defmodule DynamicSupervisor do
|
||||
}
|
||||
|
||||
@typedoc "Option values used by the `start*` functions"
|
||||
@type option :: {:name, Supervisor.name()} | init_option()
|
||||
@type option :: GenServer.option()
|
||||
|
||||
@typedoc "Options used by the `start*` functions"
|
||||
@type options :: [option, ...]
|
||||
|
||||
@typedoc "Options given to `start_link/2` and `init/1`"
|
||||
@typedoc "Options given to `start_link/1` and `init/1`"
|
||||
@type init_option ::
|
||||
{:strategy, strategy()}
|
||||
| {:max_restarts, non_neg_integer()}
|
||||
@@ -211,7 +207,7 @@ defmodule DynamicSupervisor do
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
@behaviour DynamicSupervisor
|
||||
if Module.get_attribute(__MODULE__, :doc) == nil do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -255,7 +251,7 @@ defmodule DynamicSupervisor do
|
||||
with `:normal` reason.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec start_link(options) :: Supervisor.on_start()
|
||||
@spec start_link([option | init_option]) :: Supervisor.on_start()
|
||||
def start_link(options) when is_list(options) do
|
||||
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
|
||||
{sup_opts, start_opts} = Keyword.split(options, keys)
|
||||
@@ -281,7 +277,7 @@ defmodule DynamicSupervisor do
|
||||
section in the `GenServer` module docs.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec start_link(module, term, GenServer.options()) :: Supervisor.on_start()
|
||||
@spec start_link(module, term, [option]) :: Supervisor.on_start()
|
||||
def start_link(mod, init_arg, opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, {mod, init_arg, opts[:name]}, opts)
|
||||
end
|
||||
@@ -290,7 +286,7 @@ defmodule DynamicSupervisor do
|
||||
Dynamically adds a child specification to `supervisor` and starts that child.
|
||||
|
||||
`child_spec` should be a valid child specification as detailed in the
|
||||
"child_spec/1" section of the documentation for `Supervisor`. The child
|
||||
"Child specification" section of the documentation for `Supervisor`. The child
|
||||
process will be started as defined in the child specification.
|
||||
|
||||
If the child process start function returns `{:ok, child}` or `{:ok, child,
|
||||
@@ -310,7 +306,13 @@ defmodule DynamicSupervisor do
|
||||
this function returns `{:error, :max_children}`.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec start_child(Supervisor.supervisor(), Supervisor.child_spec() | {module, term} | module) ::
|
||||
@spec start_child(
|
||||
Supervisor.supervisor(),
|
||||
Supervisor.child_spec()
|
||||
| {module, term}
|
||||
| module
|
||||
| (old_erlang_child_spec :: :supervisor.child_spec())
|
||||
) ::
|
||||
on_start_child()
|
||||
def start_child(supervisor, {_, _, _, _, _, _} = child_spec) do
|
||||
validate_and_start_child(supervisor, child_spec)
|
||||
@@ -473,7 +475,7 @@ defmodule DynamicSupervisor do
|
||||
module-based supervisors. See the "Module-based supervisors" section
|
||||
in the module documentation for more information.
|
||||
|
||||
The `options` received by this function are also supported by `start_link/2`.
|
||||
The `options` received by this function are also supported by `start_link/1`.
|
||||
|
||||
This function returns a tuple containing the supervisor options.
|
||||
|
||||
@@ -745,7 +747,20 @@ defmodule DynamicSupervisor do
|
||||
end
|
||||
|
||||
def handle_info(msg, state) do
|
||||
:error_logger.error_msg('DynamicSupervisor received unexpected message: ~p~n', [msg])
|
||||
:logger.error(
|
||||
%{
|
||||
label: {DynamicSupervisor, :unexpected_msg},
|
||||
report: %{
|
||||
msg: msg
|
||||
}
|
||||
},
|
||||
%{
|
||||
domain: [:otp, :elixir],
|
||||
error_logger: %{tag: :error_msg},
|
||||
report_cb: &__MODULE__.format_report/1
|
||||
}
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -988,12 +1003,22 @@ defmodule DynamicSupervisor do
|
||||
end
|
||||
|
||||
defp report_error(error, reason, pid, child, %{name: name, extra_arguments: extra}) do
|
||||
:error_logger.error_report(
|
||||
:supervisor_report,
|
||||
supervisor: name,
|
||||
errorContext: error,
|
||||
reason: reason,
|
||||
offender: extract_child(pid, child, extra)
|
||||
:logger.error(
|
||||
%{
|
||||
label: {:supervisor, error},
|
||||
report: [
|
||||
{:supervisor, name},
|
||||
{:errorContext, error},
|
||||
{:reason, reason},
|
||||
{:offender, extract_child(pid, child, extra)}
|
||||
]
|
||||
},
|
||||
%{
|
||||
domain: [:otp, :sasl],
|
||||
report_cb: &:logger.format_otp_report/1,
|
||||
logger_formatter: %{title: "SUPERVISOR REPORT"},
|
||||
error_logger: %{tag: :error_report, type: :supervisor_report}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1024,4 +1049,12 @@ defmodule DynamicSupervisor do
|
||||
defp call(supervisor, req) do
|
||||
GenServer.call(supervisor, req, :infinity)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_report(%{
|
||||
label: {__MODULE__, :unexpected_msg},
|
||||
report: %{msg: msg}
|
||||
}) do
|
||||
{'DynamicSupervisor received unexpected message: ~p~n', [msg]}
|
||||
end
|
||||
end
|
||||
|
||||
+551
-189
File diff suppressed because it is too large
Load Diff
+51
-31
@@ -208,7 +208,7 @@ defmodule Exception do
|
||||
{_, kind, _, clauses} <- List.keyfind(defs, {function, arity}, 0) do
|
||||
clauses =
|
||||
for {meta, ex_args, guards, _block} <- clauses do
|
||||
scope = :elixir_erl.scope(meta)
|
||||
scope = :elixir_erl.scope(meta, true)
|
||||
|
||||
{erl_args, scope} =
|
||||
:elixir_erl_clauses.match(&:elixir_erl_pass.translate_args/2, ex_args, scope)
|
||||
@@ -544,8 +544,17 @@ defmodule Exception do
|
||||
defp format_application(module) do
|
||||
# We cannot use Application due to bootstrap issues
|
||||
case :application.get_application(module) do
|
||||
{:ok, app} -> "(" <> Atom.to_string(app) <> ") "
|
||||
:undefined -> ""
|
||||
{:ok, app} ->
|
||||
case :application.get_key(app, :vsn) do
|
||||
{:ok, vsn} when is_list(vsn) ->
|
||||
"(" <> Atom.to_string(app) <> " " <> List.to_string(vsn) <> ") "
|
||||
|
||||
_ ->
|
||||
"(" <> Atom.to_string(app) <> ") "
|
||||
end
|
||||
|
||||
:undefined ->
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1053,6 +1062,8 @@ end
|
||||
defmodule FunctionClauseError do
|
||||
defexception [:module, :function, :arity, :kind, :args, :clauses]
|
||||
|
||||
@clause_limit 10
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
case exception do
|
||||
@@ -1098,37 +1109,46 @@ defmodule FunctionClauseError do
|
||||
|
||||
mfa = Exception.format_mfa(module, function, arity)
|
||||
|
||||
formatted_args =
|
||||
args
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {arg, i} ->
|
||||
["\n # ", Integer.to_string(i), "\n ", pad(inspect_fun.(arg)), "\n"]
|
||||
end)
|
||||
format_clause_fun = fn {args, guards} ->
|
||||
code = Enum.reduce(guards, {function, [], args}, &{:when, [], [&2, &1]})
|
||||
" #{kind} " <> Macro.to_string(code, ast_fun) <> "\n"
|
||||
end
|
||||
|
||||
formatted_clauses =
|
||||
if clauses do
|
||||
format_clause_fun = fn {args, guards} ->
|
||||
code = Enum.reduce(guards, {function, [], args}, &{:when, [], [&2, &1]})
|
||||
" #{kind} " <> Macro.to_string(code, ast_fun) <> "\n"
|
||||
end
|
||||
|
||||
top_10 =
|
||||
clauses
|
||||
|> Enum.take(10)
|
||||
|> Enum.map(format_clause_fun)
|
||||
|
||||
[
|
||||
"\nAttempted function clauses (showing #{length(top_10)} out of #{length(clauses)}):",
|
||||
"\n\n",
|
||||
top_10
|
||||
]
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
"\n\nThe following arguments were given to #{mfa}:\n#{formatted_args}#{formatted_clauses}"
|
||||
"\n\nThe following arguments were given to #{mfa}:\n" <>
|
||||
"#{format_args(args, inspect_fun)}" <>
|
||||
"#{format_clauses(clauses, format_clause_fun, @clause_limit)}"
|
||||
end
|
||||
|
||||
defp format_args(args, inspect_fun) do
|
||||
args
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {arg, i} ->
|
||||
[pad("\n# "), Integer.to_string(i), pad("\n"), pad(inspect_fun.(arg)), "\n"]
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_clauses(clauses, format_clause_fun, limit)
|
||||
defp format_clauses(nil, _, _), do: ""
|
||||
defp format_clauses([], _, _), do: ""
|
||||
|
||||
defp format_clauses(clauses, format_clause_fun, limit) do
|
||||
top_clauses =
|
||||
clauses
|
||||
|> Enum.take(limit)
|
||||
|> Enum.map(format_clause_fun)
|
||||
|
||||
[
|
||||
"\nAttempted function clauses (showing #{length(top_clauses)} out of #{length(clauses)}):",
|
||||
"\n\n",
|
||||
top_clauses,
|
||||
non_visible_clauses(length(clauses) - limit)
|
||||
]
|
||||
end
|
||||
|
||||
defp non_visible_clauses(n) when n <= 0, do: []
|
||||
defp non_visible_clauses(1), do: [" ...\n (1 clause not shown)\n"]
|
||||
defp non_visible_clauses(n), do: [" ...\n (#{n} clauses not shown)\n"]
|
||||
|
||||
defp pad(string) do
|
||||
String.replace(string, "\n", "\n ")
|
||||
end
|
||||
|
||||
+14
-12
@@ -6,7 +6,7 @@ defmodule File do
|
||||
to interact with files or IO devices, like `open/2`,
|
||||
`copy/3` and others. This module also provides higher
|
||||
level functions that work with filenames and have their naming
|
||||
based on UNIX variants. For example, one can copy a file
|
||||
based on Unix variants. For example, one can copy a file
|
||||
via `cp/3` and remove files and directories recursively
|
||||
via `rm_rf/1`.
|
||||
|
||||
@@ -585,7 +585,7 @@ defmodule File do
|
||||
File.touch!("/tmp/a.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
#=> :ok
|
||||
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
#=> ** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
|
||||
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
|
||||
|
||||
File.touch!("/tmp/a.txt", 1544519753)
|
||||
|
||||
@@ -722,7 +722,7 @@ defmodule File do
|
||||
|
||||
Returns `:ok` in case of success, `{:error, reason}` otherwise.
|
||||
|
||||
Note: The command `mv` in Unix systems behaves differently depending on
|
||||
Note: The command `mv` in Unix-like systems behaves differently depending on
|
||||
whether `source` is a file and the `destination` is an existing directory.
|
||||
We have chosen to explicitly disallow this behaviour.
|
||||
|
||||
@@ -778,7 +778,7 @@ defmodule File do
|
||||
or do a straight copy from a source to a destination without
|
||||
preserving modes, check `copy/3` instead.
|
||||
|
||||
Note: The command `cp` in Unix systems behaves differently depending on
|
||||
Note: The command `cp` in Unix-like systems behaves differently depending on
|
||||
whether the destination is an existing directory or not. We have chosen to
|
||||
explicitly disallow copying to a destination which is a directory,
|
||||
and an error will be returned if tried.
|
||||
@@ -846,7 +846,7 @@ defmodule File do
|
||||
success, `files_and_directories` lists all files and directories copied in no
|
||||
specific order. It returns `{:error, reason, file}` otherwise.
|
||||
|
||||
Note: The command `cp` in Unix systems behaves differently depending on
|
||||
Note: The command `cp` in Unix-like systems behaves differently depending on
|
||||
whether `destination` is an existing directory or not. We have chosen to
|
||||
explicitly disallow this behaviour. If `source` is a `file` and `destination`
|
||||
is a directory, `{:error, :eisdir}` will be returned.
|
||||
@@ -1243,7 +1243,7 @@ defmodule File do
|
||||
end
|
||||
|
||||
# On Windows, symlinks are treated as directory and must be removed
|
||||
# with rmdir/1. But on Unix, we remove them via rm/1. So we first try
|
||||
# with rmdir/1. But on Unix-like systems, we remove them via rm/1. So we first try
|
||||
# to remove it as a directory and, if we get :enotdir, we fall back to
|
||||
# a file removal.
|
||||
defp do_rm_directory(path, {:ok, acc} = entry) do
|
||||
@@ -1310,7 +1310,7 @@ defmodule File do
|
||||
|
||||
The allowed modes:
|
||||
|
||||
* `:binary` - opens the file in binary mode, disabling special handling of unicode sequences
|
||||
* `:binary` - opens the file in binary mode, disabling special handling of Unicode sequences
|
||||
(default mode).
|
||||
|
||||
* `:read` - the file, which must exist, is opened for reading.
|
||||
@@ -1355,9 +1355,11 @@ defmodule File do
|
||||
* `{:ok, io_device}` - the file has been opened in the requested mode.
|
||||
|
||||
`io_device` is actually the PID of the process which handles the file.
|
||||
This process is linked to the process which originally opened the file.
|
||||
If any process to which the `io_device` is linked terminates, the file
|
||||
will be closed and the process itself will be terminated.
|
||||
This process monitors the process that originally opened the file (the
|
||||
owner process). If the owner process terminates, the file is closed and
|
||||
the process itself terminates too. If any process to which the `io_device`
|
||||
is linked terminates, the file will be closed and the process itself will
|
||||
be terminated.
|
||||
|
||||
An `io_device` returned from this call can be used as an argument to the
|
||||
`IO` module functions.
|
||||
@@ -1462,7 +1464,7 @@ defmodule File do
|
||||
@doc """
|
||||
Gets the current working directory.
|
||||
|
||||
In rare circumstances, this function can fail on Unix. It may happen
|
||||
In rare circumstances, this function can fail on Unix-like systems. It may happen
|
||||
if read permissions do not exist for the parent directories of the
|
||||
current directory. For this reason, returns `{:ok, cwd}` in case
|
||||
of success, `{:error, reason}` otherwise.
|
||||
@@ -1610,7 +1612,7 @@ defmodule File do
|
||||
in raw mode for performance reasons. Therefore, Elixir **will** open
|
||||
streams in `:raw` mode with the `:read_ahead` option unless an encoding
|
||||
is specified. This means any data streamed into the file must be
|
||||
converted to `t:iodata/0` type. If you pass e.g. `[encoding: :utf8]`
|
||||
converted to `t:iodata/0` 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` .
|
||||
|
||||
@@ -23,7 +23,7 @@ defmodule File.Stat do
|
||||
* `mtime` - the last time the file was written.
|
||||
|
||||
* `ctime` - the interpretation of this time field depends on the operating
|
||||
system. On Unix, it is the last time the file or the inode was changed.
|
||||
system. On Unix-like operating systems, it is the last time the file or the inode was changed.
|
||||
In Windows, it is the time of creation.
|
||||
|
||||
* `mode` - the file permissions.
|
||||
@@ -35,17 +35,17 @@ defmodule File.Stat do
|
||||
In Windows, the number indicates a drive as follows: 0 means A:, 1 means
|
||||
B:, and so on.
|
||||
|
||||
* `minor_device` - only valid for character devices on Unix. In all other
|
||||
* `minor_device` - only valid for character devices on Unix-like systems. In all other
|
||||
cases, this field is zero.
|
||||
|
||||
* `inode` - gives the inode number. On non-Unix file systems, this field
|
||||
* `inode` - gives the inode number. On non-Unix-like file systems, this field
|
||||
will be zero.
|
||||
|
||||
* `uid` - indicates the owner of the file. Will be zero for non-Unix file
|
||||
* `uid` - indicates the owner of the file. Will be zero for non-Unix-like file
|
||||
systems.
|
||||
|
||||
* `gid` - indicates the group that owns the file. Will be zero for
|
||||
non-Unix file systems.
|
||||
non-Unix-like file systems.
|
||||
|
||||
The time type returned in `atime`, `mtime`, and `ctime` is dependent on the
|
||||
time type set in options. `{:time, type}` where type can be `:local`,
|
||||
|
||||
@@ -2,11 +2,43 @@ defmodule Function do
|
||||
@moduledoc """
|
||||
A set of functions for working with functions.
|
||||
|
||||
There are two types of captured functions: **external** and **local**.
|
||||
External functions are functions residing in modules that are captured
|
||||
with `&/1`, such as `&String.length/1`. Local functions are anonymous functions
|
||||
defined with `fn/1` or with the capture operator `&/1` using `&1`, `&2`,
|
||||
and so on as replacements.
|
||||
Anonymous functions are typically created by using `fn`:
|
||||
|
||||
iex> add = fn a, b -> a + b end
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
It is also possible to capture module functions and pass them around
|
||||
as if they were anonymous functions by using the capture operator `&/1`:
|
||||
|
||||
iex> add = &Kernel.+/2
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
iex> length = &String.length/1
|
||||
iex> length.("hello")
|
||||
5
|
||||
|
||||
It is also possible to capture a definition in the current module by
|
||||
skipping the module prefix, such as `&my_fun/2`.
|
||||
|
||||
The capture operator can also be used to create anonymous functions
|
||||
that expect at least one argument:
|
||||
|
||||
iex> add = &(&1 + &2)
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
In such cases, using the capture operator is no different than using `fn`.
|
||||
|
||||
We say that functions that point to definitions residing in modules, such
|
||||
as `&String.length/1`, are **external** functions. All other functions are
|
||||
**local** and they are always bound to the file or module that defined them.
|
||||
|
||||
Besides the functions in this module to work with functions, `Kernel` also
|
||||
has an `apply/2` function that invokes a function with a dynamic number of
|
||||
arguments, as well as `is_function/1` and `is_function/2`, to check
|
||||
respectively if a given value is a function or a function of a given arity.
|
||||
"""
|
||||
|
||||
@type information ::
|
||||
@@ -134,4 +166,27 @@ defmodule Function do
|
||||
@doc since: "1.7.0"
|
||||
@spec info(fun, item) :: {item, term} when item: information
|
||||
def info(fun, item), do: :erlang.fun_info(fun, item)
|
||||
|
||||
@doc """
|
||||
Returns its input `value`. This function can be passed as an anonymous function
|
||||
to transformation functions.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Function.identity("Hello world!")
|
||||
"Hello world!"
|
||||
|
||||
iex> 'abcdaabccc' |> Enum.sort() |> Enum.chunk_by(&Function.identity/1)
|
||||
['aaa', 'bb', 'cccc', 'd']
|
||||
|
||||
iex> Enum.group_by('abracadabra', &Function.identity/1)
|
||||
%{97 => 'aaaaa', 98 => 'bb', 99 => 'c', 100 => 'd', 114 => 'rr'}
|
||||
|
||||
iex> Enum.map([1, 2, 3, 4], &Function.identity/1)
|
||||
[1, 2, 3, 4]
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec identity(value) :: value when value: var
|
||||
def identity(value), do: value
|
||||
end
|
||||
|
||||
@@ -142,7 +142,6 @@ defmodule GenServer do
|
||||
The generated `child_spec/1` can be customized with the following options:
|
||||
|
||||
* `:id` - the child specification identifier, defaults to the current module
|
||||
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
|
||||
* `:restart` - when the child should be restarted, defaults to `:permanent`
|
||||
* `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down
|
||||
|
||||
@@ -630,7 +629,7 @@ defmodule GenServer do
|
||||
Therefore it is not guaranteed that `c:terminate/2` is called when a `GenServer`
|
||||
exits. For such reasons, we usually recommend important clean-up rules to
|
||||
happen in separated processes either by use of monitoring or by links
|
||||
themselves. There is no cleanup needed when the `GenServer` controls a `port` (e.g.
|
||||
themselves. There is no cleanup needed when the `GenServer` controls a `port` (for example,
|
||||
`:gen_tcp.socket`) or `t:File.io_device/0`, because these will be closed on
|
||||
receiving a `GenServer`'s exit signal and do not need to be closed manually
|
||||
in `c:terminate/2`.
|
||||
@@ -738,7 +737,7 @@ defmodule GenServer do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
@behaviour GenServer
|
||||
|
||||
if Module.get_attribute(__MODULE__, :doc) == nil do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -786,8 +785,22 @@ defmodule GenServer do
|
||||
{_, name} -> name
|
||||
end
|
||||
|
||||
pattern = '~p ~p received unexpected message in handle_info/2: ~p~n'
|
||||
:error_logger.error_msg(pattern, [__MODULE__, proc, msg])
|
||||
:logger.error(
|
||||
%{
|
||||
label: {GenServer, :no_handle_info},
|
||||
report: %{
|
||||
module: __MODULE__,
|
||||
message: msg,
|
||||
name: proc
|
||||
}
|
||||
},
|
||||
%{
|
||||
domain: [:otp, :elixir],
|
||||
error_logger: %{tag: :error_msg},
|
||||
report_cb: &GenServer.format_report/1
|
||||
}
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -1206,4 +1219,12 @@ defmodule GenServer do
|
||||
def whereis({name, node} = server) when is_atom(name) and is_atom(node) do
|
||||
server
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_report(%{
|
||||
label: {GenServer, :no_handle_info},
|
||||
report: %{module: mod, message: msg, name: proc}
|
||||
}) do
|
||||
{'~p ~p received unexpected message in handle_info/2: ~p~n', [mod, proc, msg]}
|
||||
end
|
||||
end
|
||||
|
||||
+36
-20
@@ -32,11 +32,10 @@ defprotocol Inspect do
|
||||
end
|
||||
|
||||
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from `Inspect.Algebra` and it
|
||||
concatenates algebra documents together. In the example above,
|
||||
it is concatenating the string `"MapSet<"` (all strings are
|
||||
valid algebra documents that keep their formatting when pretty
|
||||
printed), the document returned by `Inspect.Algebra.to_doc/2` and the
|
||||
other string `">"`.
|
||||
concatenates algebra documents together. In the example above it is
|
||||
concatenating the string `"MapSet<"`, the document returned by
|
||||
`Inspect.Algebra.to_doc/2`, and the final string `">"`. All strings are
|
||||
valid algebra documents that keep their formatting when pretty printed.
|
||||
|
||||
Since regular strings are valid entities in an algebra document,
|
||||
an implementation of the `Inspect` protocol may simply return a
|
||||
@@ -253,7 +252,7 @@ defimpl Inspect, for: Map do
|
||||
end
|
||||
|
||||
def inspect(map, name, opts) do
|
||||
map = :maps.to_list(map)
|
||||
map = Map.to_list(map)
|
||||
open = color("%" <> name <> "{", :map, opts)
|
||||
sep = color(",", :map, opts)
|
||||
close = color("}", :map, opts)
|
||||
@@ -409,11 +408,7 @@ end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
defmacro __deriving__(module, struct, options) do
|
||||
fields =
|
||||
struct
|
||||
|> Map.drop([:__exception__, :__struct__])
|
||||
|> Map.keys()
|
||||
|
||||
fields = Map.keys(struct) -- [:__exception__, :__struct__]
|
||||
only = Keyword.get(options, :only, fields)
|
||||
except = Keyword.get(options, :except, [])
|
||||
|
||||
@@ -424,17 +419,17 @@ defimpl Inspect, for: Any do
|
||||
|
||||
inspect_module =
|
||||
if fields == only and except == [] do
|
||||
quote(do: Inspect.Map)
|
||||
Inspect.Map
|
||||
else
|
||||
quote(do: Inspect.Any)
|
||||
Inspect.Any
|
||||
end
|
||||
|
||||
quote do
|
||||
defimpl Inspect, for: unquote(module) do
|
||||
def inspect(struct, opts) do
|
||||
map = Map.take(struct, unquote(filtered_fields))
|
||||
name = Identifier.inspect_as_atom(unquote(module))
|
||||
unquote(inspect_module).inspect(map, name, opts)
|
||||
def inspect(var!(struct), var!(opts)) do
|
||||
var!(map) = Map.take(var!(struct), unquote(filtered_fields))
|
||||
var!(name) = Identifier.inspect_as_atom(unquote(module))
|
||||
unquote(inspect_module).inspect(var!(map), var!(name), var!(opts))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -447,8 +442,8 @@ defimpl Inspect, for: Any do
|
||||
_ -> Inspect.Map.inspect(struct, opts)
|
||||
else
|
||||
dunder ->
|
||||
if :maps.keys(dunder) == :maps.keys(struct) do
|
||||
pruned = :maps.remove(:__exception__, :maps.remove(:__struct__, struct))
|
||||
if Map.keys(dunder) == Map.keys(struct) do
|
||||
pruned = Map.drop(struct, [:__struct__, :__exception__])
|
||||
Inspect.Map.inspect(pruned, Identifier.inspect_as_atom(module), opts)
|
||||
else
|
||||
Inspect.Map.inspect(struct, opts)
|
||||
@@ -460,7 +455,7 @@ defimpl Inspect, for: Any do
|
||||
# Use the :limit option and an extra element to force
|
||||
# `container_doc/6` to append "...".
|
||||
opts = %{opts | limit: min(opts.limit, map_size(map))}
|
||||
map = :maps.to_list(map) ++ ["..."]
|
||||
map = Map.to_list(map) ++ ["..."]
|
||||
|
||||
open = color("#" <> name <> "<", :map, opts)
|
||||
sep = color(",", :map, opts)
|
||||
@@ -469,3 +464,24 @@ defimpl Inspect, for: Any do
|
||||
container_doc(open, map, close, opts, &Inspect.List.keyword/2, separator: sep, break: :strict)
|
||||
end
|
||||
end
|
||||
|
||||
require Protocol
|
||||
|
||||
Protocol.derive(
|
||||
Inspect,
|
||||
Macro.Env,
|
||||
only: [
|
||||
:module,
|
||||
:file,
|
||||
:line,
|
||||
:function,
|
||||
:context,
|
||||
:aliases,
|
||||
:requires,
|
||||
:functions,
|
||||
:macros,
|
||||
:macro_aliases,
|
||||
:context_modules,
|
||||
:lexical_tracker
|
||||
]
|
||||
)
|
||||
|
||||
@@ -4,13 +4,15 @@ defmodule Inspect.Opts do
|
||||
|
||||
The following fields are available:
|
||||
|
||||
* `:structs` - when `false`, structs are not formatted by the inspect
|
||||
protocol, they are instead printed as maps, defaults to `true`.
|
||||
* `:base` - prints integers as `:binary`, `:octal`, `:decimal`, or `:hex`,
|
||||
defaults to `:decimal`. When inspecting binaries any `:base` other than
|
||||
`:decimal` implies `binaries: :as_binaries`.
|
||||
|
||||
* `:binaries` - when `:as_strings` all binaries will be printed as strings,
|
||||
non-printable bytes will be escaped.
|
||||
* `:binaries` - when `:as_binaries` all binaries will be printed in bit
|
||||
syntax.
|
||||
|
||||
When `:as_binaries` all binaries will be printed in bit syntax.
|
||||
When `:as_strings` all binaries will be printed as strings, non-printable
|
||||
bytes will be escaped.
|
||||
|
||||
When the default `:infer`, the binary will be printed as a string if it
|
||||
is printable, otherwise in bit syntax. See `String.printable?/1` to learn
|
||||
@@ -25,81 +27,81 @@ defmodule Inspect.Opts do
|
||||
is printable, otherwise as list. See `List.ascii_printable?/1` to learn
|
||||
when a charlist is printable.
|
||||
|
||||
* `:custom_options` (since v1.9.0) - a keyword list storing custom user-defined
|
||||
options. Useful when implementing the `Inspect` protocol for nested structs
|
||||
to pass the custom options through.
|
||||
|
||||
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents,
|
||||
defaults to `Inspect.inspect/2`.
|
||||
|
||||
* `:limit` - limits the number of items that are inspected for tuples,
|
||||
bitstrings, maps, lists and any other collection of items. It does not
|
||||
apply to printable strings nor printable charlists and defaults to 50.
|
||||
If you don't want to limit the number of items to a particular number,
|
||||
use `:infinity`.
|
||||
|
||||
* `:printable_limit` - limits the number of characters that are inspected
|
||||
on printable strings and printable charlists. You can use `String.printable?/1`
|
||||
and `List.ascii_printable?/1` to check if a a given string or charlist is
|
||||
printable. Defaults to 4096. If you don't want to limit the number of
|
||||
characters to a particular number, use `:infinity`.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing, defaults to `false`.
|
||||
|
||||
* `:width` - defaults to 80 characters, used when pretty is `true` or when
|
||||
printing to IO devices. Set to 0 to force each item to be printed on its
|
||||
own line. If you don't want to limit the number of items to a particular
|
||||
number, use `:infinity`.
|
||||
|
||||
* `:base` - prints integers as `:binary`, `:octal`, `:decimal`, or `:hex`,
|
||||
defaults to `:decimal`. When inspecting binaries any `:base` other than
|
||||
`:decimal` implies `binaries: :as_binaries`.
|
||||
* `:printable_limit` - limits the number of characters that are inspected
|
||||
on printable strings and printable charlists. You can use `String.printable?/1`
|
||||
and `List.ascii_printable?/1` to check if a given string or charlist is
|
||||
printable. Defaults to 4096. If you don't want to limit the number of
|
||||
characters to a particular number, use `:infinity`.
|
||||
|
||||
* `:safe` - when `false`, failures while inspecting structs will be raised
|
||||
as errors instead of being wrapped in the `Inspect.Error` exception. This
|
||||
is useful when debugging failures and crashes for custom inspect
|
||||
implementations.
|
||||
|
||||
* `:structs` - when `false`, structs are not formatted by the inspect
|
||||
protocol, they are instead printed as maps, defaults to `true`.
|
||||
|
||||
* `:syntax_colors` - when set to a keyword list of colors the output is
|
||||
colorized. The keys are types and the values are the colors to use for
|
||||
each type (for example, `[number: :red, atom: :blue]`). Types can include
|
||||
`:number`, `:atom`, `regex`, `:tuple`, `:map`, `:list`, and `:reset`.
|
||||
`:atom`, `:binary`, `:boolean`, `:list`, `:map`, `:number`, `:regex`,
|
||||
`:string`, and `:tuple`. Custom data types may provide their own options.
|
||||
Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.
|
||||
|
||||
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents,
|
||||
defaults to `Inspect.inspect/2`
|
||||
|
||||
* `:custom_options` (since v1.9.0) - a keyword list storing custom user-defined
|
||||
options. Useful when implementing the `Inspect` protocol for nested structs
|
||||
to pass the custom options through.
|
||||
* `:width` - defaults to 80 characters, used when pretty is `true` or when
|
||||
printing to IO devices. Set to 0 to force each item to be printed on its
|
||||
own line. If you don't want to limit the number of items to a particular
|
||||
number, use `:infinity`.
|
||||
|
||||
"""
|
||||
|
||||
# TODO: Remove :char_lists key on v2.0
|
||||
defstruct structs: true,
|
||||
defstruct base: :decimal,
|
||||
binaries: :infer,
|
||||
charlists: :infer,
|
||||
char_lists: :infer,
|
||||
limit: 50,
|
||||
printable_limit: 4096,
|
||||
width: 80,
|
||||
base: :decimal,
|
||||
pretty: false,
|
||||
safe: true,
|
||||
syntax_colors: [],
|
||||
charlists: :infer,
|
||||
custom_options: [],
|
||||
inspect_fun: &Inspect.inspect/2,
|
||||
custom_options: []
|
||||
limit: 50,
|
||||
pretty: false,
|
||||
printable_limit: 4096,
|
||||
safe: true,
|
||||
structs: true,
|
||||
syntax_colors: [],
|
||||
width: 80
|
||||
|
||||
@type color_key :: atom
|
||||
|
||||
# TODO: Remove :char_lists key and :as_char_lists value on v2.0
|
||||
@type t :: %__MODULE__{
|
||||
structs: boolean,
|
||||
binaries: :infer | :as_binaries | :as_strings,
|
||||
charlists: :infer | :as_lists | :as_charlists,
|
||||
char_lists: :infer | :as_lists | :as_char_lists,
|
||||
limit: pos_integer | :infinity,
|
||||
printable_limit: pos_integer | :infinity,
|
||||
width: pos_integer | :infinity,
|
||||
base: :decimal | :binary | :hex | :octal,
|
||||
pretty: boolean,
|
||||
safe: boolean,
|
||||
syntax_colors: [{color_key, IO.ANSI.ansidata()}],
|
||||
binaries: :infer | :as_binaries | :as_strings,
|
||||
char_lists: :infer | :as_lists | :as_char_lists,
|
||||
charlists: :infer | :as_lists | :as_charlists,
|
||||
custom_options: keyword,
|
||||
inspect_fun: (any, t -> Inspect.Algebra.t()),
|
||||
custom_options: keyword
|
||||
limit: pos_integer | :infinity,
|
||||
pretty: boolean,
|
||||
printable_limit: pos_integer | :infinity,
|
||||
safe: boolean,
|
||||
structs: boolean,
|
||||
syntax_colors: [{color_key, IO.ANSI.ansidata()}],
|
||||
width: pos_integer | :infinity
|
||||
}
|
||||
end
|
||||
|
||||
@@ -191,17 +193,17 @@ defmodule Inspect.Algebra do
|
||||
|
||||
@type t ::
|
||||
binary
|
||||
| :doc_nil
|
||||
| :doc_line
|
||||
| doc_string
|
||||
| doc_cons
|
||||
| doc_nest
|
||||
| :doc_nil
|
||||
| doc_break
|
||||
| doc_group
|
||||
| doc_color
|
||||
| doc_force
|
||||
| doc_fits
|
||||
| doc_collapse
|
||||
| doc_color
|
||||
| doc_cons
|
||||
| doc_fits
|
||||
| doc_force
|
||||
| doc_group
|
||||
| doc_nest
|
||||
| doc_string
|
||||
|
||||
@typep doc_string :: {:doc_string, t, non_neg_integer}
|
||||
defmacrop doc_string(string, length) do
|
||||
@@ -249,15 +251,15 @@ defmodule Inspect.Algebra do
|
||||
end
|
||||
|
||||
@docs [
|
||||
:doc_string,
|
||||
:doc_cons,
|
||||
:doc_nest,
|
||||
:doc_break,
|
||||
:doc_group,
|
||||
:doc_collapse,
|
||||
:doc_color,
|
||||
:doc_force,
|
||||
:doc_cons,
|
||||
:doc_fits,
|
||||
:doc_collapse
|
||||
:doc_force,
|
||||
:doc_group,
|
||||
:doc_nest,
|
||||
:doc_string
|
||||
]
|
||||
|
||||
defguard is_doc(doc)
|
||||
|
||||
@@ -233,7 +233,7 @@ defmodule Integer do
|
||||
raise ArgumentError, "invalid base #{inspect(base)}"
|
||||
end
|
||||
|
||||
def parse(binary, base) do
|
||||
def parse(binary, base) when is_binary(binary) do
|
||||
case count_digits(binary, base) do
|
||||
0 ->
|
||||
:error
|
||||
@@ -244,14 +244,14 @@ defmodule Integer do
|
||||
end
|
||||
end
|
||||
|
||||
defp count_digits(<<sign, rest::binary>>, base) when sign in '+-' do
|
||||
defp count_digits(<<sign, rest::bits>>, base) when sign in '+-' do
|
||||
case count_digits_nosign(rest, base, 1) do
|
||||
1 -> 0
|
||||
count -> count
|
||||
end
|
||||
end
|
||||
|
||||
defp count_digits(<<rest::binary>>, base) do
|
||||
defp count_digits(<<rest::bits>>, base) do
|
||||
count_digits_nosign(rest, base, 0)
|
||||
end
|
||||
|
||||
@@ -261,13 +261,13 @@ defmodule Integer do
|
||||
char <- chars do
|
||||
digit = char + diff
|
||||
|
||||
defp count_digits_nosign(<<unquote(char), rest::binary>>, base, count)
|
||||
defp count_digits_nosign(<<unquote(char), rest::bits>>, base, count)
|
||||
when base > unquote(digit) do
|
||||
count_digits_nosign(rest, base, count + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp count_digits_nosign(<<_::binary>>, _, count), do: count
|
||||
defp count_digits_nosign(<<_::bits>>, _, count), do: count
|
||||
|
||||
# TODO: Remove Integer.to_string/1 once the minimum supported version is
|
||||
# Erlang/OTP 22, since it is covered by the now BIF Integer.to_string/2.
|
||||
|
||||
+22
-7
@@ -84,7 +84,7 @@ defmodule IO do
|
||||
Building IO data is cheaper than concatenating binaries. Concatenating multiple
|
||||
pieces of IO data just means putting them together inside a list since IO data
|
||||
can be arbitrarily nested, and that's a cheap and efficient operation. Most of
|
||||
the IO-based APIs, such as `:gen_tcp`, `IO`, etc, receive IO data and write it
|
||||
the IO-based APIs, such as `:gen_tcp` and `IO`, receive IO data and write it
|
||||
to the socket directly without converting it to binary.
|
||||
|
||||
One drawback of IO data is that you can't do things like pattern match on the
|
||||
@@ -98,14 +98,14 @@ defmodule IO do
|
||||
|
||||
Erlang and Elixir also have the idea of `t:chardata/0`. Chardata is very
|
||||
similar to IO data: the only difference is that integers in IO data represent
|
||||
bytes while integers in chardata represent Unicode codepoints. Bytes
|
||||
(`t:byte/0`) are integers in the `0..255` range, while Unicode codepoints
|
||||
bytes while integers in chardata represent Unicode code points. Bytes
|
||||
(`t:byte/0`) are integers in the `0..255` range, while Unicode code points
|
||||
(`t:char/0`) are integers in the range `0..0x10FFFF`. The `IO` module provides
|
||||
the `chardata_to_string/1` function for chardata as the "counter-part" of the
|
||||
`iodata_to_binary/1` function for IO data.
|
||||
|
||||
If you try to use `iodata_to_binary/1` on chardata, it will result in an
|
||||
argument error. For example, let's try to put a codepoint that is not
|
||||
argument error. For example, let's try to put a code point that is not
|
||||
representable with one byte, like `?π`, inside IO data:
|
||||
|
||||
iex> IO.iodata_to_binary(["The symbol for pi is: ", ?π])
|
||||
@@ -148,7 +148,7 @@ defmodule IO do
|
||||
def read(device \\ :stdio, line_or_chars)
|
||||
|
||||
def read(device, :all) do
|
||||
do_read_all(map_dev(device), "")
|
||||
do_read_all(map_dev(device), :empty)
|
||||
end
|
||||
|
||||
def read(device, :line) do
|
||||
@@ -161,12 +161,27 @@ defmodule IO do
|
||||
|
||||
defp do_read_all(mapped_dev, acc) do
|
||||
case :io.get_line(mapped_dev, "") do
|
||||
line when is_binary(line) -> do_read_all(mapped_dev, acc <> line)
|
||||
:eof -> acc
|
||||
line when is_binary(line) or is_list(line) -> do_read_all(mapped_dev, concat(acc, line))
|
||||
:eof -> read_eof(mapped_dev, acc)
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp concat(:empty, line), do: line
|
||||
defp concat(acc, line) when is_binary(acc), do: acc <> line
|
||||
defp concat(acc, line) when is_list(acc), do: acc ++ line
|
||||
|
||||
defp read_eof(device, :empty) do
|
||||
with [_ | _] = opts <- :io.getopts(device),
|
||||
false <- Keyword.get(opts, :binary, true) do
|
||||
''
|
||||
else
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp read_eof(_device, acc), do: acc
|
||||
|
||||
@doc """
|
||||
Reads from the IO `device`. The operation is Unicode unsafe.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ defmodule IO.ANSI.Docs do
|
||||
* `:doc_code` - code blocks (cyan)
|
||||
* `:doc_headings` - h1, h2, h3, h4, h5, h6 headings (yellow)
|
||||
* `:doc_metadata` - documentation metadata keys (yellow)
|
||||
* `:doc_quote` - leading quote character `> ` (light black)
|
||||
* `:doc_inline_code` - inline code (cyan)
|
||||
* `:doc_table_heading` - the style for table headings
|
||||
* `:doc_title` - top level heading (reverse, yellow)
|
||||
@@ -31,6 +32,7 @@ defmodule IO.ANSI.Docs do
|
||||
doc_code: [:cyan],
|
||||
doc_headings: [:yellow],
|
||||
doc_metadata: [:yellow],
|
||||
doc_quote: [:light_black],
|
||||
doc_inline_code: [:cyan],
|
||||
doc_table_heading: [:reverse],
|
||||
doc_title: [:reverse, :yellow],
|
||||
@@ -73,7 +75,7 @@ defmodule IO.ANSI.Docs do
|
||||
{key, value}, _printed when is_binary(value) and key in @metadata_filter ->
|
||||
label = metadata_label(key, options)
|
||||
indent = String.duplicate(" ", length_without_escape(label, 0) + 1)
|
||||
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true)
|
||||
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true, "")
|
||||
|
||||
{key, value}, _printed when is_boolean(value) and key in @metadata_filter ->
|
||||
IO.puts([metadata_label(key, options), ' ', to_string(value)])
|
||||
@@ -139,6 +141,11 @@ defmodule IO.ANSI.Docs do
|
||||
write_heading(heading, rest, text, indent, options)
|
||||
end
|
||||
|
||||
defp process([">" <> line | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
process_quote(rest, [line], indent, options)
|
||||
end
|
||||
|
||||
defp process(["" | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
process(rest, [], indent, options)
|
||||
@@ -183,6 +190,47 @@ defmodule IO.ANSI.Docs do
|
||||
process(rest, [], "", options)
|
||||
end
|
||||
|
||||
## Quotes
|
||||
|
||||
defp process_quote([], lines, indent, options) do
|
||||
write_quote(lines, indent, options, false)
|
||||
end
|
||||
|
||||
defp process_quote([">", ">" <> line | rest], lines, indent, options) do
|
||||
write_quote(lines, indent, options, true)
|
||||
write_empty_quote_line(options)
|
||||
process_quote(rest, [line], indent, options)
|
||||
end
|
||||
|
||||
defp process_quote([">" <> line | rest], lines, indent, options) do
|
||||
process_quote(rest, [line | lines], indent, options)
|
||||
end
|
||||
|
||||
defp process_quote(rest, lines, indent, options) do
|
||||
write_quote(lines, indent, options, false)
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
defp write_quote(lines, indent, options, no_wrap) do
|
||||
lines
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.reverse()
|
||||
|> write_lines(
|
||||
indent,
|
||||
options,
|
||||
no_wrap,
|
||||
quote_prefix(options)
|
||||
)
|
||||
end
|
||||
|
||||
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{IO.ANSI.reset()}"
|
||||
|
||||
defp write_empty_quote_line(options) do
|
||||
options
|
||||
|> quote_prefix()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
## Lists
|
||||
|
||||
defp process_rest(stripped, rest, count, text, indent, options) do
|
||||
@@ -267,16 +315,25 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp write_text(lines, indent, options, no_wrap) do
|
||||
write_lines(lines, indent, options, no_wrap, "")
|
||||
end
|
||||
|
||||
defp write_lines(lines, indent, options, no_wrap, prefix) do
|
||||
lines
|
||||
|> Enum.join(" ")
|
||||
|> handle_links
|
||||
|> handle_inline(options)
|
||||
|> format_text(options)
|
||||
|> String.split(@spaces)
|
||||
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap)
|
||||
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
|
||||
|
||||
unless no_wrap, do: newline_after_block()
|
||||
end
|
||||
|
||||
defp format_text(text, options) do
|
||||
text
|
||||
|> handle_links()
|
||||
|> handle_inline(options)
|
||||
end
|
||||
|
||||
## Code blocks
|
||||
|
||||
defp process_code([], code, indent, options) do
|
||||
@@ -348,17 +405,17 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
defp split_into_columns(line, options) do
|
||||
line
|
||||
|> String.trim(" ")
|
||||
|> String.trim("|")
|
||||
|> String.trim()
|
||||
|> String.split(" | ")
|
||||
|> String.split("|")
|
||||
|> Enum.map(&render_column(&1, options))
|
||||
end
|
||||
|
||||
defp render_column(col, options) do
|
||||
col =
|
||||
col
|
||||
|> String.replace("\\\|", "|")
|
||||
|> String.trim()
|
||||
|> String.replace("\\\|", "|")
|
||||
|> handle_links
|
||||
|> handle_inline(options)
|
||||
|
||||
@@ -450,7 +507,7 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp table_line?(line) do
|
||||
line =~ " | "
|
||||
line =~ ~r/[:\ -]\|[:\ -]/
|
||||
end
|
||||
|
||||
## Helpers
|
||||
@@ -470,14 +527,27 @@ defmodule IO.ANSI.Docs do
|
||||
IO.puts([color(style, options), string, IO.ANSI.reset()])
|
||||
end
|
||||
|
||||
defp write_with_wrap([], _available, _indent, _first) do
|
||||
defp write_with_wrap([], _available, _indent, _first, _prefix) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_with_wrap(words, available, indent, first) do
|
||||
{words, rest} = take_words(words, available, [])
|
||||
IO.puts(if(first, do: "", else: indent) <> Enum.join(words, " "))
|
||||
write_with_wrap(rest, available, indent, false)
|
||||
defp write_with_wrap(words, available, indent, first, prefix) do
|
||||
words
|
||||
|> wrap_text(available, indent, first, prefix, [])
|
||||
|> Enum.join("\n")
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
defp wrap_text([], _available, _indent, _first, _prefix, wrapped_lines) do
|
||||
Enum.reverse(wrapped_lines)
|
||||
end
|
||||
|
||||
defp wrap_text(words, available, indent, first, prefix, wrapped_lines) do
|
||||
prefix_length = length_without_escape(prefix, 0)
|
||||
{words, rest} = take_words(words, available - prefix_length, [])
|
||||
line = [if(first, do: "", else: indent), prefix, Enum.join(words, " ")]
|
||||
|
||||
wrap_text(rest, available, indent, false, prefix, [line | wrapped_lines])
|
||||
end
|
||||
|
||||
defp take_words([word | words], available, acc) do
|
||||
|
||||
+330
-135
@@ -13,13 +13,13 @@ defmodule Kernel do
|
||||
It mainly consists of:
|
||||
|
||||
* basic language primitives, such as arithmetic operators, spawning of processes,
|
||||
data type handling, etc.
|
||||
* macros for control-flow and defining new functionality (modules, functions, and so on)
|
||||
data type handling, and others
|
||||
* macros for control-flow and defining new functionality (modules, functions, and the like)
|
||||
* guard checks for augmenting pattern matching
|
||||
|
||||
You can use `Kernel` functions/macros without the `Kernel` prefix anywhere in
|
||||
Elixir code as all its functions and macros are automatically imported. For
|
||||
example, in IEx:
|
||||
You can invoke `Kernel` functions and macros anywhere in Elixir code
|
||||
without the use of the `Kernel.` prefix since they have all been
|
||||
automatically imported. For example, in IEx, you can call:
|
||||
|
||||
iex> is_number(13)
|
||||
true
|
||||
@@ -130,11 +130,12 @@ defmodule Kernel do
|
||||
* [Compatibility and Deprecations](compatibility-and-deprecations.html) - lists
|
||||
compatibility between every Elixir version and Erlang/OTP, release schema;
|
||||
lists all deprecated functions, when they were deprecated and alternatives
|
||||
* [Guards](guards.html) - an introduction to guards and extensions
|
||||
* [Library Guidelines](library-guidelines.html) - general guidelines, anti-patterns,
|
||||
and rules for those writing libraries
|
||||
* [Naming Conventions](naming-conventions.html) - naming conventions for Elixir code
|
||||
* [Operators](operators.html) - lists all Elixir operators and their precedence
|
||||
* [Patterns and Guards](patterns-and-guards.html) - an introduction to patterns,
|
||||
guards, and extensions
|
||||
* [Syntax Reference](syntax-reference.html) - the language syntax reference
|
||||
* [Typespecs](typespecs.html)- types and function specifications, including list of types
|
||||
* [Unicode Syntax](unicode-syntax.html) - outlines Elixir support for Unicode
|
||||
@@ -152,8 +153,10 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
The clause above will only be invoked if the user's age is more than
|
||||
or equal to 16. A more complete introduction to guards is available
|
||||
[in the Guards page](guards.html).
|
||||
or equal to 16. Guards also support joining multiple conditions with
|
||||
`and` and `or`. The whole guard is true if all guard expressions will
|
||||
evaluate to `true`. A more complete introduction to guards is available
|
||||
[in the "Patterns and Guards" page](patterns-and-guards.html).
|
||||
|
||||
## Inlining
|
||||
|
||||
@@ -176,7 +179,7 @@ defmodule Kernel do
|
||||
|
||||
## Truthy and falsy values
|
||||
|
||||
Besides the booleans `true` and `false` Elixir also has the
|
||||
Besides the booleans `true` and `false`, Elixir has the
|
||||
concept of a "truthy" or "falsy" value.
|
||||
|
||||
* a value is truthy when it is neither `false` nor `nil`
|
||||
@@ -393,7 +396,7 @@ defmodule Kernel do
|
||||
#=> -49
|
||||
|
||||
div(100, 0)
|
||||
#=> ** (ArithmeticError) bad argument in arithmetic expression
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@@ -437,7 +440,7 @@ defmodule Kernel do
|
||||
|
||||
Exiting with any other reason is considered abnormal and treated
|
||||
as a crash. This means the default supervisor behaviour kicks in,
|
||||
error reports are emitted, etc.
|
||||
error reports are emitted, and so forth.
|
||||
|
||||
This behaviour is relied on in many different places. For example,
|
||||
`ExUnit` uses `exit(:shutdown)` when exiting the test process to
|
||||
@@ -495,7 +498,7 @@ defmodule Kernel do
|
||||
#=> 1
|
||||
|
||||
hd([])
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
hd([1 | 2])
|
||||
#=> 1
|
||||
@@ -701,6 +704,19 @@ defmodule Kernel do
|
||||
:erlang.is_map(term)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if `key` is a key in `map`; otherwise returns `false`.
|
||||
|
||||
It raises `BadMapError` if the first element is not a map.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
"""
|
||||
@doc guard: true, since: "1.10.0"
|
||||
@spec is_map_key(map, term) :: boolean
|
||||
def is_map_key(map, key) do
|
||||
:erlang.is_map_key(key, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the length of `list`.
|
||||
|
||||
@@ -899,8 +915,7 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec round(float) :: integer
|
||||
@spec round(value) :: value when value: integer
|
||||
@spec round(number) :: integer
|
||||
def round(number) do
|
||||
:erlang.round(number)
|
||||
end
|
||||
@@ -1116,7 +1131,7 @@ defmodule Kernel do
|
||||
#=> [2, 3, :go]
|
||||
|
||||
tl([])
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
tl([:one])
|
||||
#=> []
|
||||
@@ -1153,8 +1168,7 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec trunc(value) :: value when value: integer
|
||||
@spec trunc(float) :: integer
|
||||
@spec trunc(number) :: integer
|
||||
def trunc(number) do
|
||||
:erlang.trunc(number)
|
||||
end
|
||||
@@ -1230,7 +1244,8 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec +value :: value when value: number
|
||||
@spec +integer :: integer
|
||||
@spec +float :: float
|
||||
def +value do
|
||||
:erlang.+(value)
|
||||
end
|
||||
@@ -1297,7 +1312,7 @@ defmodule Kernel do
|
||||
#=> 5.0
|
||||
|
||||
7 / 0
|
||||
#=> ** (ArithmeticError) bad argument in arithmetic expression
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@@ -1310,7 +1325,7 @@ defmodule Kernel do
|
||||
Concatenates a proper list and a term, returning a list.
|
||||
|
||||
The complexity of `a ++ b` is proportional to `length(a)`, so avoid repeatedly
|
||||
appending to lists of arbitrary length, e.g. `list ++ [element]`.
|
||||
appending to lists of arbitrary length, for example, `list ++ [element]`.
|
||||
Instead, consider prepending via `[element | rest]` and then reversing.
|
||||
|
||||
If the `right` operand is not a proper list, it returns an improper list.
|
||||
@@ -1578,10 +1593,10 @@ defmodule Kernel do
|
||||
#=> :bar
|
||||
|
||||
elem({}, 0)
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
elem({:foo, :bar}, 2)
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@@ -1802,8 +1817,8 @@ defmodule Kernel do
|
||||
defp invalid_concat_left_argument_error(arg) do
|
||||
:erlang.error(
|
||||
ArgumentError.exception(
|
||||
"the left argument of <> operator inside a match should be always a literal " <>
|
||||
"binary as its size can't be verified, got: #{arg}"
|
||||
"the left argument of <> operator inside a match should always be a literal " <>
|
||||
"binary because its size can't be verified. Got: #{arg}"
|
||||
)
|
||||
)
|
||||
end
|
||||
@@ -1912,26 +1927,31 @@ defmodule Kernel do
|
||||
defmacro reraise(message, stacktrace) do
|
||||
# Try to figure out the type at compilation time
|
||||
# to avoid dead code and make Dialyzer happy.
|
||||
|
||||
case Macro.expand(message, __CALLER__) do
|
||||
message when is_binary(message) ->
|
||||
quote do
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
:erlang.error(
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
)
|
||||
end
|
||||
|
||||
{:<<>>, _, _} = message ->
|
||||
quote do
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
:erlang.error(
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
)
|
||||
end
|
||||
|
||||
alias when is_atom(alias) ->
|
||||
quote do
|
||||
:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace))
|
||||
:erlang.error(:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace)))
|
||||
end
|
||||
|
||||
message ->
|
||||
quote do
|
||||
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
|
||||
:erlang.error(
|
||||
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2068,7 +2088,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates and updates structs.
|
||||
Creates and updates a struct.
|
||||
|
||||
The `struct` argument may be an atom (which defines `defstruct`)
|
||||
or a `struct` itself. The second argument is any `Enumerable` that
|
||||
@@ -2076,7 +2096,8 @@ defmodule Kernel do
|
||||
|
||||
Keys in the `Enumerable` that don't exist in the struct are automatically
|
||||
discarded. Note that keys must be atoms, as only atoms are allowed when
|
||||
defining a struct.
|
||||
defining a struct. If keys in the `Enumerable` are duplicated, the last
|
||||
entry will be taken (same behaviour as `Map.new/1`).
|
||||
|
||||
This function is useful for dynamically creating and updating structs, as
|
||||
well as for converting maps to structs; in the latter case, just inserting
|
||||
@@ -2193,6 +2214,42 @@ defmodule Kernel do
|
||||
:erlang.error(ArgumentError.exception(error_message))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is a struct; otherwise returns `false`.
|
||||
|
||||
Allowed in guard tests.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_struct(URI.parse("/"))
|
||||
true
|
||||
|
||||
iex> is_struct(%{})
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0", guard: true
|
||||
defmacro is_struct(term) do
|
||||
case __CALLER__.context do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(term) do
|
||||
%_{} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
:match ->
|
||||
invalid_match!(:is_struct)
|
||||
|
||||
:guard ->
|
||||
quote do
|
||||
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
is_atom(:erlang.map_get(:__struct__, unquote(term)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a value from a nested structure.
|
||||
|
||||
@@ -2206,8 +2263,7 @@ defmodule Kernel do
|
||||
iex> get_in(users, ["john", :age])
|
||||
27
|
||||
|
||||
In case any of the entries in the middle returns `nil`, `nil` will
|
||||
be returned as per the `Access` module:
|
||||
In case any of the keys returns `nil`, `nil` will be returned:
|
||||
|
||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||
iex> get_in(users, ["unknown", :age])
|
||||
@@ -2223,9 +2279,6 @@ defmodule Kernel do
|
||||
* a function to be invoked next
|
||||
|
||||
This means `get_in/2` can be extended to provide custom lookups.
|
||||
The downside is that functions cannot be stored as keys in the accessed
|
||||
data structures.
|
||||
|
||||
In the example below, we use a function to get all the maps inside
|
||||
a list:
|
||||
|
||||
@@ -2568,7 +2621,7 @@ defmodule Kernel do
|
||||
get_and_update_in(struct.foo.bar, &{&1, &1 + 1})
|
||||
|
||||
Note that in order for this macro to work, the complete path must always
|
||||
be visible by this macro. See the Paths section below.
|
||||
be visible by this macro. See the "Paths" section below.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -2781,9 +2834,6 @@ defmodule Kernel do
|
||||
iex> match?(1, 1)
|
||||
true
|
||||
|
||||
iex> match?(1, 2)
|
||||
false
|
||||
|
||||
iex> match?({1, _}, {1, 2})
|
||||
true
|
||||
|
||||
@@ -2818,15 +2868,17 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
defmacro match?(pattern, expr) do
|
||||
quote do
|
||||
case unquote(expr) do
|
||||
unquote(pattern) ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
success =
|
||||
quote do
|
||||
unquote(pattern) -> true
|
||||
end
|
||||
end
|
||||
|
||||
failure =
|
||||
quote generated: true do
|
||||
_ -> false
|
||||
end
|
||||
|
||||
{:case, [], [expr, [do: success ++ failure]]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -2845,7 +2897,8 @@ defmodule Kernel do
|
||||
|
||||
defmodule MyServer do
|
||||
@my_data 13
|
||||
IO.inspect(@my_data) #=> 13
|
||||
IO.inspect(@my_data)
|
||||
#=> 13
|
||||
end
|
||||
|
||||
Unlike Erlang, such attributes are not stored in the module by default since
|
||||
@@ -3246,7 +3299,7 @@ defmodule Kernel do
|
||||
|
||||
@doc """
|
||||
Provides a short-circuit operator that evaluates and returns
|
||||
the second expression only if the first one evaluates to to a truthy value
|
||||
the second expression only if the first one evaluates to a truthy value
|
||||
(neither `false` nor `nil`). Returns the first expression
|
||||
otherwise.
|
||||
|
||||
@@ -3402,18 +3455,6 @@ defmodule Kernel do
|
||||
[{h, _} | t] = Macro.unpipe({:|>, [], [left, right]})
|
||||
|
||||
fun = fn {x, pos}, acc ->
|
||||
case x do
|
||||
{op, _, [_]} when op == :+ or op == :- ->
|
||||
message =
|
||||
<<"piping into a unary operator is deprecated, please use the ",
|
||||
"qualified name. For example, Kernel.+(5), instead of +5">>
|
||||
|
||||
IO.warn(message, Macro.Env.stacktrace(__CALLER__))
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
Macro.pipe(acc, x, pos)
|
||||
end
|
||||
|
||||
@@ -3800,11 +3841,17 @@ defmodule Kernel do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> defmodule Foo do
|
||||
...> def bar, do: :baz
|
||||
...> end
|
||||
iex> Foo.bar()
|
||||
:baz
|
||||
defmodule Number do
|
||||
def one, do: 1
|
||||
def two, do: 2
|
||||
end
|
||||
#=> {:module, Number, <<70, 79, 82, ...>>, {:two, 0}}
|
||||
|
||||
Number.one()
|
||||
#=> 1
|
||||
|
||||
Number.two()
|
||||
#=> 2
|
||||
|
||||
## Nesting
|
||||
|
||||
@@ -3861,7 +3908,7 @@ defmodule Kernel do
|
||||
defmodule Any do
|
||||
# code
|
||||
end
|
||||
#=> ** (CompileError) iex:1: module Any is reserved and cannot be defined
|
||||
** (CompileError) iex:1: module Any is reserved and cannot be defined
|
||||
|
||||
Elixir reserves the following module names: `Elixir`, `Any`, `BitString`,
|
||||
`PID`, and `Reference`.
|
||||
@@ -3911,8 +3958,7 @@ defmodule Kernel do
|
||||
:elixir_quote.escape(block, :default, false)
|
||||
end
|
||||
|
||||
# We reimplement Macro.Env.vars/1 due to bootstrap concerns.
|
||||
module_vars = module_vars(:maps.keys(env.current_vars), 0)
|
||||
module_vars = :lists.map(&module_var/1, :maps.keys(elem(env.current_vars, 0)))
|
||||
|
||||
quote do
|
||||
unquote(with_alias)
|
||||
@@ -3947,25 +3993,11 @@ defmodule Kernel do
|
||||
{module, module, nil}
|
||||
end
|
||||
|
||||
# quote vars to be injected into the module definition
|
||||
defp module_vars([{key, kind} | vars], counter) do
|
||||
var =
|
||||
case is_atom(kind) do
|
||||
true -> {key, [generated: true], kind}
|
||||
false -> {key, [counter: kind, generated: true], nil}
|
||||
end
|
||||
|
||||
under = String.to_atom(<<"_@", :erlang.integer_to_binary(counter)::binary>>)
|
||||
args = [key, kind, under, var]
|
||||
[{:{}, [], args} | module_vars(vars, counter + 1)]
|
||||
end
|
||||
|
||||
defp module_vars([], _counter) do
|
||||
[]
|
||||
end
|
||||
defp module_var({name, kind}) when is_atom(kind), do: {name, [generated: true], kind}
|
||||
defp module_var({name, kind}), do: {name, [counter: kind, generated: true], nil}
|
||||
|
||||
@doc ~S"""
|
||||
Defines a function with the given name and body.
|
||||
Defines a public function with the given name and body.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -4072,7 +4104,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro def(call, expr \\ nil) do
|
||||
defmacro def(call, expr \\ []) do
|
||||
define(:def, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4099,15 +4131,15 @@ defmodule Kernel do
|
||||
#=> 3
|
||||
|
||||
Foo.sum(1, 2)
|
||||
#=> ** (UndefinedFunctionError) undefined function Foo.sum/2
|
||||
** (UndefinedFunctionError) undefined function Foo.sum/2
|
||||
|
||||
"""
|
||||
defmacro defp(call, expr \\ nil) do
|
||||
defmacro defp(call, expr \\ []) do
|
||||
define(:defp, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a macro with the given name and body.
|
||||
Defines a public macro with the given name and body.
|
||||
|
||||
Macros must be defined before its usage.
|
||||
|
||||
@@ -4130,7 +4162,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro defmacro(call, expr \\ nil) do
|
||||
defmacro defmacro(call, expr \\ []) do
|
||||
define(:defmacro, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4146,7 +4178,7 @@ defmodule Kernel do
|
||||
naming and default arguments.
|
||||
|
||||
"""
|
||||
defmacro defmacrop(call, expr \\ nil) do
|
||||
defmacro defmacrop(call, expr \\ []) do
|
||||
define(:defmacrop, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4241,7 +4273,7 @@ defmodule Kernel do
|
||||
defstruct name: nil, age: 10 + 11
|
||||
end
|
||||
|
||||
MyProtocol.call(john) #=> works
|
||||
MyProtocol.call(john) # it works!
|
||||
|
||||
For each protocol in the `@derive` list, Elixir will assert the protocol has
|
||||
been implemented for `Any`. If the `Any` implementation defines a
|
||||
@@ -4342,7 +4374,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
quote do
|
||||
if Module.get_attribute(__MODULE__, :struct) do
|
||||
if Module.has_attribute?(__MODULE__, :struct) do
|
||||
raise ArgumentError,
|
||||
"defstruct has already been called for " <>
|
||||
"#{Kernel.inspect(__MODULE__)}, defstruct can only be called once per module"
|
||||
@@ -4433,14 +4465,18 @@ defmodule Kernel do
|
||||
defoverridable message: 1
|
||||
|
||||
@impl true
|
||||
def exception(msg) when is_binary(msg) do
|
||||
def exception(msg) when Kernel.is_binary(msg) do
|
||||
exception(message: msg)
|
||||
end
|
||||
end
|
||||
|
||||
# Calls to Kernel functions must be fully-qualified to ensure
|
||||
# reproducible builds; otherwise, this macro will generate ASTs
|
||||
# with different metadata (:import, :context) depending on if
|
||||
# it is the bootstrapped version or not.
|
||||
# TODO: Change the implementation on v2.0 to simply call Kernel.struct!/2
|
||||
@impl true
|
||||
def exception(args) when is_list(args) do
|
||||
def exception(args) when Kernel.is_list(args) do
|
||||
struct = __struct__()
|
||||
{valid, invalid} = Enum.split_with(args, fn {k, _} -> Map.has_key?(struct, k) end)
|
||||
|
||||
@@ -4451,9 +4487,9 @@ defmodule Kernel do
|
||||
_ ->
|
||||
IO.warn(
|
||||
"the following fields are unknown when raising " <>
|
||||
"#{inspect(__MODULE__)}: #{inspect(invalid)}. " <>
|
||||
"#{Kernel.inspect(__MODULE__)}: #{Kernel.inspect(invalid)}. " <>
|
||||
"Please make sure to only give known fields when raising " <>
|
||||
"or redefine #{inspect(__MODULE__)}.exception/1 to " <>
|
||||
"or redefine #{Kernel.inspect(__MODULE__)}.exception/1 to " <>
|
||||
"discard unknown fields. Future Elixir versions will raise on " <>
|
||||
"unknown fields given to raise/2"
|
||||
)
|
||||
@@ -4637,7 +4673,7 @@ defmodule Kernel do
|
||||
macro_definition =
|
||||
case impls do
|
||||
[] ->
|
||||
define(kind, call, nil, env)
|
||||
define(kind, call, [], env)
|
||||
|
||||
[guard] ->
|
||||
quoted =
|
||||
@@ -4880,16 +4916,24 @@ defmodule Kernel do
|
||||
@doc ~S"""
|
||||
Handles the sigil `~S` for strings.
|
||||
|
||||
It simply returns a string without escaping characters and without
|
||||
interpolations.
|
||||
It returns a string without interpolations and without escape
|
||||
characters, except for the escaping of the closing sigil character
|
||||
itself.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> ~S(foo)
|
||||
"foo"
|
||||
|
||||
iex> ~S(f#{o}o)
|
||||
"f\#{o}o"
|
||||
iex> ~S(\o/)
|
||||
"\\o/"
|
||||
|
||||
However, if you want to re-use the sigil character itself on
|
||||
the string, you need to escape it:
|
||||
|
||||
iex> ~S((\))
|
||||
"()"
|
||||
|
||||
"""
|
||||
defmacro sigil_S(term, modifiers)
|
||||
@@ -4926,8 +4970,9 @@ defmodule Kernel do
|
||||
@doc ~S"""
|
||||
Handles the sigil `~C` for charlists.
|
||||
|
||||
It simply returns a charlist without escaping characters and without
|
||||
interpolations.
|
||||
It returns a charlist without interpolations and without escape
|
||||
characters, except for the escaping of the closing sigil character
|
||||
itself.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -5007,8 +5052,10 @@ defmodule Kernel do
|
||||
@doc ~S"""
|
||||
Handles the sigil `~R` for regular expressions.
|
||||
|
||||
It returns a regular expression pattern without escaping
|
||||
nor interpreting interpolations.
|
||||
It returns a regular expression pattern without interpolations and
|
||||
without escape characters. Note it still supports escape of Regex
|
||||
tokens (such as escaping `+` or `?`) and it also requires you to
|
||||
escape the closing sigil character itself if it appears on the Regex.
|
||||
|
||||
More information on regexes can be found in the `Regex` module.
|
||||
|
||||
@@ -5028,6 +5075,21 @@ defmodule Kernel do
|
||||
@doc ~S"""
|
||||
Handles the sigil `~D` for dates.
|
||||
|
||||
By default, this sigil uses the built-in `Calendar.ISO`, which
|
||||
requires dates to be written in the ISO8601 format:
|
||||
|
||||
~D[yyyy-mm-dd]
|
||||
|
||||
such as:
|
||||
|
||||
~D[2015-01-13]
|
||||
|
||||
If you are using alternative calendars, any representation can
|
||||
be used as long as you follow the representation by a single space
|
||||
and the calendar name:
|
||||
|
||||
~D[SOME-REPRESENTATION My.Alternative.Calendar]
|
||||
|
||||
The lower case `~d` variant does not exist as interpolation
|
||||
and escape characters are not useful for date sigils.
|
||||
|
||||
@@ -5042,12 +5104,30 @@ defmodule Kernel do
|
||||
defmacro sigil_D(date_string, modifiers)
|
||||
|
||||
defmacro sigil_D({:<<>>, _, [string]}, []) do
|
||||
Macro.escape(Date.from_iso8601!(string))
|
||||
{{:ok, {year, month, day}}, calendar} = parse_with_calendar!(string, :parse_date, "Date")
|
||||
to_calendar_struct(Date, calendar: calendar, year: year, month: month, day: day)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Handles the sigil `~T` for times.
|
||||
|
||||
By default, this sigil uses the built-in `Calendar.ISO`, which
|
||||
requires times to be written in the ISO8601 format:
|
||||
|
||||
~T[hh:mm:ss]
|
||||
~T[hh:mm:ss.ssssss]
|
||||
|
||||
such as:
|
||||
|
||||
~T[13:00:07]
|
||||
~T[13:00:07.123]
|
||||
|
||||
If you are using alternative calendars, any representation can
|
||||
be used as long as you follow the representation by a single space
|
||||
and the calendar name:
|
||||
|
||||
~T[SOME-REPRESENTATION My.Alternative.Calendar]
|
||||
|
||||
The lower case `~t` variant does not exist as interpolation
|
||||
and escape characters are not useful for time sigils.
|
||||
|
||||
@@ -5064,16 +5144,44 @@ defmodule Kernel do
|
||||
defmacro sigil_T(time_string, modifiers)
|
||||
|
||||
defmacro sigil_T({:<<>>, _, [string]}, []) do
|
||||
Macro.escape(Time.from_iso8601!(string))
|
||||
{{:ok, {hour, minute, second, microsecond}}, calendar} =
|
||||
parse_with_calendar!(string, :parse_time, "Time")
|
||||
|
||||
to_calendar_struct(Time,
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Handles the sigil `~N` for naive date times.
|
||||
|
||||
By default, this sigil uses the built-in `Calendar.ISO`, which
|
||||
requires naive date times to be written in the ISO8601 format:
|
||||
|
||||
~N[yyyy-mm-dd hh:mm:ss]
|
||||
~N[yyyy-mm-dd hh:mm:ss.ssssss]
|
||||
~N[yyyy-mm-ddThh:mm:ss.ssssss]
|
||||
|
||||
such as:
|
||||
|
||||
~N[2015-01-13 13:00:07]
|
||||
~N[2015-01-13T13:00:07.123]
|
||||
|
||||
If you are using alternative calendars, any representation can
|
||||
be used as long as you follow the representation by a single space
|
||||
and the calendar name:
|
||||
|
||||
~N[SOME-REPRESENTATION My.Alternative.Calendar]
|
||||
|
||||
The lower case `~n` variant does not exist as interpolation
|
||||
and escape characters are not useful for date time sigils.
|
||||
|
||||
More information on naive date times can be found in the `NaiveDateTime` module.
|
||||
More information on naive date times can be found in the
|
||||
`NaiveDateTime` module.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -5086,18 +5194,48 @@ defmodule Kernel do
|
||||
defmacro sigil_N(naive_datetime_string, modifiers)
|
||||
|
||||
defmacro sigil_N({:<<>>, _, [string]}, []) do
|
||||
Macro.escape(NaiveDateTime.from_iso8601!(string))
|
||||
{{:ok, {year, month, day, hour, minute, second, microsecond}}, calendar} =
|
||||
parse_with_calendar!(string, :parse_naive_datetime, "NaiveDateTime")
|
||||
|
||||
to_calendar_struct(NaiveDateTime,
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Handles the sigil `~U` to create a UTC `DateTime`.
|
||||
|
||||
By default, this sigil uses the built-in `Calendar.ISO`, which
|
||||
requires UTC date times to be written in the ISO8601 format:
|
||||
|
||||
~U[yyyy-mm-dd hh:mm:ssZ]
|
||||
~U[yyyy-mm-dd hh:mm:ss.ssssssZ]
|
||||
~U[yyyy-mm-ddThh:mm:ss.ssssss+00:00]
|
||||
|
||||
such as:
|
||||
|
||||
~U[2015-01-13 13:00:07Z]
|
||||
~U[2015-01-13T13:00:07.123+00:00]
|
||||
|
||||
If you are using alternative calendars, any representation can
|
||||
be used as long as you follow the representation by a single space
|
||||
and the calendar name:
|
||||
|
||||
~U[SOME-REPRESENTATION My.Alternative.Calendar]
|
||||
|
||||
The given `datetime_string` must include "Z" or "00:00" offset
|
||||
which marks it as UTC, otherwise an error is raised.
|
||||
|
||||
The lower case `~u` variant does not exist as interpolation
|
||||
and escape characters are not useful for date time sigils.
|
||||
|
||||
The given `datetime_string` must include "Z" or "00:00" offset which marks it
|
||||
as UTC, otherwise an error is raised.
|
||||
|
||||
More information on date times can be found in the `DateTime` module.
|
||||
|
||||
## Examples
|
||||
@@ -5112,21 +5250,64 @@ defmodule Kernel do
|
||||
defmacro sigil_U(datetime_string, modifiers)
|
||||
|
||||
defmacro sigil_U({:<<>>, _, [string]}, []) do
|
||||
Macro.escape(datetime_from_utc_iso8601!(string))
|
||||
{{:ok, {year, month, day, hour, minute, second, microsecond}, offset}, calendar} =
|
||||
parse_with_calendar!(string, :parse_utc_datetime, "UTC DateTime")
|
||||
|
||||
if offset != 0 do
|
||||
raise ArgumentError,
|
||||
"cannot parse #{inspect(string)} as UTC DateTime for #{inspect(calendar)}, reason: :non_utc_offset"
|
||||
end
|
||||
|
||||
to_calendar_struct(DateTime,
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
time_zone: "Etc/UTC",
|
||||
zone_abbr: "UTC",
|
||||
utc_offset: 0,
|
||||
std_offset: 0
|
||||
)
|
||||
end
|
||||
|
||||
defp datetime_from_utc_iso8601!(string) do
|
||||
case DateTime.from_iso8601(string) do
|
||||
{:ok, utc_datetime, 0} ->
|
||||
utc_datetime
|
||||
defp parse_with_calendar!(string, fun, context) do
|
||||
{calendar, string} = extract_calendar(string)
|
||||
result = apply(calendar, fun, [string])
|
||||
{maybe_raise!(result, calendar, context, string), calendar}
|
||||
end
|
||||
|
||||
{:ok, _datetime, _offset} ->
|
||||
raise ArgumentError,
|
||||
"cannot parse #{inspect(string)} as UTC datetime, reason: :non_utc_offset"
|
||||
defp extract_calendar(string) do
|
||||
case :binary.split(string, " ", [:global]) do
|
||||
[_] -> {Calendar.ISO, string}
|
||||
parts -> maybe_atomize_calendar(List.last(parts), string)
|
||||
end
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot parse #{inspect(string)} as UTC datetime, reason: #{inspect(reason)}"
|
||||
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}
|
||||
end
|
||||
|
||||
defp maybe_atomize_calendar(_last_part, string) do
|
||||
{Calendar.ISO, string}
|
||||
end
|
||||
|
||||
defp maybe_raise!({:error, reason}, calendar, type, string) do
|
||||
raise ArgumentError,
|
||||
"cannot parse #{inspect(string)} as #{type} for #{inspect(calendar)}, " <>
|
||||
"reason: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
defp maybe_raise!(other, _calendar, _type, _string), do: other
|
||||
|
||||
defp to_calendar_struct(type, fields) do
|
||||
quote do
|
||||
%{unquote_splicing([__struct__: type] ++ fields)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5160,19 +5341,20 @@ defmodule Kernel do
|
||||
defmacro sigil_w(term, modifiers)
|
||||
|
||||
defmacro sigil_w({:<<>>, _meta, [string]}, modifiers) when is_binary(string) do
|
||||
split_words(:elixir_interpolation.unescape_chars(string), modifiers)
|
||||
split_words(:elixir_interpolation.unescape_chars(string), modifiers, __CALLER__)
|
||||
end
|
||||
|
||||
defmacro sigil_w({:<<>>, meta, pieces}, modifiers) do
|
||||
binary = {:<<>>, meta, unescape_tokens(pieces)}
|
||||
split_words(binary, modifiers)
|
||||
split_words(binary, modifiers, __CALLER__)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Handles the sigil `~W` for list of words.
|
||||
|
||||
It returns a list of "words" split by whitespace without escaping nor
|
||||
interpreting interpolations.
|
||||
It returns a list of "words" split by whitespace without interpolations
|
||||
and without escape characters, except for the escaping of the closing
|
||||
sigil character itself.
|
||||
|
||||
## Modifiers
|
||||
|
||||
@@ -5189,19 +5371,32 @@ defmodule Kernel do
|
||||
defmacro sigil_W(term, modifiers)
|
||||
|
||||
defmacro sigil_W({:<<>>, _meta, [string]}, modifiers) when is_binary(string) do
|
||||
split_words(string, modifiers)
|
||||
split_words(string, modifiers, __CALLER__)
|
||||
end
|
||||
|
||||
defp split_words(string, []) do
|
||||
split_words(string, [?s])
|
||||
defp split_words(string, [], caller) do
|
||||
split_words(string, [?s], caller)
|
||||
end
|
||||
|
||||
defp split_words(string, [mod])
|
||||
defp split_words(string, [mod], caller)
|
||||
when mod == ?s or mod == ?a or mod == ?c do
|
||||
case is_binary(string) do
|
||||
true ->
|
||||
parts = String.split(string)
|
||||
|
||||
parts_with_trailing_comma =
|
||||
:lists.filter(&(byte_size(&1) > 1 and :binary.last(&1) == ?,), parts)
|
||||
|
||||
if parts_with_trailing_comma != [] do
|
||||
stacktrace = Macro.Env.stacktrace(caller)
|
||||
|
||||
IO.warn(
|
||||
"the sigils ~w/~W do not allow trailing commas at the end of each word. " <>
|
||||
"If the comma is necessary, define a regular list with [...], otherwise remove the comma.",
|
||||
stacktrace
|
||||
)
|
||||
end
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> :lists.map(&String.to_atom/1, parts)
|
||||
@@ -5219,7 +5414,7 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp split_words(_string, _mods) do
|
||||
defp split_words(_string, _mods, _caller) do
|
||||
raise ArgumentError, "modifier must be one of: s, a, c"
|
||||
end
|
||||
|
||||
@@ -5247,7 +5442,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/guards.html"
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
defmodule Kernel.CLI do
|
||||
@moduledoc false
|
||||
|
||||
@compile {:no_warn_undefined, [Logger, IEx]}
|
||||
|
||||
@blank_config %{
|
||||
commands: [],
|
||||
output: ".",
|
||||
@@ -10,7 +12,8 @@ defmodule Kernel.CLI do
|
||||
errors: [],
|
||||
pa: [],
|
||||
pz: [],
|
||||
verbose_compile: false
|
||||
verbose_compile: false,
|
||||
profile: nil
|
||||
}
|
||||
|
||||
@doc """
|
||||
@@ -99,7 +102,7 @@ defmodule Kernel.CLI do
|
||||
Function invoked across nodes for `--rpc-eval`.
|
||||
"""
|
||||
def rpc_eval(expr) do
|
||||
wrapper(fn -> :elixir.eval(to_charlist(expr), [], []) end)
|
||||
wrapper(fn -> Code.eval_string(expr) end)
|
||||
catch
|
||||
kind, reason -> {kind, reason, __STACKTRACE__}
|
||||
end
|
||||
@@ -354,6 +357,12 @@ defmodule Kernel.CLI do
|
||||
parse_compiler(t, %{config | verbose_compile: true})
|
||||
end
|
||||
|
||||
# Private compiler options
|
||||
|
||||
defp parse_compiler(["--profile", "time" | t], config) do
|
||||
parse_compiler(t, %{config | profile: :time})
|
||||
end
|
||||
|
||||
defp parse_compiler([h | t] = list, config) do
|
||||
case h do
|
||||
"-" <> _ ->
|
||||
@@ -487,13 +496,22 @@ defmodule Kernel.CLI do
|
||||
wrapper(fn ->
|
||||
Code.compiler_options(config.compiler_options)
|
||||
|
||||
opts =
|
||||
verbose_opts =
|
||||
if config.verbose_compile do
|
||||
[each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 15s)")]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
profile_opts =
|
||||
if config.profile do
|
||||
[profile: config.profile]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
opts = verbose_opts ++ profile_opts
|
||||
|
||||
case Kernel.ParallelCompiler.compile_to_path(files, config.output, opts) do
|
||||
{:ok, _, _} -> :ok
|
||||
{:error, _, _} -> exit({:shutdown, 1})
|
||||
|
||||
@@ -6,21 +6,14 @@
|
||||
# any of the `GenServer.Behaviour` conveniences.
|
||||
defmodule Kernel.LexicalTracker do
|
||||
@moduledoc false
|
||||
@timeout 30000
|
||||
@timeout :infinity
|
||||
@behaviour :gen_server
|
||||
|
||||
@doc """
|
||||
Returns all remotes referenced in this lexical scope.
|
||||
Returns all references in this lexical scope.
|
||||
"""
|
||||
def remote_references(pid) do
|
||||
:gen_server.call(pid, :remote_references, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all remote dispatches in this lexical scope.
|
||||
"""
|
||||
def remote_dispatches(pid) do
|
||||
:gen_server.call(pid, :remote_dispatches, @timeout)
|
||||
def references(pid) do
|
||||
:gen_server.call(pid, :references, @timeout)
|
||||
end
|
||||
|
||||
# Internal API
|
||||
@@ -33,7 +26,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc false
|
||||
def stop(pid) do
|
||||
:gen_server.cast(pid, :stop)
|
||||
:gen_server.call(pid, :stop)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -47,23 +40,18 @@ defmodule Kernel.LexicalTracker do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def remote_reference(pid, module, mode) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_reference, module, mode})
|
||||
def remote_dispatch(pid, module, mode) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_dispatch, module, mode})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def remote_dispatch(pid, module, fa, line, mode) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_dispatch, module, fa, line, mode})
|
||||
def remote_struct(pid, module) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_struct, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def remote_struct(pid, module, line) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_struct, module, line})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def import_dispatch(pid, module, fa, line, mode) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module, fa, line, mode})
|
||||
def import_dispatch(pid, module, fa) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module, fa})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -71,6 +59,11 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.cast(pid, {:alias_dispatch, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_compile_env(pid, app, path, return) do
|
||||
:gen_server.cast(pid, {:compile_env, app, path, return})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def set_file(pid, file) do
|
||||
:gen_server.cast(pid, {:set_file, file})
|
||||
@@ -113,10 +106,9 @@ defmodule Kernel.LexicalTracker do
|
||||
state = %{
|
||||
directives: %{},
|
||||
references: %{},
|
||||
compile: %{},
|
||||
runtime: %{},
|
||||
structs: %{},
|
||||
cache: %{},
|
||||
compile_env: :ordsets.new(),
|
||||
file: nil
|
||||
}
|
||||
|
||||
@@ -133,45 +125,35 @@ defmodule Kernel.LexicalTracker do
|
||||
{:reply, Enum.sort(directives), state}
|
||||
end
|
||||
|
||||
def handle_call(:remote_references, _from, state) do
|
||||
{compile, runtime} = partition(:maps.to_list(state.references), [], [])
|
||||
{:reply, {compile, :maps.keys(state.structs), runtime}, state}
|
||||
end
|
||||
|
||||
def handle_call(:remote_dispatches, _from, state) do
|
||||
{:reply, {state.compile, state.runtime}, state}
|
||||
def handle_call(:references, _from, state) do
|
||||
{compile, runtime} = partition(Map.to_list(state.references), [], [])
|
||||
{:reply, {compile, Map.keys(state.structs), runtime, state.compile_env}, state}
|
||||
end
|
||||
|
||||
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
|
||||
{:reply, :maps.get(key, cache), state}
|
||||
{:reply, Map.get(cache, key), state}
|
||||
end
|
||||
|
||||
def handle_call(:stop, _from, state) do
|
||||
{:stop, :normal, :ok, state}
|
||||
end
|
||||
|
||||
def handle_cast({:write_cache, key, value}, %{cache: cache} = state) do
|
||||
{:noreply, %{state | cache: :maps.put(key, value, cache)}}
|
||||
{:noreply, %{state | cache: Map.put(cache, key, value)}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_reference, module, mode}, state) do
|
||||
{:noreply, %{state | references: add_reference(state.references, module, mode)}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_struct, module, line}, state) do
|
||||
state = add_remote_dispatch(state, module, {:__struct__, 0}, line, :compile)
|
||||
structs = :maps.put(module, true, state.structs)
|
||||
def handle_cast({:remote_struct, module}, state) do
|
||||
structs = Map.put(state.structs, module, true)
|
||||
{:noreply, %{state | structs: structs}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_dispatch, module, fa, line, mode}, state) do
|
||||
def handle_cast({:remote_dispatch, module, mode}, state) do
|
||||
references = add_reference(state.references, module, mode)
|
||||
state = add_remote_dispatch(state, module, fa, line, mode)
|
||||
{:noreply, %{state | references: references}}
|
||||
end
|
||||
|
||||
def handle_cast({:import_dispatch, module, {function, arity} = fa, line, mode}, state) do
|
||||
state =
|
||||
state
|
||||
|> add_import_dispatch(module, function, arity)
|
||||
|> add_remote_dispatch(module, fa, line, mode)
|
||||
|
||||
def handle_cast({:import_dispatch, module, {function, arity}}, state) do
|
||||
state = add_import_dispatch(state, module, function, arity)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -187,11 +169,15 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, %{state | file: nil}}
|
||||
end
|
||||
|
||||
def handle_cast({:compile_env, app, path, return}, state) do
|
||||
{:noreply, update_in(state.compile_env, &:ordsets.add_element({app, path, return}, &1))}
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, module, fas, line, warn}, state) do
|
||||
directives =
|
||||
state.directives
|
||||
|> Enum.reject(&match?({{:import, {^module, _, _}}, _}, &1))
|
||||
|> :maps.from_list()
|
||||
|> Map.new()
|
||||
|> add_directive(module, line, warn, :import)
|
||||
|
||||
directives =
|
||||
@@ -206,10 +192,6 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, %{state | directives: add_directive(state.directives, module, line, warn, :alias)}}
|
||||
end
|
||||
|
||||
def handle_cast(:stop, state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
@@ -236,28 +218,15 @@ defmodule Kernel.LexicalTracker do
|
||||
# Callbacks helpers
|
||||
|
||||
defp add_reference(references, module, :compile) when is_atom(module),
|
||||
do: :maps.put(module, :compile, references)
|
||||
do: Map.put(references, module, :compile)
|
||||
|
||||
defp add_reference(references, module, :runtime) when is_atom(module) do
|
||||
case :maps.find(module, references) do
|
||||
case Map.fetch(references, module) do
|
||||
{:ok, _} -> references
|
||||
:error -> :maps.put(module, :runtime, references)
|
||||
:error -> Map.put(references, module, :runtime)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_remote_dispatch(state, module, fa, line, mode) when is_atom(module) do
|
||||
location = location(state.file, line)
|
||||
|
||||
map_update(mode, %{module => %{fa => [location]}}, state, fn mode_dispatches ->
|
||||
map_update(module, %{fa => [location]}, mode_dispatches, fn module_dispatches ->
|
||||
map_update(fa, [location], module_dispatches, &[location | List.delete(&1, location)])
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp location(nil, line), do: line
|
||||
defp location(file, line), do: {file, line}
|
||||
|
||||
defp add_import_dispatch(state, module, function, arity) do
|
||||
directives =
|
||||
add_dispatch(state.directives, module, :import)
|
||||
@@ -266,7 +235,6 @@ defmodule Kernel.LexicalTracker do
|
||||
# Always compile time because we depend
|
||||
# on the module at compile time
|
||||
references = add_reference(state.references, module, :compile)
|
||||
|
||||
%{state | directives: directives, references: references}
|
||||
end
|
||||
|
||||
@@ -275,17 +243,10 @@ defmodule Kernel.LexicalTracker do
|
||||
# If the value is true, it was imported/aliased and used
|
||||
defp add_directive(directives, module_or_mfa, line, warn, tag) do
|
||||
marker = if warn, do: line, else: true
|
||||
:maps.put({tag, module_or_mfa}, marker, directives)
|
||||
Map.put(directives, {tag, module_or_mfa}, marker)
|
||||
end
|
||||
|
||||
defp add_dispatch(directives, module_or_mfa, tag) do
|
||||
:maps.put({tag, module_or_mfa}, true, directives)
|
||||
end
|
||||
|
||||
defp map_update(key, initial, map, fun) do
|
||||
case :maps.find(key, map) do
|
||||
{:ok, val} -> :maps.put(key, fun.(val), map)
|
||||
:error -> :maps.put(key, initial, map)
|
||||
end
|
||||
Map.put(directives, {tag, module_or_mfa}, true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -62,16 +62,24 @@ defmodule Kernel.ParallelCompiler do
|
||||
the file, module and the module bytecode
|
||||
|
||||
* `:each_cycle` - after the given files are compiled, invokes this function
|
||||
that return a list with potentially more files to compile
|
||||
that should return the following values:
|
||||
* `{:compile, modules}` - to continue compilation with a list of further modules to compile
|
||||
* `{:runtime, modules}` - to stop compilation and verify the list of modules because
|
||||
dependent modules have changed
|
||||
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) after the
|
||||
`:each_long_compilation` callback is invoked; defaults to `15`
|
||||
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
|
||||
* `:dest` - the destination directory for the BEAM files. When using `compile/2`,
|
||||
this information is only used to properly annotate the BEAM files before
|
||||
they are loaded into memory. If you want a file to actually be written to
|
||||
`dest`, use `compile_to_path/3` instead.
|
||||
|
||||
* `:beam_timestamp` - the modification timestamp to give all BEAM files
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
def compile(files, options \\ []) when is_list(options) do
|
||||
@@ -135,12 +143,16 @@ defmodule Kernel.ParallelCompiler do
|
||||
result =
|
||||
spawn_workers(files, 0, [], [], %{}, [], %{
|
||||
dest: Keyword.get(options, :dest),
|
||||
each_cycle: Keyword.get(options, :each_cycle, fn -> [] end),
|
||||
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, []} end),
|
||||
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
|
||||
each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
|
||||
each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
|
||||
output: output,
|
||||
beam_timestamp: Keyword.get(options, :beam_timestamp),
|
||||
long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
|
||||
profile: Keyword.get(options, :profile),
|
||||
cycle_start: System.monotonic_time(),
|
||||
module_counter: 0,
|
||||
output: output,
|
||||
schedulers: schedulers
|
||||
})
|
||||
|
||||
@@ -218,25 +230,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
try do
|
||||
case output do
|
||||
{:compile, path} ->
|
||||
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
|
||||
:erlang.put(:elixir_compiler_dest, path)
|
||||
:elixir_compiler.file_to_path(file, path, &each_file(&1, &2, parent))
|
||||
|
||||
:compile ->
|
||||
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
|
||||
:erlang.put(:elixir_compiler_dest, dest)
|
||||
:elixir_compiler.file(file, &each_file(&1, &2, parent))
|
||||
|
||||
:require ->
|
||||
case :elixir_code_server.call({:acquire, file}) do
|
||||
:required ->
|
||||
send(parent, {:file_cancel, self()})
|
||||
|
||||
:proceed ->
|
||||
:elixir_compiler.file(file, &each_file(&1, &2, parent))
|
||||
:elixir_code_server.cast({:required, file})
|
||||
end
|
||||
{:compile, path} -> compile_file(file, path, parent)
|
||||
:compile -> compile_file(file, dest, parent)
|
||||
:require -> require_file(file, parent)
|
||||
end
|
||||
catch
|
||||
kind, reason ->
|
||||
@@ -253,13 +249,16 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
# No more queue, nothing waiting, this cycle is done
|
||||
defp spawn_workers([], 0, [], [], result, warnings, state) do
|
||||
case state.each_cycle.() do
|
||||
[] ->
|
||||
modules = for {{:module, mod}, _} <- result, do: mod
|
||||
warnings = Enum.reverse(warnings)
|
||||
{:ok, modules, warnings}
|
||||
state = cycle_timing(result, state)
|
||||
|
||||
more ->
|
||||
case each_cycle_return(state.each_cycle.()) do
|
||||
{:runtime, dependent_modules} ->
|
||||
write_and_verify_modules(result, warnings, dependent_modules, state)
|
||||
|
||||
{:compile, []} ->
|
||||
write_and_verify_modules(result, warnings, [], state)
|
||||
|
||||
{:compile, more} ->
|
||||
spawn_workers(more, 0, [], [], result, warnings, state)
|
||||
end
|
||||
end
|
||||
@@ -285,7 +284,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
# There is potentially a deadlock. We will release modules with
|
||||
# the following order:
|
||||
#
|
||||
# 1. Code.ensure_compiled?/1 checks (deadlock = soft)
|
||||
# 1. Code.ensure_compiled/1 checks (deadlock = soft)
|
||||
# 2. Struct checks (deadlock = hard)
|
||||
# 3. Modules without a known definition
|
||||
# 4. Code invocation (deadlock = raise)
|
||||
@@ -311,6 +310,115 @@ defmodule Kernel.ParallelCompiler do
|
||||
wait_for_messages([], spawned, waiting, files, result, warnings, state)
|
||||
end
|
||||
|
||||
defp compile_file(file, path, parent) do
|
||||
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
|
||||
:erlang.put(:elixir_compiler_dest, path)
|
||||
:elixir_compiler.file(file, &each_file(&1, &2, parent))
|
||||
end
|
||||
|
||||
defp require_file(file, parent) do
|
||||
case :elixir_code_server.call({:acquire, file}) do
|
||||
:required ->
|
||||
send(parent, {:file_cancel, self()})
|
||||
|
||||
:proceed ->
|
||||
:elixir_compiler.file(file, &each_file(&1, &2, parent))
|
||||
:elixir_code_server.cast({:required, file})
|
||||
end
|
||||
end
|
||||
|
||||
defp cycle_timing(result, %{profile: :time} = state) do
|
||||
%{cycle_start: cycle_start, module_counter: module_counter} = state
|
||||
num_modules = count_modules(result)
|
||||
diff_modules = num_modules - module_counter
|
||||
now = System.monotonic_time()
|
||||
time = System.convert_time_unit(now - cycle_start, :native, :millisecond)
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] Finished compilation cycle of #{diff_modules} modules in #{time}ms"
|
||||
)
|
||||
|
||||
%{state | cycle_start: now, module_counter: num_modules}
|
||||
end
|
||||
|
||||
defp cycle_timing(_result, %{profile: nil} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp count_modules(result) do
|
||||
Enum.count(result, &match?({{:module, _}, _}, &1))
|
||||
end
|
||||
|
||||
# TODO: Deprecate on v1.14
|
||||
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules}
|
||||
defp each_cycle_return(other), do: other
|
||||
|
||||
defp write_and_verify_modules(result, warnings, dependent_modules, state) do
|
||||
modules = write_module_binaries(result, state)
|
||||
checker_warnings = maybe_check_modules(result, dependent_modules, state)
|
||||
warnings = Enum.reverse(warnings, checker_warnings)
|
||||
{:ok, modules, warnings}
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, %{output: {:compile, path}, beam_timestamp: timestamp}) do
|
||||
Enum.flat_map(result, fn
|
||||
{{:module, module}, {binary, _map}} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
[module]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _state) do
|
||||
for {{:module, module}, _} <- result, do: module
|
||||
end
|
||||
|
||||
defp maybe_check_modules(result, runtime_modules, state) do
|
||||
%{schedulers: schedulers, profile: profile} = state
|
||||
|
||||
if :elixir_config.get(:bootstrap) do
|
||||
[]
|
||||
else
|
||||
compiled_modules = checker_compiled_modules(result)
|
||||
runtime_modules = checker_runtime_modules(runtime_modules)
|
||||
|
||||
profile_checker(profile, compiled_modules, runtime_modules, fn ->
|
||||
Module.ParallelChecker.verify(compiled_modules, runtime_modules, schedulers)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_compiled_modules(result) do
|
||||
for {{:module, _module}, {binary, module_map}} <- result do
|
||||
{module_map, binary}
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_runtime_modules(modules) do
|
||||
for module <- modules,
|
||||
path = :code.which(module),
|
||||
is_list(path) do
|
||||
{module, File.read!(path)}
|
||||
end
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = :time, compiled_modules, runtime_modules, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
time = div(time, 1000)
|
||||
num_modules = length(compiled_modules) + length(runtime_modules)
|
||||
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
|
||||
result
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = nil, _compiled_modules, _runtime_modules, fun) do
|
||||
fun.()
|
||||
end
|
||||
|
||||
# The goal of this function is to find leaves in the dependency graph,
|
||||
# i.e. to find code that depends on code that we know is not being defined.
|
||||
defp without_definition(waiting, files) do
|
||||
@@ -323,7 +431,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp deadlocked(waiting, type) do
|
||||
nillify_empty(for {_, _, ref, _, _, ^type} <- waiting, do: {ref, :not_found})
|
||||
nillify_empty(for {_, _, ref, _, _, ^type} <- waiting, do: {ref, :deadlock})
|
||||
end
|
||||
|
||||
defp nillify_empty([]), do: nil
|
||||
@@ -346,7 +454,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
result = Map.put(result, {kind, module}, true)
|
||||
spawn_workers(available ++ queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
{:module_available, child, ref, file, module, binary} ->
|
||||
{:module_available, child, ref, file, module, binary, module_map} ->
|
||||
state.each_module.(file, module, binary)
|
||||
|
||||
# Release the module loader which is waiting for an ack
|
||||
@@ -357,7 +465,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
do: {ref, :found}
|
||||
|
||||
cancel_waiting_timer(files, child)
|
||||
result = Map.put(result, {:module, module}, true)
|
||||
result = Map.put(result, {:module, module}, {binary, module_map})
|
||||
spawn_workers(available ++ queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
# If we are simply requiring files, we do not add to waiting.
|
||||
|
||||
@@ -3,14 +3,20 @@ defmodule Kernel.SpecialForms do
|
||||
Special forms are the basic building blocks of Elixir, and therefore
|
||||
cannot be overridden by the developer.
|
||||
|
||||
We define them in this module. Some of these forms are lexical (like
|
||||
`alias/2`, `case/2`, etc.). The macros `{}/1` and `<<>>/1` are also special
|
||||
The `Kernel.SpecialForms` module consists solely of macros that can be
|
||||
invoked anywhere in Elixir code without the use of the
|
||||
`Kernel.SpecialForms.` prefix. This is possible because they all have
|
||||
been automatically imported, in the same fashion as the functions and
|
||||
macros from the `Kernel` module.
|
||||
|
||||
These building blocks are defined in this module. Some of these special forms are lexical (such as
|
||||
`alias/2` and `case/2`). The macros `{}/1` and `<<>>/1` are also special
|
||||
forms used to define tuple and binary data structures respectively.
|
||||
|
||||
This module also documents macros that return information about Elixir's
|
||||
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0` and `__CALLER__/0`).
|
||||
|
||||
Finally, it also documents two special forms, `__block__/1` and
|
||||
Additionally, it documents two special forms, `__block__/1` and
|
||||
`__aliases__/1`, which are not intended to be called directly by the
|
||||
developer but they appear in quoted contents since they are essential
|
||||
in Elixir's constructs.
|
||||
@@ -344,8 +350,8 @@ defmodule Kernel.SpecialForms do
|
||||
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
|
||||
@jpg_signature <<255::size(8), 216::size(8)>>
|
||||
|
||||
def type(<<@png_signature, rest::binary>>), do: :png
|
||||
def type(<<@jpg_signature, rest::binary>>), do: :jpg
|
||||
def type(<<@png_signature, _rest::binary>>), do: :png
|
||||
def type(<<@jpg_signature, _rest::binary>>), do: :jpg
|
||||
def type(_), do: :unknown
|
||||
end
|
||||
|
||||
@@ -798,7 +804,7 @@ defmodule Kernel.SpecialForms do
|
||||
* The first element of the tuple is always an atom or
|
||||
another tuple in the same representation.
|
||||
|
||||
* The second element of the tuple represents metadata.
|
||||
* The second element of the tuple represents [metadata](t:Macro.metadata/0).
|
||||
|
||||
* The third element of the tuple are the arguments for the
|
||||
function call. The third argument may be an atom, which is
|
||||
@@ -820,6 +826,22 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
## Options
|
||||
|
||||
* `:bind_quoted` - passes a binding to the macro. Whenever a binding is
|
||||
given, `unquote/1` is automatically disabled.
|
||||
|
||||
* `:context` - sets the resolution context.
|
||||
|
||||
* `:generated` - marks the given chunk as generated so it does not emit warnings.
|
||||
Currently it only works on special forms (for example, you can annotate a `case`
|
||||
but not an `if`).
|
||||
|
||||
* `:file` - sets the quoted expressions to have the given file.
|
||||
|
||||
* `:line` - sets the quoted expressions to have the given line.
|
||||
|
||||
* `:location` - when set to `:keep`, keeps the current line and file from
|
||||
quote. Read the "Stacktrace information" section below for more information.
|
||||
|
||||
* `:unquote` - when `false`, disables unquoting. This means any `unquote`
|
||||
call will be kept as is in the AST, instead of replaced by the `unquote`
|
||||
arguments. For example:
|
||||
@@ -834,21 +856,6 @@ defmodule Kernel.SpecialForms do
|
||||
...> end
|
||||
{:unquote, [], ["hello"]}
|
||||
|
||||
* `:location` - when set to `:keep`, keeps the current line and file from
|
||||
quote. Read the Stacktrace information section below for more
|
||||
information.
|
||||
|
||||
* `:line` - sets the quoted expressions to have the given line.
|
||||
|
||||
* `:generated` - marks the given chunk as generated so it does not emit warnings.
|
||||
Currently it only works on special forms (for example, you can annotate a `case`
|
||||
but not an `if`).
|
||||
|
||||
* `:context` - sets the resolution context.
|
||||
|
||||
* `:bind_quoted` - passes a binding to the macro. Whenever a binding is
|
||||
given, `unquote/1` is automatically disabled.
|
||||
|
||||
## Quote and macros
|
||||
|
||||
`quote/2` is commonly used with macros for code generation. As an exercise,
|
||||
@@ -948,7 +955,7 @@ defmodule Kernel.SpecialForms do
|
||||
import Math
|
||||
squared(5)
|
||||
x
|
||||
#=> ** (CompileError) undefined variable x or undefined function x/0
|
||||
** (CompileError) undefined variable x or undefined function x/0
|
||||
|
||||
We can see that `x` did not leak to the user context. This happens
|
||||
because Elixir macros are hygienic, a topic we will discuss at length
|
||||
@@ -1013,7 +1020,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
Hygiene.write()
|
||||
Hygiene.read()
|
||||
#=> ** (RuntimeError) undefined variable a or undefined function a/0
|
||||
** (RuntimeError) undefined variable a or undefined function a/0
|
||||
|
||||
For such, you can explicitly pass the current module scope as
|
||||
argument:
|
||||
@@ -1104,7 +1111,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
require Hygiene
|
||||
Hygiene.no_interference()
|
||||
#=> ** (UndefinedFunctionError) ...
|
||||
** (UndefinedFunctionError) ...
|
||||
|
||||
Hygiene.interference()
|
||||
#=> "world"
|
||||
@@ -1191,8 +1198,8 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
require Sample
|
||||
Sample.add(:one, :two)
|
||||
#=> ** (ArithmeticError) bad argument in arithmetic expression
|
||||
#=> adder.ex:5: Sample.add/2
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
adder.ex:5: Sample.add/2
|
||||
|
||||
When using `location: :keep` and invalid arguments are given to
|
||||
`Sample.add/2`, the stacktrace information will point to the file
|
||||
@@ -1513,7 +1520,7 @@ defmodule Kernel.SpecialForms do
|
||||
non-matched value:
|
||||
|
||||
with :foo = :bar, do: :ok
|
||||
#=> ** (MatchError) no match of right hand side value: :bar
|
||||
** (MatchError) no match of right hand side value: :bar
|
||||
|
||||
As with any other function or macro call in Elixir, explicit parens can
|
||||
also be used around the arguments before the `do`/`end` block:
|
||||
@@ -1678,7 +1685,7 @@ defmodule Kernel.SpecialForms do
|
||||
unambiguously identified by the operator `:.`. For example:
|
||||
|
||||
iex> quote do
|
||||
...> Foo.bar
|
||||
...> Foo.bar()
|
||||
...> end
|
||||
{{:., [], [{:__aliases__, [alias: false], [:Foo]}, :bar]}, [], []}
|
||||
|
||||
@@ -1737,8 +1744,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
## Variable handling
|
||||
|
||||
Notice that variables bound in a clause "head" do not leak to the
|
||||
outer context:
|
||||
Notice that variables bound in a clause do not leak to the outer context:
|
||||
|
||||
case data do
|
||||
{:ok, value} -> value
|
||||
@@ -1748,8 +1754,8 @@ defmodule Kernel.SpecialForms do
|
||||
value
|
||||
#=> unbound variable value
|
||||
|
||||
However, variables explicitly bound in the clause "body" are
|
||||
accessible from the outer context:
|
||||
When binding variables with the same names as variables in the outer context,
|
||||
the variables in the outer context are not affected.
|
||||
|
||||
value = 7
|
||||
|
||||
@@ -1759,12 +1765,11 @@ defmodule Kernel.SpecialForms do
|
||||
end
|
||||
|
||||
value
|
||||
#=> 7 or 13
|
||||
#=> 7
|
||||
|
||||
In the example above, `value` is going to be `7` or `13` depending on
|
||||
the value of `lucky?`. In case `value` has no previous value before
|
||||
case, clauses that do not explicitly bind a value have the variable
|
||||
bound to `nil`.
|
||||
In the example above, `value` is going to be `7` regardless of the value of
|
||||
`lucky?`. The variable `value` bound in the clause and the variable `value`
|
||||
bound in the outer context are two entirely separate variables.
|
||||
|
||||
If you want to pattern match against an existing variable,
|
||||
you need to use the `^/1` operator:
|
||||
|
||||
@@ -285,7 +285,7 @@ defmodule Kernel.Typespec do
|
||||
if is_atom(args) do
|
||||
[]
|
||||
else
|
||||
for(arg <- args, do: variable(arg))
|
||||
:lists.map(&variable/1, args)
|
||||
end
|
||||
|
||||
vars = :lists.filter(&match?({:var, _, _}, &1), args)
|
||||
@@ -557,7 +557,7 @@ defmodule Kernel.Typespec do
|
||||
types =
|
||||
:lists.map(
|
||||
fn {field, _} -> {field, Keyword.get(fields, field, quote(do: term()))} end,
|
||||
struct
|
||||
:lists.sort(struct)
|
||||
)
|
||||
|
||||
fun = fn {field, _} ->
|
||||
@@ -661,7 +661,7 @@ defmodule Kernel.Typespec do
|
||||
:elixir_errors.erl_warn(caller.line, caller.file, message)
|
||||
|
||||
# This may be generating an invalid typespec but we need to generate it
|
||||
# to avoid breaking existing code that was valid but only broke dialyzer
|
||||
# to avoid breaking existing code that was valid but only broke Dialyzer
|
||||
{right, state} = typespec(expr, vars, caller, state)
|
||||
{{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state}
|
||||
|
||||
@@ -680,7 +680,7 @@ defmodule Kernel.Typespec do
|
||||
:elixir_errors.erl_warn(caller.line, caller.file, message)
|
||||
|
||||
# This may be generating an invalid typespec but we need to generate it
|
||||
# to avoid breaking existing code that was valid but only broke dialyzer
|
||||
# to avoid breaking existing code that was valid but only broke Dialyzer
|
||||
state = %{state | undefined_type_error_enabled?: false}
|
||||
{left, state} = typespec(left, vars, caller, state)
|
||||
state = %{state | undefined_type_error_enabled?: true}
|
||||
@@ -725,14 +725,19 @@ defmodule Kernel.Typespec do
|
||||
# aliases in typespecs as compile time dependencies.
|
||||
remote = Macro.expand(remote, %{caller | function: {:typespec, 0}})
|
||||
|
||||
unless is_atom(remote) do
|
||||
compile_error(caller, "invalid remote in typespec: #{Macro.to_string(orig)}")
|
||||
end
|
||||
cond do
|
||||
not is_atom(remote) ->
|
||||
compile_error(caller, "invalid remote in typespec: #{Macro.to_string(orig)}")
|
||||
|
||||
{remote_spec, state} = typespec(remote, vars, caller, state)
|
||||
{name_spec, state} = typespec(name, vars, caller, state)
|
||||
type = {remote_spec, meta, name_spec, args}
|
||||
remote_type(type, vars, caller, state)
|
||||
remote == caller.module ->
|
||||
typespec({name, meta, args}, vars, caller, state)
|
||||
|
||||
true ->
|
||||
{remote_spec, state} = typespec(remote, vars, caller, state)
|
||||
{name_spec, state} = typespec(name, vars, caller, state)
|
||||
type = {remote_spec, meta, name_spec, args}
|
||||
remote_type(type, vars, caller, state)
|
||||
end
|
||||
end
|
||||
|
||||
# Handle tuples
|
||||
@@ -828,7 +833,10 @@ defmodule Kernel.Typespec do
|
||||
false ->
|
||||
if state.undefined_type_error_enabled? and
|
||||
not Map.has_key?(state.defined_type_pairs, {name, arity}) do
|
||||
compile_error(caller, "type #{name}/#{arity} undefined")
|
||||
compile_error(
|
||||
caller,
|
||||
"type #{name}/#{arity} undefined (no such type in #{inspect(caller.module)})"
|
||||
)
|
||||
end
|
||||
|
||||
state =
|
||||
|
||||
@@ -94,6 +94,9 @@ defmodule Kernel.Utils do
|
||||
fields = :lists.map(mapper, fields)
|
||||
enforce_keys = List.wrap(Module.get_attribute(module, :enforce_keys))
|
||||
|
||||
# TODO: Make it raise on v2.0
|
||||
warn_on_duplicate_struct_key(:lists.keysort(1, fields))
|
||||
|
||||
foreach = fn
|
||||
key when is_atom(key) ->
|
||||
:ok
|
||||
@@ -108,6 +111,19 @@ defmodule Kernel.Utils do
|
||||
{struct, enforce_keys, Module.get_attribute(module, :derive)}
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_struct_key([]) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_struct_key([{key, _} | [{key, _} | _] = rest]) do
|
||||
IO.warn("duplicate key #{inspect(key)} found in struct")
|
||||
warn_on_duplicate_struct_key(rest)
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_struct_key([_ | rest]) do
|
||||
warn_on_duplicate_struct_key(rest)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Announcing callback for defstruct.
|
||||
"""
|
||||
|
||||
+154
-103
@@ -1,10 +1,10 @@
|
||||
defmodule Keyword do
|
||||
@moduledoc """
|
||||
A set of functions for working with keywords.
|
||||
Keyword lists are lists of two-element tuples, where the first
|
||||
element of the tuple is an atom and the second element can be any
|
||||
value, used mostly to work with optional values.
|
||||
|
||||
A keyword list is a list of two-element tuples where the first
|
||||
element of the tuple is an atom and the second element
|
||||
can be any value.
|
||||
## Examples
|
||||
|
||||
For example, the following is a keyword list:
|
||||
|
||||
@@ -15,62 +15,68 @@ defmodule Keyword do
|
||||
|
||||
[exit_on_close: true, active: :once, packet_size: 1024]
|
||||
|
||||
This is also the syntax that Elixir uses to inspect keyword lists:
|
||||
|
||||
iex> [{:active, :once}]
|
||||
[active: :once]
|
||||
|
||||
The two syntaxes are completely equivalent. Like atoms, keywords
|
||||
must be composed of Unicode characters such as letters, numbers,
|
||||
underscore, and `@`. If the keyword has a character that does not
|
||||
belong to the category above, such as spaces, you can wrap it in
|
||||
quotes:
|
||||
The two syntaxes are completely equivalent. Like atoms, keyword
|
||||
lists keys must be composed of Unicode characters such as letters,
|
||||
numbers, underscore, and `@`. If the keyword has a character that
|
||||
does not belong to the category above, such as spaces, you can wrap
|
||||
it in quotes:
|
||||
|
||||
iex> ["exit on close": true]
|
||||
["exit on close": true]
|
||||
|
||||
Wrapping a keyword in quotes does not make it a string. Keywords are
|
||||
always atoms. If you use quotes when all characters are a valid part
|
||||
of a keyword without quotes, Elixir will warn.
|
||||
Wrapping a keyword in quotes does not make it a string. Keyword lists
|
||||
keys are always atoms. If you use quotes around the key when quoting
|
||||
is not necessary, Elixir will warn.
|
||||
|
||||
Note that when keyword lists are passed as the last argument to a function,
|
||||
if the short-hand syntax is used then the square brackets around the keyword list
|
||||
can be omitted as well. For example, the following:
|
||||
## Duplicate keys and ordering
|
||||
|
||||
String.split("1-0", "-", trim: true, parts: 2)
|
||||
A keyword may have duplicated keys so it is not strictly a key-value
|
||||
data type. However most of the functions in this module behave exactly
|
||||
as a key-value so they work similarly to the functions you would find
|
||||
in the `Map` module. For example, `Keyword.get/3` will get the first
|
||||
entry matching the given key, regardless if duplicated entries exist.
|
||||
Similarly, `Keyword.put/3` and `Keyword.delete/2` ensure all duplicated
|
||||
entries for a given key are removed when invoked. Note however that
|
||||
keyword list operations need to traverse the list in order to find
|
||||
keys, so these operations are slower than their map counterparts.
|
||||
|
||||
is equivalent to:
|
||||
A handful of functions exist to handle duplicated keys, for example,
|
||||
`get_values/2` returns all values for a given key and `delete_first/2`
|
||||
deletes just one of the existing entries.
|
||||
|
||||
String.split("1-0", "-", [trim: true, parts: 2])
|
||||
|
||||
A keyword may have duplicated keys so it is not strictly
|
||||
a key-value store. However most of the functions in this module
|
||||
behave exactly as a dictionary so they work similarly to
|
||||
the functions you would find in the `Map` module.
|
||||
|
||||
For example, `Keyword.get/3` will get the first entry matching
|
||||
the given key, regardless if duplicated entries exist.
|
||||
Similarly, `Keyword.put/3` and `Keyword.delete/3` ensure all
|
||||
duplicated entries for a given key are removed when invoked.
|
||||
Note that operations that require keys to be found in the keyword
|
||||
list (like `Keyword.get/3`) need to traverse the list in order
|
||||
to find keys, so these operations may be slower than their map
|
||||
counterparts.
|
||||
|
||||
A handful of functions exist to handle duplicated keys, in
|
||||
particular, `Enum.into/2` allows creating new keywords without
|
||||
removing duplicated keys, `get_values/2` returns all values for
|
||||
a given key and `delete_first/2` deletes just one of the existing
|
||||
entries.
|
||||
|
||||
The functions in `Keyword` do not guarantee any property when
|
||||
it comes to ordering. However, since a keyword list is simply a
|
||||
list, all the operations defined in `Enum` and `List` can be
|
||||
applied too, especially when ordering is required.
|
||||
The functions in `Keyword` do not guarantee any property when it comes
|
||||
to ordering. However, since a keyword list is simply a list, all the
|
||||
operations defined in `Enum` and `List` can be applied too, especially
|
||||
when ordering is required.
|
||||
|
||||
Most of the functions in this module work in linear time. This means
|
||||
that, the time it takes to perform an operation grows at the same
|
||||
rate as the length of the list.
|
||||
|
||||
## Call syntax
|
||||
|
||||
When keyword lists are passed as the last argument to a function, then
|
||||
the square brackets around the keyword list can be omitted as well. For
|
||||
example, the keyword list syntax:
|
||||
|
||||
String.split("1-0", "-", [trim: true, parts: 2])
|
||||
|
||||
can be written without the enclosing brackets whenever it is the last
|
||||
argument of a function call:
|
||||
|
||||
String.split("1-0", "-", trim: true, parts: 2)
|
||||
|
||||
Since tuples, lists, maps, and others are treated the same as function
|
||||
calls in Elixir syntax, this property is also available to them:
|
||||
|
||||
iex> {1, 2, foo: :bar}
|
||||
{1, 2, [{:foo, :bar}]}
|
||||
|
||||
iex> [1, 2, foo: :bar]
|
||||
[1, 2, {:foo, :bar}]
|
||||
|
||||
iex> %{1 => 2, foo: :bar}
|
||||
%{1 => 2, :foo => :bar}
|
||||
"""
|
||||
|
||||
@compile :inline_list_funcs
|
||||
@@ -409,14 +415,13 @@ defmodule Keyword do
|
||||
"""
|
||||
@spec get_values(t, key) :: [value]
|
||||
def get_values(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
fun = fn
|
||||
{^key, val} -> {true, val}
|
||||
{_, _} -> false
|
||||
end
|
||||
|
||||
:lists.filtermap(fun, keywords)
|
||||
get_values(keywords, key, [])
|
||||
end
|
||||
|
||||
defp get_values([{key, value} | tail], key, values), do: get_values(tail, key, [value | values])
|
||||
defp get_values([{_, _} | tail], key, values), do: get_values(tail, key, values)
|
||||
defp get_values([], _key, values), do: :lists.reverse(values)
|
||||
|
||||
@doc """
|
||||
Returns all keys from the keyword list.
|
||||
|
||||
@@ -453,24 +458,8 @@ defmodule Keyword do
|
||||
:lists.map(fn {_, v} -> v end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the entries in the keyword list for a `key` with `value`.
|
||||
|
||||
If no `key` with `value` exists, returns the keyword list unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.delete([a: 1, b: 2], :a, 1)
|
||||
[b: 2]
|
||||
iex> Keyword.delete([a: 1, b: 2, a: 3], :a, 3)
|
||||
[a: 1, b: 2]
|
||||
iex> Keyword.delete([a: 1], :a, 5)
|
||||
[a: 1]
|
||||
iex> Keyword.delete([a: 1], :b, 5)
|
||||
[a: 1]
|
||||
|
||||
"""
|
||||
@spec delete(t, key, value) :: t
|
||||
@doc false
|
||||
@deprecated "Use Keyword.fetch/2 + Keyword.delete/2 instead"
|
||||
def delete(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keymember(key, 1, keywords) do
|
||||
true -> delete_key_value(keywords, key, value)
|
||||
@@ -516,17 +505,9 @@ defmodule Keyword do
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_key([{key, _} | tail], key) do
|
||||
delete_key(tail, key)
|
||||
end
|
||||
|
||||
defp delete_key([{_, _} = pair | tail], key) do
|
||||
[pair | delete_key(tail, key)]
|
||||
end
|
||||
|
||||
defp delete_key([], _key) do
|
||||
[]
|
||||
end
|
||||
defp delete_key([{key, _} | tail], key), do: delete_key(tail, key)
|
||||
defp delete_key([{_, _} = pair | tail], key), do: [pair | delete_key(tail, key)]
|
||||
defp delete_key([], _key), do: []
|
||||
|
||||
@doc """
|
||||
Deletes the first entry in the keyword list for a specific `key`.
|
||||
@@ -648,8 +629,10 @@ defmodule Keyword do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.replace!([a: 1, b: 2, a: 4], :a, 3)
|
||||
[a: 3, b: 2]
|
||||
iex> Keyword.replace!([a: 1, b: 2, a: 3], :a, :new)
|
||||
[a: :new, b: 2]
|
||||
iex> Keyword.replace!([a: 1, b: 2, c: 3, b: 4], :b, :new)
|
||||
[a: 1, b: :new, c: 3]
|
||||
|
||||
iex> Keyword.replace!([a: 1], :b, 2)
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
@@ -658,10 +641,19 @@ defmodule Keyword do
|
||||
@doc since: "1.5.0"
|
||||
@spec replace!(t, key, value) :: t
|
||||
def replace!(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, _} -> [{key, value} | delete(keywords, key)]
|
||||
false -> raise KeyError, key: key, term: keywords
|
||||
end
|
||||
replace!(keywords, key, value, keywords)
|
||||
end
|
||||
|
||||
defp replace!([{key, _} | keywords], key, value, _original) do
|
||||
[{key, value} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp replace!([{_, _} = e | keywords], key, value, original) do
|
||||
[e | replace!(keywords, key, value, original)]
|
||||
end
|
||||
|
||||
defp replace!([], key, _value, original) when is_atom(key) do
|
||||
raise(KeyError, key: key, term: original)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -821,10 +813,10 @@ defmodule Keyword do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.update!([a: 1], :a, &(&1 * 2))
|
||||
[a: 2]
|
||||
iex> Keyword.update!([a: 1, a: 2], :a, &(&1 * 2))
|
||||
[a: 2]
|
||||
iex> Keyword.update!([a: 1, b: 2, a: 3], :a, &(&1 * 2))
|
||||
[a: 2, b: 2]
|
||||
iex> Keyword.update!([a: 1, b: 2, c: 3], :b, &(&1 * 2))
|
||||
[a: 1, b: 4, c: 3]
|
||||
|
||||
iex> Keyword.update!([a: 1], :b, &(&1 * 2))
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
@@ -836,16 +828,16 @@ defmodule Keyword do
|
||||
update!(keywords, key, fun, keywords)
|
||||
end
|
||||
|
||||
defp update!([{key, value} | keywords], key, fun, _dict) do
|
||||
defp update!([{key, value} | keywords], key, fun, _original) do
|
||||
[{key, fun.(value)} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp update!([{_, _} = e | keywords], key, fun, dict) do
|
||||
[e | update!(keywords, key, fun, dict)]
|
||||
defp update!([{_, _} = e | keywords], key, fun, original) do
|
||||
[e | update!(keywords, key, fun, original)]
|
||||
end
|
||||
|
||||
defp update!([], key, _fun, dict) when is_atom(key) do
|
||||
raise(KeyError, key: key, term: dict)
|
||||
defp update!([], key, _fun, original) when is_atom(key) do
|
||||
raise(KeyError, key: key, term: original)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -976,14 +968,73 @@ defmodule Keyword do
|
||||
@spec pop(t, key, value) :: {value, 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}
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {default, keywords}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first value for `key` and removes all associated antries in the keyword list,
|
||||
raising if `key` is not present.
|
||||
|
||||
This function behaves like `pop/3`, but raises in cases the `key` is not present in the
|
||||
given `keywords`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.pop!([a: 1], :a)
|
||||
{1, []}
|
||||
iex> Keyword.pop!([a: 1, a: 2], :a)
|
||||
{1, []}
|
||||
iex> Keyword.pop!([a: 1], :b)
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
|
||||
"""
|
||||
@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
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all values for `key` and removes all associated entries in the keyword list.
|
||||
|
||||
It returns a tuple where the first element is a list of values for `key` and the
|
||||
second element is a keyword list with all entries associated with `key` removed.
|
||||
If the `key` is not present in the keyword list, `{[], keyword_list}` is
|
||||
returned.
|
||||
|
||||
If you don't want to remove all the entries associated with `key` use `pop_first/3`
|
||||
instead, that function will remove only the first entry.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.pop_values([a: 1], :a)
|
||||
{[1], []}
|
||||
iex> Keyword.pop_values([a: 1], :b)
|
||||
{[], [a: 1]}
|
||||
iex> Keyword.pop_values([a: 1, a: 2], :a)
|
||||
{[1, 2], []}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec pop_values(t, key) :: {[value], t}
|
||||
def pop_values(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
pop_values(:lists.reverse(keywords), key, [], [])
|
||||
end
|
||||
|
||||
defp pop_values([{key, value} | tail], key, values, acc),
|
||||
do: pop_values(tail, key, [value | values], acc)
|
||||
|
||||
defp pop_values([{_, _} = pair | tail], key, values, acc),
|
||||
do: pop_values(tail, key, values, [pair | acc])
|
||||
|
||||
defp pop_values([], _key, values, acc),
|
||||
do: {values, acc}
|
||||
|
||||
@doc """
|
||||
Lazily returns and removes all values associated with `key` in the keyword list.
|
||||
|
||||
|
||||
+32
-19
@@ -1,19 +1,6 @@
|
||||
defmodule List do
|
||||
@moduledoc """
|
||||
Functions that work on (linked) lists.
|
||||
|
||||
Many of the functions provided for lists, which implement
|
||||
the `Enumerable` protocol, are found in the `Enum` module.
|
||||
|
||||
Additionally, the following functions and operators for lists are
|
||||
found in `Kernel`:
|
||||
|
||||
* `++/2`
|
||||
* `--/2`
|
||||
* `hd/1`
|
||||
* `tl/1`
|
||||
* `in/2`
|
||||
* `length/1`
|
||||
Linked lists hold zero, one, or more elements in the choosen order.
|
||||
|
||||
Lists in Elixir are specified between square brackets:
|
||||
|
||||
@@ -69,6 +56,17 @@ defmodule List do
|
||||
time because they need to iterate through every element of the list, but
|
||||
`first/1` will run in constant time because it only needs the first element.
|
||||
|
||||
Lists also implement the `Enumerable` protocol, so many functions to work with
|
||||
lists are found in the `Enum` module. Additionally, the following functions and
|
||||
operators for lists are found in `Kernel`:
|
||||
|
||||
* `++/2`
|
||||
* `--/2`
|
||||
* `hd/1`
|
||||
* `tl/1`
|
||||
* `in/2`
|
||||
* `length/1`
|
||||
|
||||
## Charlists
|
||||
|
||||
If a list is made of non-negative integers, where each integer represents a
|
||||
@@ -90,10 +88,23 @@ defmodule List do
|
||||
iex> 'abc'
|
||||
'abc'
|
||||
|
||||
Even though the representation changed, the raw data does remain a list of
|
||||
numbers, which can be handled as such:
|
||||
|
||||
iex> inspect('abc', charlists: :as_list)
|
||||
"[97, 98, 99]"
|
||||
iex> Enum.map('abc', fn num -> 1000 + num end)
|
||||
[1097, 1098, 1099]
|
||||
|
||||
You can use the `IEx.Helpers.i/1` helper to get a condensed rundown on
|
||||
charlists in IEx when you encounter them, which shows you the type, description
|
||||
and also the raw representation in one single summary.
|
||||
|
||||
The rationale behind this behaviour is to better support
|
||||
Erlang libraries which may return text as charlists
|
||||
instead of Elixir strings. One example of such functions
|
||||
is `Application.loaded_applications/0`:
|
||||
instead of Elixir strings. In Erlang, charlists are the default
|
||||
way of handling strings, while in Elixir it's binaries. One
|
||||
example of such functions is `Application.loaded_applications/0`:
|
||||
|
||||
Application.loaded_applications()
|
||||
#=> [
|
||||
@@ -846,6 +857,8 @@ defmodule List do
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
The base needs to be between `2` and `36`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_integer('3FF', 16)
|
||||
@@ -885,7 +898,7 @@ defmodule List do
|
||||
* a list containing one of these three elements
|
||||
|
||||
Notice that this function expects a list of integers representing
|
||||
UTF-8 code points. If you have a list of bytes, you must instead use
|
||||
Unicode code points. If you have a list of bytes, you must instead use
|
||||
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
|
||||
|
||||
## Examples
|
||||
@@ -936,11 +949,11 @@ defmodule List do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a list of integers representing code points, lists or
|
||||
Converts a list of integers representing Unicode code points, lists or
|
||||
strings into a charlist.
|
||||
|
||||
Notice that this function expects a list of integers representing
|
||||
UTF-8 code points. If you have a list of bytes, you must instead use
|
||||
Unicode code points. If you have a list of bytes, you must instead use
|
||||
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -24,6 +24,8 @@ defprotocol List.Chars do
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Atom do
|
||||
def to_charlist(nil), do: ''
|
||||
|
||||
def to_charlist(atom), do: Atom.to_charlist(atom)
|
||||
end
|
||||
|
||||
|
||||
+240
-65
@@ -2,16 +2,62 @@ import Kernel, except: [to_string: 1]
|
||||
|
||||
defmodule Macro do
|
||||
@moduledoc ~S"""
|
||||
Conveniences for working with macros.
|
||||
Macros are compile-time constructs that are invoked with Elixir's AST
|
||||
as input and a superset of Elixir's AST as output.
|
||||
|
||||
Let's see a simple example that shows the difference between functions and macros:
|
||||
|
||||
defmodule Example do
|
||||
defmacro macro_inspect(value) do
|
||||
IO.inspect(value)
|
||||
value
|
||||
end
|
||||
|
||||
def fun_inspect(value) do
|
||||
IO.inpect(value)
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
Now let's give it a try:
|
||||
|
||||
import Example
|
||||
|
||||
macro_inspect(1)
|
||||
#=> 1
|
||||
#=> 1
|
||||
|
||||
fun_inspect(1)
|
||||
#=> 1
|
||||
#=> 1
|
||||
|
||||
So far they behave the same, as we are passing an integer as argument.
|
||||
But what happens when we pass an expresion:
|
||||
|
||||
macro_inspect(1 + 2)
|
||||
#=> {:+, [line: 3], [1, 2]}
|
||||
#=> 3
|
||||
|
||||
fun_inspect(1 + 2)
|
||||
#=> 3
|
||||
#=> 3
|
||||
|
||||
The macro receives the representation of the code given as argument,
|
||||
while a function receives the result of the code given as argument.
|
||||
A macro must return a superset of the code representation. See
|
||||
`t:input/0` and `t:output/0` for more information.
|
||||
|
||||
To learn more about Elixir's AST and how to build them programmatically,
|
||||
see `quote/2`.
|
||||
|
||||
## Custom Sigils
|
||||
|
||||
To create a custom sigil, define a function with the name
|
||||
`sigil_{identifier}` that takes two arguments. The first argument will be
|
||||
the string, the second will be a charlist containing any modifiers. If the
|
||||
sigil is lower case (such as `sigil_x`) then the string argument will allow
|
||||
interpolation. If the sigil is upper case (such as `sigil_X`) then the string
|
||||
will not be interpolated.
|
||||
Macros are also commonly used to implement custom sigils. To create a custom
|
||||
sigil, define a function with the name `sigil_{identifier}` that takes two
|
||||
arguments. The first argument will be the string, the second will be a charlist
|
||||
containing any modifiers. If the sigil is lower case (such as `sigil_x`) then
|
||||
the string argument will allow interpolation. If the sigil is upper case
|
||||
(such as `sigil_X`) then the string will not be interpolated.
|
||||
|
||||
Valid modifiers include only lower and upper case letters. Other characters
|
||||
will cause a syntax error.
|
||||
@@ -59,13 +105,94 @@ defmodule Macro do
|
||||
alias Code.Identifier
|
||||
|
||||
@typedoc "Abstract Syntax Tree (AST)"
|
||||
@type t :: expr | literal
|
||||
@type t :: input
|
||||
|
||||
@typedoc "Represents expressions in the AST"
|
||||
@type expr :: {expr | atom, keyword, atom | [t]}
|
||||
@typedoc "The inputs of a macro"
|
||||
@type input ::
|
||||
input_expr
|
||||
| {input, input}
|
||||
| [input]
|
||||
| atom
|
||||
| number
|
||||
| binary
|
||||
|
||||
@typedoc "Represents literals in the AST"
|
||||
@type literal :: atom | number | binary | fun | {t, t} | [t]
|
||||
@typep input_expr :: {input_expr | atom, metadata, atom | [input]}
|
||||
|
||||
@typedoc "The output of a macro"
|
||||
@type output ::
|
||||
output_expr
|
||||
| {output, output}
|
||||
| [output]
|
||||
| atom
|
||||
| number
|
||||
| binary
|
||||
| captured_remote_function
|
||||
| pid
|
||||
|
||||
@typep output_expr :: {output_expr | atom, metadata, atom | [output]}
|
||||
|
||||
@typedoc """
|
||||
A keyword list of AST metadata.
|
||||
|
||||
The metadata in Elixir AST is a keyword list of values. Any key can be used
|
||||
and different parts of the compiler may use different keys. For example,
|
||||
the AST received by a macro will always include the `:line` annotation,
|
||||
while the AST emitted by `quote/2` will only have the `:line` annotation if
|
||||
the `:line` option is provided.
|
||||
|
||||
The following metadata keys are public:
|
||||
|
||||
* `:context` - Defines the context in which the AST was generated.
|
||||
For example, `quote/2` will include the module calling `quote/2`
|
||||
as the context. This is often used to distinguish regular code from code
|
||||
generated by a macro or by `quote/2`.
|
||||
* `:counter` - The variable counter used for variable hygiene. In terms of
|
||||
the compiler, each variable is identified by the combination of either
|
||||
`name` and `metadata[:counter]`, or `name` and `context`.
|
||||
* `:generated` - Whether the code should be considered as generated by
|
||||
the compiler or not. This means the compiler and tools like Dialyzer may not
|
||||
emit certain warnings.
|
||||
* `:keep` - Used by `quote/2` with the option `location: :keep` to annotate
|
||||
the file and the line number of the quoted source.
|
||||
* `:line` - The line number of the AST node.
|
||||
|
||||
The following metadata keys are enabled by `Code.string_to_quoted/2`:
|
||||
|
||||
* `:closing` - contains metadata about the closing pair, such as a `}`
|
||||
in a tuple or in a map, or such as the closing `)` in a function call
|
||||
with parens. The `:closing` does not delimit the end of expression if
|
||||
there are `:do` and `:end` metadata (when `:token_metadata` is true)
|
||||
* `:column` - the column number of the AST node (when `:columns` is true)
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword
|
||||
* `:do` - contains metadata about the `do` location in a function call with
|
||||
`do/end` blocks (when `:token_metadata` is true)
|
||||
* `:end` - contains metadata about the `end` location in a function call with
|
||||
`do/end` blocks (when `:token_metadata` is true)
|
||||
* `:end_of_expression` - denotes when the end of expression effectively
|
||||
happens. Available for all expressions except the last one inside a
|
||||
`__block__` (when `:token_metadata` is true)
|
||||
|
||||
The following metadata keys are private:
|
||||
|
||||
* `:alias` - Used for alias hygiene.
|
||||
* `:ambiguous_op` - Used for improved error messages in the compiler.
|
||||
* `:import` - Used for import hygiene.
|
||||
* `:var` - Used for improved error messages on undefined variables.
|
||||
|
||||
Do not rely on them as they may change or be fully removed in future versions
|
||||
of the language. They are often used by `quote/2` and the compiler to provide
|
||||
features like hygiene, better error messages, and so forth.
|
||||
|
||||
If you introduce custom keys into the AST metadata, please make sure to prefix
|
||||
them with the name of your library or application, so that they will not conflict
|
||||
with keys that could potentially be introduced by the compiler in the future.
|
||||
"""
|
||||
@type metadata :: keyword
|
||||
|
||||
@typedoc "A captured remote function in the format of &Mod.fun/arity"
|
||||
@type captured_remote_function :: fun
|
||||
|
||||
@doc """
|
||||
Breaks a pipeline expression into a list.
|
||||
@@ -138,7 +265,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
# {:fn, _, _} is what we get when we pipe into an anonymous function without
|
||||
# calling it, e.g., `:foo |> (fn x -> x end)`.
|
||||
# calling it, for example, `:foo |> (fn x -> x end)`.
|
||||
def pipe(expr, {:fn, _, _}, _integer) do
|
||||
raise ArgumentError,
|
||||
"cannot pipe #{to_string(expr)} into an anonymous function without" <>
|
||||
@@ -150,13 +277,26 @@ defmodule Macro do
|
||||
{call, line, List.insert_at([], integer, expr)}
|
||||
end
|
||||
|
||||
def pipe(expr, {call, line, args} = call_args, integer) when is_list(args) do
|
||||
if is_atom(call) and Identifier.binary_op(call) != :error do
|
||||
raise ArgumentError,
|
||||
"cannot pipe #{to_string(expr)} into #{to_string(call_args)}, " <>
|
||||
"the #{to_string(call)} operator can only take two arguments"
|
||||
else
|
||||
{call, line, List.insert_at(args, integer, expr)}
|
||||
def pipe(_expr, {op, _line, [arg]}, _integer) when op == :+ or op == :- do
|
||||
raise ArgumentError,
|
||||
"piping into a unary operator is not supported, please use the qualified name: " <>
|
||||
"Kernel.#{op}(#{to_string(arg)}), instead of #{op}#{to_string(arg)}"
|
||||
end
|
||||
|
||||
def pipe(expr, {op, line, args} = op_args, integer) when is_list(args) do
|
||||
cond do
|
||||
is_atom(op) and Identifier.unary_op(op) != :error ->
|
||||
raise ArgumentError,
|
||||
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
|
||||
"the #{to_string(op)} operator can only take one argument"
|
||||
|
||||
is_atom(op) and Identifier.binary_op(op) != :error ->
|
||||
raise ArgumentError,
|
||||
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
|
||||
"the #{to_string(op)} operator can only take two arguments"
|
||||
|
||||
true ->
|
||||
{op, line, List.insert_at(args, integer, expr)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -755,6 +895,16 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
# All other calls
|
||||
def to_string({target, meta, []} = ast, fun) do
|
||||
target = call_to_string(target, fun)
|
||||
|
||||
if meta[:no_parens] do
|
||||
fun.(ast, target)
|
||||
else
|
||||
fun.(ast, target <> "()")
|
||||
end
|
||||
end
|
||||
|
||||
def to_string({target, _, args} = ast, fun) when is_list(args) do
|
||||
with :error <- unary_call(ast, fun),
|
||||
:error <- binary_call(ast, fun),
|
||||
@@ -858,7 +1008,13 @@ defmodule Macro do
|
||||
false
|
||||
end
|
||||
|
||||
defp interpolate({:<<>>, _, parts}, fun) do
|
||||
defp interpolate(ast, fun), do: interpolate(ast, "\"", "\"", fun)
|
||||
|
||||
defp interpolate({:<<>>, _, [parts]}, left, right, _) when left in [~s["""\n], ~s['''\n]] do
|
||||
<<left::binary, parts::binary, right::binary>>
|
||||
end
|
||||
|
||||
defp interpolate({:<<>>, _, parts}, left, right, fun) do
|
||||
parts =
|
||||
Enum.map_join(parts, "", fn
|
||||
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [arg]}, {:binary, _, _}]} ->
|
||||
@@ -869,9 +1025,16 @@ defmodule Macro do
|
||||
binary_part(binary, 1, byte_size(binary) - 2)
|
||||
end)
|
||||
|
||||
<<?", parts::binary, ?">>
|
||||
escaped = escape_sigil(parts, left)
|
||||
<<left::binary, escaped::binary, right::binary>>
|
||||
end
|
||||
|
||||
defp escape_sigil(parts, "("), do: String.replace(parts, ")", ~S"\)")
|
||||
defp escape_sigil(parts, "{"), do: String.replace(parts, "}", ~S"\}")
|
||||
defp escape_sigil(parts, "["), do: String.replace(parts, "]", ~S"\]")
|
||||
defp escape_sigil(parts, "<"), do: String.replace(parts, ">", ~S"\>")
|
||||
defp escape_sigil(parts, delimiter), do: String.replace(parts, delimiter, "\\#{delimiter}")
|
||||
|
||||
defp module_to_string(atom, _fun) when is_atom(atom) do
|
||||
inspect_no_limit(atom)
|
||||
end
|
||||
@@ -931,25 +1094,22 @@ defmodule Macro do
|
||||
:error
|
||||
end
|
||||
|
||||
defp sigil_call({sigil, _, [{:<<>>, _, _} = parts, args]} = ast, fun)
|
||||
defp sigil_call({sigil, meta, [{:<<>>, _, _} = parts, args]} = ast, fun)
|
||||
when is_atom(sigil) and is_list(args) do
|
||||
delimiter = Keyword.get(meta, :delimiter, "\"")
|
||||
{left, right} = delimiter_pair(delimiter)
|
||||
|
||||
case Atom.to_string(sigil) do
|
||||
<<"sigil_", name>> when name >= ?A and name <= ?Z ->
|
||||
args = sigil_args(args, fun)
|
||||
{:<<>>, _, [binary]} = parts
|
||||
|
||||
formatted =
|
||||
if :binary.last(binary) == ?\n do
|
||||
binary = String.replace(binary, ~s["""], ~s["\\""])
|
||||
<<?~, name, ~s["""\n], binary::binary, ~s["""], sigil_args(args, fun)::binary>>
|
||||
else
|
||||
{left, right} = select_sigil_container(binary)
|
||||
<<?~, name, left, binary::binary, right, sigil_args(args, fun)::binary>>
|
||||
end
|
||||
|
||||
formatted = <<?~, name, left::binary, binary::binary, right::binary, args::binary>>
|
||||
{:ok, fun.(ast, formatted)}
|
||||
|
||||
<<"sigil_", name>> when name >= ?a and name <= ?z ->
|
||||
{:ok, fun.(ast, "~" <> <<name>> <> interpolate(parts, fun) <> sigil_args(args, fun))}
|
||||
args = sigil_args(args, fun)
|
||||
formatted = "~" <> <<name>> <> interpolate(parts, left, right, fun) <> args
|
||||
{:ok, fun.(ast, formatted)}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
@@ -960,17 +1120,13 @@ defmodule Macro do
|
||||
:error
|
||||
end
|
||||
|
||||
defp select_sigil_container(binary) do
|
||||
cond do
|
||||
:binary.match(binary, ["\""]) == :nomatch -> {?", ?"}
|
||||
:binary.match(binary, ["\'"]) == :nomatch -> {?', ?'}
|
||||
:binary.match(binary, ["(", ")"]) == :nomatch -> {?(, ?)}
|
||||
:binary.match(binary, ["[", "]"]) == :nomatch -> {?[, ?]}
|
||||
:binary.match(binary, ["{", "}"]) == :nomatch -> {?{, ?}}
|
||||
:binary.match(binary, ["<", ">"]) == :nomatch -> {?<, ?>}
|
||||
true -> {?/, ?/}
|
||||
end
|
||||
end
|
||||
defp delimiter_pair("["), do: {"[", "]"}
|
||||
defp delimiter_pair("{"), do: {"{", "}"}
|
||||
defp delimiter_pair("("), do: {"(", ")"}
|
||||
defp delimiter_pair("<"), do: {"<", ">"}
|
||||
defp delimiter_pair("\"\"\""), do: {"\"\"\"\n", "\"\"\""}
|
||||
defp delimiter_pair("'''"), do: {"'''\n", "'''"}
|
||||
defp delimiter_pair(str), do: {str, str}
|
||||
|
||||
defp sigil_args([], _fun), do: ""
|
||||
defp sigil_args(args, fun), do: fun.(args, List.to_string(args))
|
||||
@@ -1213,10 +1369,10 @@ defmodule Macro do
|
||||
elem(do_expand_once(ast, env), 0)
|
||||
end
|
||||
|
||||
defp do_expand_once({:__aliases__, _, _} = original, env) do
|
||||
case :elixir_aliases.expand(original, env.aliases, env.macro_aliases, env.lexical_tracker) do
|
||||
defp do_expand_once({:__aliases__, meta, _} = original, env) do
|
||||
case :elixir_aliases.expand(original, env) do
|
||||
receiver when is_atom(receiver) ->
|
||||
:elixir_lexical.record_remote(receiver, env.function, env.lexical_tracker)
|
||||
:elixir_env.trace({:alias_reference, meta, receiver}, env)
|
||||
{receiver, true}
|
||||
|
||||
aliases ->
|
||||
@@ -1225,7 +1381,7 @@ defmodule Macro do
|
||||
case :lists.all(&is_atom/1, aliases) do
|
||||
true ->
|
||||
receiver = :elixir_aliases.concat(aliases)
|
||||
:elixir_lexical.record_remote(receiver, env.function, env.lexical_tracker)
|
||||
:elixir_env.trace({:alias_reference, meta, receiver}, env)
|
||||
{receiver, true}
|
||||
|
||||
false ->
|
||||
@@ -1252,17 +1408,9 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
# Expand possible macro import invocation
|
||||
defp do_expand_once({atom, meta, context} = original, env)
|
||||
defp do_expand_once({atom, meta, context} = original, _env)
|
||||
when is_atom(atom) and is_list(meta) and is_atom(context) do
|
||||
if Macro.Env.has_var?(env, {atom, Keyword.get(meta, :counter, context)}) do
|
||||
{original, false}
|
||||
else
|
||||
case do_expand_once({atom, meta, []}, env) do
|
||||
{_, true} = exp -> exp
|
||||
{_, false} -> {original, false}
|
||||
end
|
||||
end
|
||||
{original, false}
|
||||
end
|
||||
|
||||
defp do_expand_once({atom, meta, args} = original, env)
|
||||
@@ -1347,19 +1495,46 @@ defmodule Macro do
|
||||
def operator?(name, arity) when is_atom(name) and is_integer(arity), do: false
|
||||
|
||||
@doc """
|
||||
Returns `true` if the given quoted expression is an AST literal.
|
||||
Returns `true` if the given quoted expression represents a quoted literal.
|
||||
|
||||
Atoms, numbers, and functions are always literals. Binaries, lists, tuples,
|
||||
maps, and structs are only literals if all of their terms are also literals.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.quoted_literal?(quote(do: "foo"))
|
||||
true
|
||||
iex> Macro.quoted_literal?(quote(do: {"foo", 1}))
|
||||
true
|
||||
iex> Macro.quoted_literal?(quote(do: {"foo", 1, :baz}))
|
||||
true
|
||||
iex> Macro.quoted_literal?(quote(do: %{foo: "bar"}))
|
||||
true
|
||||
iex> Macro.quoted_literal?(quote(do: %URI{path: "/"}))
|
||||
true
|
||||
iex> Macro.quoted_literal?(quote(do: URI.parse("/")))
|
||||
false
|
||||
iex> Macro.quoted_literal?(quote(do: {foo, var}))
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec quoted_literal?(literal) :: true
|
||||
@spec quoted_literal?(expr) :: false
|
||||
@spec quoted_literal?(t) :: boolean
|
||||
def quoted_literal?(term)
|
||||
|
||||
def quoted_literal?({:__aliases__, _, args}),
|
||||
do: quoted_literal?(args)
|
||||
|
||||
def quoted_literal?({:%, _, [left, right]}),
|
||||
do: quoted_literal?(left) and quoted_literal?(right)
|
||||
|
||||
def quoted_literal?({:%{}, _, args}), do: quoted_literal?(args)
|
||||
def quoted_literal?({:{}, _, args}), do: quoted_literal?(args)
|
||||
def quoted_literal?({left, right}), do: quoted_literal?(left) and quoted_literal?(right)
|
||||
def quoted_literal?(list) when is_list(list), do: Enum.all?(list, "ed_literal?/1)
|
||||
|
||||
def quoted_literal?(term) do
|
||||
is_atom(term) or is_number(term) or is_binary(term) or is_function(term)
|
||||
end
|
||||
def quoted_literal?(term),
|
||||
do: is_atom(term) or is_number(term) or is_binary(term) or is_function(term)
|
||||
|
||||
@doc """
|
||||
Receives an AST node and expands it until it can no longer
|
||||
|
||||
+72
-61
@@ -21,31 +21,31 @@ defmodule Macro.Env do
|
||||
|
||||
It contains the following fields:
|
||||
|
||||
* `module` - the current module name
|
||||
* `aliases` - a list of two-element tuples, where the first
|
||||
element is the aliased name and the second one the actual name
|
||||
* `context` - the context of the environment; it can be `nil`
|
||||
(default context), `:guard` (inside a guard) or `:match` (inside a match)
|
||||
* `context_modules` - a list of modules defined in the current context
|
||||
* `file` - the current file name as a binary
|
||||
* `line` - the current line as an integer
|
||||
* `function` - a tuple as `{atom, integer}`, where the first
|
||||
element is the function name and the second its arity; returns
|
||||
`nil` if not inside a function
|
||||
* `context` - the context of the environment; it can be `nil`
|
||||
(default context), `:guard` (inside a guard) or `:match` (inside a match)
|
||||
* `aliases` - a list of two-element tuples, where the first
|
||||
element is the aliased name and the second one the actual name
|
||||
* `requires` - the list of required modules
|
||||
* `functions` - a list of functions imported from each module
|
||||
* `macros` - a list of macros imported from each module
|
||||
* `line` - the current line as an integer
|
||||
* `macro_aliases` - a list of aliases defined inside the current macro
|
||||
* `context_modules` - a list of modules defined in the current context
|
||||
* `lexical_tracker` - PID of the lexical tracker which is responsible for
|
||||
keeping user info
|
||||
* `macros` - a list of macros imported from each module
|
||||
* `module` - the current module name
|
||||
* `requires` - the list of required modules
|
||||
|
||||
The following fields pertain to variable handling and must not be accessed or
|
||||
relied on. To get a list of all variables, see `vars/1`:
|
||||
The following fields are private to Elixir's macro expansion mechanism and
|
||||
must not be accessed directly:
|
||||
|
||||
* `current_vars`
|
||||
* `unused_vars`
|
||||
* `prematch_vars`
|
||||
* `contextual_vars`
|
||||
* `current_vars`
|
||||
* `lexical_tracker`
|
||||
* `prematch_vars`
|
||||
* `tracers`
|
||||
* `unused_vars`
|
||||
|
||||
The following fields are deprecated and must not be accessed or relied on:
|
||||
|
||||
@@ -53,69 +53,80 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
|
||||
@type name_arity :: {atom, arity}
|
||||
@type file :: binary
|
||||
@type line :: non_neg_integer
|
||||
@type aliases :: [{module, module}]
|
||||
@type macro_aliases :: [{module, {term, module}}]
|
||||
@type context :: :match | :guard | nil
|
||||
@type requires :: [module]
|
||||
@type functions :: [{module, [name_arity]}]
|
||||
@type macros :: [{module, [name_arity]}]
|
||||
@type context_modules :: [module]
|
||||
@type file :: binary
|
||||
@type functions :: [{module, [name_arity]}]
|
||||
@type lexical_tracker :: pid | nil
|
||||
@type line :: non_neg_integer
|
||||
@type macro_aliases :: [{module, {term, module}}]
|
||||
@type macros :: [{module, [name_arity]}]
|
||||
@type name_arity :: {atom, arity}
|
||||
@type requires :: [module]
|
||||
@type variable :: {atom, atom | term}
|
||||
|
||||
@typep vars :: [variable]
|
||||
@typep contextual_vars :: [atom]
|
||||
@typep current_vars ::
|
||||
{%{optional(variable) => {var_version, var_type}},
|
||||
%{optional(variable) => {var_version, var_type}} | false}
|
||||
@typep unused_vars ::
|
||||
{%{optional({atom, var_version}) => non_neg_integer | false}, non_neg_integer}
|
||||
@typep prematch_vars ::
|
||||
{%{optional(variable) => {var_version, var_type}}, non_neg_integer}
|
||||
| :warn
|
||||
| :raise
|
||||
| :pin
|
||||
| :apply
|
||||
@typep tracers :: [module]
|
||||
@typep var_type :: :term
|
||||
@typep var_version :: non_neg_integer
|
||||
@typep unused_vars :: %{optional({variable, var_version}) => non_neg_integer | false}
|
||||
@typep current_vars :: %{optional(variable) => {var_version, var_type}}
|
||||
@typep prematch_vars :: current_vars | :warn | :raise | :pin | :apply
|
||||
@typep contextual_vars :: [atom]
|
||||
@typep vars :: [variable]
|
||||
|
||||
@type t :: %{
|
||||
__struct__: __MODULE__,
|
||||
module: atom,
|
||||
file: file,
|
||||
line: line,
|
||||
function: name_arity | nil,
|
||||
context: context,
|
||||
requires: requires,
|
||||
aliases: aliases,
|
||||
functions: functions,
|
||||
macros: macros,
|
||||
macro_aliases: aliases,
|
||||
context: context,
|
||||
context_modules: context_modules,
|
||||
vars: vars,
|
||||
unused_vars: unused_vars,
|
||||
contextual_vars: contextual_vars,
|
||||
current_vars: current_vars,
|
||||
prematch_vars: prematch_vars,
|
||||
file: file,
|
||||
function: name_arity | nil,
|
||||
functions: functions,
|
||||
lexical_tracker: lexical_tracker,
|
||||
contextual_vars: contextual_vars
|
||||
line: line,
|
||||
macro_aliases: macro_aliases,
|
||||
macros: macros,
|
||||
module: atom,
|
||||
prematch_vars: prematch_vars,
|
||||
unused_vars: unused_vars,
|
||||
requires: requires,
|
||||
tracers: tracers,
|
||||
vars: vars
|
||||
}
|
||||
|
||||
# TODO: Remove :vars field on v2.0
|
||||
def __struct__ do
|
||||
%{
|
||||
__struct__: __MODULE__,
|
||||
module: nil,
|
||||
file: "nofile",
|
||||
line: 0,
|
||||
function: nil,
|
||||
context: nil,
|
||||
requires: [],
|
||||
aliases: [],
|
||||
functions: [],
|
||||
macros: [],
|
||||
macro_aliases: [],
|
||||
context: nil,
|
||||
context_modules: [],
|
||||
vars: [],
|
||||
unused_vars: %{},
|
||||
current_vars: %{},
|
||||
prematch_vars: :warn,
|
||||
contextual_vars: [],
|
||||
current_vars: {%{}, %{}},
|
||||
file: "nofile",
|
||||
function: nil,
|
||||
functions: [],
|
||||
lexical_tracker: nil,
|
||||
contextual_vars: []
|
||||
line: 0,
|
||||
macro_aliases: [],
|
||||
macros: [],
|
||||
module: nil,
|
||||
prematch_vars: :warn,
|
||||
requires: [],
|
||||
tracers: [],
|
||||
unused_vars: {%{}, 0},
|
||||
vars: []
|
||||
}
|
||||
end
|
||||
|
||||
@@ -135,8 +146,8 @@ defmodule Macro.Env do
|
||||
@spec vars(t) :: [variable]
|
||||
def vars(env)
|
||||
|
||||
def vars(%{__struct__: Macro.Env, current_vars: current_vars}) do
|
||||
Map.keys(current_vars)
|
||||
def vars(%{__struct__: Macro.Env, current_vars: {read, _}}) do
|
||||
Map.keys(read)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -146,8 +157,8 @@ defmodule Macro.Env do
|
||||
@spec has_var?(t, variable) :: boolean()
|
||||
def has_var?(env, var)
|
||||
|
||||
def has_var?(%{__struct__: Macro.Env, current_vars: current_vars}, var) do
|
||||
Map.has_key?(current_vars, var)
|
||||
def has_var?(%{__struct__: Macro.Env, current_vars: {read, _}}, var) do
|
||||
Map.has_key?(read, var)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -169,8 +180,8 @@ defmodule Macro.Env do
|
||||
env
|
||||
end
|
||||
|
||||
def to_match(%{__struct__: Macro.Env, current_vars: vars} = env) do
|
||||
%{env | context: :match, prematch_vars: vars}
|
||||
def to_match(%{__struct__: Macro.Env, current_vars: {read, _}, unused_vars: {_, counter}} = env) do
|
||||
%{env | context: :match, prematch_vars: {read, counter}}
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
+40
-20
@@ -1,15 +1,9 @@
|
||||
defmodule Map do
|
||||
@moduledoc """
|
||||
A set of functions for working with maps.
|
||||
Maps are the "go to" key-value data structure in Elixir.
|
||||
|
||||
Many functions for maps, which implement the `Enumerable` protocol,
|
||||
are found in the `Enum` module. Additionally, the following functions
|
||||
for maps are found in `Kernel`:
|
||||
|
||||
* `map_size/1`
|
||||
|
||||
Maps are the "go to" key-value data structure in Elixir. Maps can be created
|
||||
with the `%{}` syntax, and key-value pairs can be expressed as `key => value`:
|
||||
Maps can be created with the `%{}` syntax, and key-value pairs can be
|
||||
expressed as `key => value`:
|
||||
|
||||
iex> %{}
|
||||
%{}
|
||||
@@ -97,6 +91,13 @@ defmodule Map do
|
||||
it performs better because lists have a linear time complexity. Some functions,
|
||||
such as `keys/1` and `values/1`, run in linear time because they need to get to every
|
||||
element in the map.
|
||||
|
||||
Maps also implement the `Enumerable` protocol, so many functions to work with maps
|
||||
are found in the `Enum` module. Additionally, the following functions for maps are
|
||||
found in `Kernel`:
|
||||
|
||||
* `map_size/1`
|
||||
|
||||
"""
|
||||
|
||||
@type key :: any
|
||||
@@ -342,8 +343,8 @@ defmodule Map do
|
||||
in `map` unless `key` is already present.
|
||||
|
||||
This function is useful in case you want to compute the value to put under
|
||||
`key` only if `key` is not already present (e.g., the value is expensive to
|
||||
calculate or generally difficult to setup and teardown again).
|
||||
`key` only if `key` is not already present, as for example, when the value is expensive to
|
||||
calculate or generally difficult to setup and teardown again.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -635,6 +636,31 @@ defmodule Map do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns and removes the value associated with `key` in `map` or raises
|
||||
if `key` is not present.
|
||||
|
||||
Behaves the same as `pop/3` but raises if `key` is not present in `map`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.pop!(%{a: 1}, :a)
|
||||
{1, %{}}
|
||||
iex> Map.pop!(%{a: 1, b: 2}, :a)
|
||||
{1, %{b: 2}}
|
||||
iex> Map.pop!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec pop!(map, key) :: {value, map}
|
||||
def pop!(map, key) do
|
||||
case :maps.take(key, map) do
|
||||
{_, _} = tuple -> tuple
|
||||
:error -> raise KeyError, key: key, term: map
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lazily returns and removes the value associated with `key` in `map`.
|
||||
|
||||
@@ -661,15 +687,9 @@ defmodule Map do
|
||||
"""
|
||||
@spec pop_lazy(map, key, (() -> value)) :: {value, map}
|
||||
def pop_lazy(map, key, fun) when is_function(fun, 0) do
|
||||
case map do
|
||||
%{^key => value} ->
|
||||
{value, delete(map, key)}
|
||||
|
||||
%{} ->
|
||||
{fun.(), map}
|
||||
|
||||
other ->
|
||||
:erlang.error({:badmap, other}, [map, key, fun])
|
||||
case :maps.take(key, map) do
|
||||
{_, _} = tuple -> tuple
|
||||
:error -> {fun.(), map}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+48
-37
@@ -2,14 +2,26 @@ defmodule MapSet do
|
||||
@moduledoc """
|
||||
Functions that work on sets.
|
||||
|
||||
`MapSet` is the "go to" set data structure in Elixir. A set can be constructed
|
||||
using `MapSet.new/0`:
|
||||
A set is a data structure that can contain unique elements of any kind,
|
||||
without any particular order. `MapSet` is the "go to" set data structure in Elixir.
|
||||
|
||||
A set can be constructed using `MapSet.new/0`:
|
||||
|
||||
iex> MapSet.new()
|
||||
#MapSet<[]>
|
||||
|
||||
A set can contain any kind of elements, and elements in a set don't have to be
|
||||
of the same type. By definition, sets can't contain duplicate elements: when
|
||||
Elements in a set don't have to be of the same type and they can be
|
||||
populated from an [enumerable](`t:Enumerable.t/0`) using `MapSet.new/1`:
|
||||
|
||||
iex> MapSet.new([1, :two, {"three"}])
|
||||
#MapSet<[1, :two, {"three"}]>
|
||||
|
||||
Elements can be inserted using `MapSet.put/2`:
|
||||
|
||||
iex> MapSet.new([2]) |> MapSet.put(4) |> MapSet.put(0)
|
||||
#MapSet<[0, 2, 4]>
|
||||
|
||||
By definition, sets can't contain duplicate elements: when
|
||||
inserting an element in a set where it's already present, the insertion is
|
||||
simply a no-op.
|
||||
|
||||
@@ -105,7 +117,7 @@ defmodule MapSet do
|
||||
end
|
||||
|
||||
defp new_from_list([], acc) do
|
||||
:maps.from_list(acc)
|
||||
Map.new(acc)
|
||||
end
|
||||
|
||||
defp new_from_list([element | rest], acc) do
|
||||
@@ -113,7 +125,7 @@ defmodule MapSet do
|
||||
end
|
||||
|
||||
defp new_from_list_transform([], _fun, acc) do
|
||||
:maps.from_list(acc)
|
||||
Map.new(acc)
|
||||
end
|
||||
|
||||
defp new_from_list_transform([element | rest], fun, acc) do
|
||||
@@ -151,14 +163,14 @@ defmodule MapSet do
|
||||
@spec difference(t(val1), t(val2)) :: t(val1) when val1: value, val2: value
|
||||
def difference(map_set1, map_set2)
|
||||
|
||||
# If the first set is less than twice the size of the second map,
|
||||
# it is fastest to re-accumulate elements in the first set that are not
|
||||
# present in the second set.
|
||||
# If the first set is less than twice the size of the second map, it is fastest
|
||||
# to re-accumulate elements in the first set that are not present in the second set.
|
||||
def difference(%MapSet{map: map1}, %MapSet{map: map2})
|
||||
when map_size(map1) < map_size(map2) * 2 do
|
||||
map =
|
||||
map1
|
||||
|> Map.keys()
|
||||
|> :maps.iterator()
|
||||
|> :maps.next()
|
||||
|> filter_not_in(map2, [])
|
||||
|
||||
%MapSet{map: map}
|
||||
@@ -171,12 +183,13 @@ defmodule MapSet do
|
||||
%{map_set | map: Map.drop(map1, Map.keys(map2))}
|
||||
end
|
||||
|
||||
defp filter_not_in([], _map2, acc), do: :maps.from_list(acc)
|
||||
defp filter_not_in(:none, _map2, acc), do: Map.new(acc)
|
||||
|
||||
defp filter_not_in([key | rest], map2, acc) do
|
||||
case map2 do
|
||||
%{^key => _} -> filter_not_in(rest, map2, acc)
|
||||
_ -> filter_not_in(rest, map2, [{key, @dummy_value} | acc])
|
||||
defp filter_not_in({key, _val, iter}, map2, acc) do
|
||||
if :erlang.is_map_key(key, map2) do
|
||||
filter_not_in(:maps.next(iter), map2, acc)
|
||||
else
|
||||
filter_not_in(:maps.next(iter), map2, [{key, @dummy_value} | acc])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -196,19 +209,15 @@ defmodule MapSet do
|
||||
{map1, map2} = order_by_size(map1, map2)
|
||||
|
||||
map1
|
||||
|> Map.keys()
|
||||
|> :maps.iterator()
|
||||
|> :maps.next()
|
||||
|> none_in?(map2)
|
||||
end
|
||||
|
||||
defp none_in?([], _) do
|
||||
true
|
||||
end
|
||||
defp none_in?(:none, _), do: true
|
||||
|
||||
defp none_in?([key | rest], map2) do
|
||||
case map2 do
|
||||
%{^key => _} -> false
|
||||
_ -> none_in?(rest, map2)
|
||||
end
|
||||
defp none_in?({key, _val, iter}, map2) do
|
||||
not :erlang.is_map_key(key, map2) and none_in?(:maps.next(iter), map2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -232,7 +241,7 @@ defmodule MapSet do
|
||||
# Elixir v1.5 change the map representation, so on
|
||||
# version mismatch we need to compare the keys directly.
|
||||
def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do
|
||||
map_size(map1) == map_size(map2) and map_subset?(Map.keys(map1), map2)
|
||||
map_size(map1) == map_size(map2) and all_in?(map1, map2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -266,7 +275,7 @@ defmodule MapSet do
|
||||
"""
|
||||
@spec member?(t, value) :: boolean
|
||||
def member?(%MapSet{map: map}, value) do
|
||||
match?(%{^value => _}, map)
|
||||
:erlang.is_map_key(value, map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -314,19 +323,20 @@ defmodule MapSet do
|
||||
"""
|
||||
@spec subset?(t, t) :: boolean
|
||||
def subset?(%MapSet{map: map1}, %MapSet{map: map2}) do
|
||||
if map_size(map1) <= map_size(map2) do
|
||||
map1
|
||||
|> Map.keys()
|
||||
|> map_subset?(map2)
|
||||
else
|
||||
false
|
||||
end
|
||||
map_size(map1) <= map_size(map2) and all_in?(map1, map2)
|
||||
end
|
||||
|
||||
defp map_subset?([], _), do: true
|
||||
defp all_in?(:none, _), do: true
|
||||
|
||||
defp map_subset?([key | rest], map2) do
|
||||
match?(%{^key => _}, map2) and map_subset?(rest, map2)
|
||||
defp all_in?({key, _val, iter}, map2) do
|
||||
:erlang.is_map_key(key, map2) and all_in?(:maps.next(iter), map2)
|
||||
end
|
||||
|
||||
defp all_in?(map1, map2) when is_map(map1) and is_map(map2) do
|
||||
map1
|
||||
|> :maps.iterator()
|
||||
|> :maps.next()
|
||||
|> all_in?(map2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -378,7 +388,8 @@ defmodule MapSet do
|
||||
end
|
||||
|
||||
def slice(map_set) do
|
||||
{:ok, MapSet.size(map_set), &Enumerable.List.slice(MapSet.to_list(map_set), &1, &2)}
|
||||
size = MapSet.size(map_set)
|
||||
{:ok, size, &Enumerable.List.slice(MapSet.to_list(map_set), &1, &2, size)}
|
||||
end
|
||||
|
||||
def reduce(map_set, acc, fun) do
|
||||
|
||||
+85
-26
@@ -56,6 +56,9 @@ defmodule Module do
|
||||
If the behaviour changes or `URI.HTTP` does not implement
|
||||
one of the callbacks, a warning will be raised.
|
||||
|
||||
For detailed documentation, see the
|
||||
[behaviour typespec documentation](typespecs.html#behaviours).
|
||||
|
||||
### `@impl`
|
||||
|
||||
To aid in the correct implementation of behaviours, you may optionally declare
|
||||
@@ -137,7 +140,7 @@ defmodule Module do
|
||||
end
|
||||
|
||||
The Mix compiler automatically looks for calls to deprecated modules
|
||||
and emit warnings during compilation, computed via `mix xref warnings`.
|
||||
and emit warnings during compilation.
|
||||
|
||||
Using the `@deprecated` attribute will also be reflected in the
|
||||
documentation of the given function and macro. You can choose between
|
||||
@@ -346,7 +349,7 @@ defmodule Module do
|
||||
|
||||
In addition to the built-in attributes outlined above, custom attributes may
|
||||
also be added. Custom attributes are expressed using the `@/1` operator followed
|
||||
by a valid variable name. The value given to the custom attribute must be a valid
|
||||
by a valid variable name. The value given to the custom attribute must be a valid
|
||||
Elixir value:
|
||||
|
||||
defmodule MyModule do
|
||||
@@ -370,7 +373,7 @@ defmodule Module do
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__after_compile__/2`.
|
||||
|
||||
Callbacks registered first will run last.
|
||||
Callbacks will run in the order they are registered.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -394,10 +397,11 @@ defmodule Module do
|
||||
When just a module is provided, the function/macro is assumed to be
|
||||
`__before_compile__/1`.
|
||||
|
||||
Callbacks registered first will run last. Any overridable definition
|
||||
will be made concrete before the first callback runs. A definition may
|
||||
be made overridable again in another before compile callback and it
|
||||
will be made concrete one last time after after all callbacks run.
|
||||
Callbacks will run in the order they are registered. Any overridable
|
||||
definition will be made concrete before the first callback runs.
|
||||
A definition may be made overridable again in another before compile
|
||||
callback and it will be made concrete one last time after all callbacks
|
||||
run.
|
||||
|
||||
*Note*: unlike `@after_compile`, the callback function/macro must
|
||||
be placed in a separate module (because when the callback is invoked,
|
||||
@@ -435,10 +439,6 @@ defmodule Module do
|
||||
* the list of quoted guards
|
||||
* the quoted function body
|
||||
|
||||
Note the hook receives the quoted arguments and it is invoked before
|
||||
the function is stored in the module. So `Module.defines?/2` will return
|
||||
`false` for the first clause of every function.
|
||||
|
||||
If the function/macro being defined has multiple clauses, the hook will
|
||||
be called for each clause.
|
||||
|
||||
@@ -482,10 +482,10 @@ defmodule Module do
|
||||
below:
|
||||
|
||||
* `@compile :debug_info` - includes `:debug_info` regardless of the
|
||||
corresponding setting in `Code.compiler_options/1`
|
||||
corresponding setting in `Code.get_compiler_option/1`
|
||||
|
||||
* `@compile {:debug_info, false}` - disables `:debug_info` regardless
|
||||
of the corresponding setting in `Code.compiler_options/1`
|
||||
of the corresponding setting in `Code.get_compiler_option/1`
|
||||
|
||||
* `@compile {:inline, some_fun: 2, other_fun: 3}` - inlines the given
|
||||
name/arity pairs. Inlining is applied locally, calls from another
|
||||
@@ -495,6 +495,10 @@ defmodule Module do
|
||||
modules after compilation. Instead, the module will be loaded after
|
||||
it is dispatched to
|
||||
|
||||
* `@compile {:no_warn_undefined, Mod}` or
|
||||
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
|
||||
the given module or the given `Mod.fun/arity` are not defined
|
||||
|
||||
You can see a handful more options used by the Erlang compiler in
|
||||
the documentation for the [`:compile` module](http://www.erlang.org/doc/man/compile.html).
|
||||
'''
|
||||
@@ -606,7 +610,7 @@ defmodule Module do
|
||||
assert_not_compiled!(__ENV__.function, module)
|
||||
:elixir_def.reset_last(module)
|
||||
|
||||
{value, binding, _env, _scope} =
|
||||
{value, binding, _env} =
|
||||
:elixir.eval_quoted(quoted, binding, Keyword.put(opts, :module, module))
|
||||
|
||||
{value, binding}
|
||||
@@ -1066,7 +1070,7 @@ defmodule Module do
|
||||
"""
|
||||
@spec make_overridable(module, [definition]) :: :ok
|
||||
def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
|
||||
assert_not_compiled!(__ENV__.function, module)
|
||||
assert_not_readonly!(__ENV__.function, module)
|
||||
|
||||
func = fn
|
||||
{function_name, arity} = tuple
|
||||
@@ -1122,7 +1126,7 @@ defmodule Module do
|
||||
behaviour_definitions = bag_lookup_element(bag, {:accumulate, :behaviour}, 2)
|
||||
|
||||
cond do
|
||||
not Code.ensure_compiled?(behaviour) ->
|
||||
Code.ensure_compiled(behaviour) != {:module, behaviour} ->
|
||||
{:error, "it was not defined"}
|
||||
|
||||
not function_exported?(behaviour, :behaviour_info, 1) ->
|
||||
@@ -1227,6 +1231,42 @@ defmodule Module do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the given attribute has been defined.
|
||||
|
||||
An attribute is defined if it has been registered with `register_attribute/3`
|
||||
or assigned a value. If an attribute has been deleted with `delete_attribute/2`
|
||||
it is no longer considered defined.
|
||||
|
||||
This function can only be used on modules that have not yet been compiled.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule MyModule do
|
||||
@value 1
|
||||
Module.register_attribute(__MODULE__, :other_value)
|
||||
Module.put_attribute(__MODULE__, :another_value, 1)
|
||||
|
||||
Module.has_attribute?(__MODULE__, :value) #=> true
|
||||
Module.has_attribute?(__MODULE__, :other_value) #=> true
|
||||
Module.has_attribute?(__MODULE__, :another_value) #=> true
|
||||
|
||||
Module.has_attribute?(__MODULE__, :undefined) #=> false
|
||||
|
||||
Module.delete_attribute(__MODULE__, :value)
|
||||
Module.has_attribute?(__MODULE__, :value) #=> false
|
||||
end
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec has_attribute?(module, atom) :: boolean
|
||||
def has_attribute?(module, key) when is_atom(module) and is_atom(key) do
|
||||
assert_not_compiled!(__ENV__.function, module)
|
||||
{set, _bag} = data_tables_for(module)
|
||||
|
||||
:ets.member(set, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the module attribute that matches the given key.
|
||||
|
||||
@@ -1242,7 +1282,7 @@ defmodule Module do
|
||||
"""
|
||||
@spec delete_attribute(module, atom) :: term
|
||||
def delete_attribute(module, key) when is_atom(module) and is_atom(key) do
|
||||
assert_not_compiled!(__ENV__.function, module)
|
||||
assert_not_readonly!(__ENV__.function, module)
|
||||
{set, bag} = data_tables_for(module)
|
||||
|
||||
case :ets.lookup(set, key) do
|
||||
@@ -1294,7 +1334,7 @@ defmodule Module do
|
||||
@spec register_attribute(module, atom, [{:accumulate, boolean}, {:persist, boolean}]) :: :ok
|
||||
def register_attribute(module, attribute, options)
|
||||
when is_atom(module) and is_atom(attribute) and is_list(options) do
|
||||
assert_not_compiled!(__ENV__.function, module)
|
||||
assert_not_readonly!(__ENV__.function, module)
|
||||
{set, bag} = data_tables_for(module)
|
||||
|
||||
if Keyword.get(options, :persist) do
|
||||
@@ -1304,6 +1344,9 @@ defmodule Module do
|
||||
if Keyword.get(options, :accumulate) do
|
||||
:ets.insert_new(set, {attribute, [], :accumulate}) ||
|
||||
:ets.update_element(set, attribute, {3, :accumulate})
|
||||
else
|
||||
:ets.insert_new(bag, {:warn_attributes, attribute})
|
||||
:ets.insert_new(set, {attribute, nil, :unset})
|
||||
end
|
||||
|
||||
:ok
|
||||
@@ -1494,7 +1537,7 @@ defmodule Module do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp check_behaviours(%{lexical_tracker: pid} = env, behaviours) do
|
||||
defp check_behaviours(env, behaviours) do
|
||||
Enum.reduce(behaviours, %{}, fn behaviour, acc ->
|
||||
cond do
|
||||
not is_atom(behaviour) ->
|
||||
@@ -1504,7 +1547,7 @@ defmodule Module do
|
||||
IO.warn(message, Macro.Env.stacktrace(env))
|
||||
acc
|
||||
|
||||
not Code.ensure_compiled?(behaviour) ->
|
||||
Code.ensure_compiled(behaviour) != {:module, behaviour} ->
|
||||
message =
|
||||
"@behaviour #{inspect(behaviour)} does not exist (in module #{inspect(env.module)})"
|
||||
|
||||
@@ -1519,7 +1562,7 @@ defmodule Module do
|
||||
acc
|
||||
|
||||
true ->
|
||||
:elixir_lexical.record_remote(behaviour, nil, pid)
|
||||
:elixir_env.trace({:require, [], behaviour, []}, env)
|
||||
optional_callbacks = behaviour_info(behaviour, :optional_callbacks)
|
||||
callbacks = behaviour_info(behaviour, :callbacks)
|
||||
Enum.reduce(callbacks, acc, &add_callback(&1, behaviour, env, optional_callbacks, &2))
|
||||
@@ -1771,11 +1814,11 @@ defmodule Module do
|
||||
[{_, _, :accumulate}] ->
|
||||
:lists.reverse(bag_lookup_element(bag, {:accumulate, key}, 2))
|
||||
|
||||
[{_, val, nil}] ->
|
||||
[{_, val, line}] when is_integer(line) ->
|
||||
:ets.update_element(set, key, {3, :used})
|
||||
val
|
||||
|
||||
[{_, val, _}] ->
|
||||
:ets.update_element(set, key, {3, nil})
|
||||
val
|
||||
|
||||
[] when is_integer(line) ->
|
||||
@@ -1796,7 +1839,7 @@ defmodule Module do
|
||||
# Used internally by Kernel's @.
|
||||
# This function is private and must be used only internally.
|
||||
def __put_attribute__(module, key, value, line) when is_atom(key) do
|
||||
assert_not_compiled!(__ENV__.function, module)
|
||||
assert_not_readonly!(__ENV__.function, module)
|
||||
{set, bag} = data_tables_for(module)
|
||||
value = preprocess_attribute(key, value)
|
||||
put_attribute(module, key, value, line, set, bag)
|
||||
@@ -1846,7 +1889,7 @@ defmodule Module do
|
||||
catch
|
||||
:error, :badarg ->
|
||||
:ets.insert(set, {:on_load, value, line})
|
||||
:ets.insert(bag, {:attributes, :on_load})
|
||||
:ets.insert(bag, {:warn_attributes, :on_load})
|
||||
else
|
||||
_ -> raise ArgumentError, "the @on_load attribute can only be set once per module"
|
||||
end
|
||||
@@ -1858,7 +1901,7 @@ defmodule Module do
|
||||
catch
|
||||
:error, :badarg ->
|
||||
:ets.insert(set, {key, value, line})
|
||||
:ets.insert(bag, {:attributes, key})
|
||||
:ets.insert(bag, {:warn_attributes, key})
|
||||
else
|
||||
:accumulate -> :ets.insert(bag, {{:accumulate, key}, value})
|
||||
_ -> :ets.insert(set, {key, value, line})
|
||||
@@ -2041,6 +2084,22 @@ defmodule Module do
|
||||
assert_not_compiled_message(function_name_arity, module, extra_msg)
|
||||
end
|
||||
|
||||
defp assert_not_readonly!({function_name, arity}, module) do
|
||||
case :elixir_module.mode(module) do
|
||||
:all ->
|
||||
:ok
|
||||
|
||||
:readonly ->
|
||||
raise ArgumentError,
|
||||
"could not call Module.#{function_name}/#{arity} because the module " <>
|
||||
"#{inspect(module)} is in read-only mode (@after_compile)"
|
||||
|
||||
:closed ->
|
||||
raise ArgumentError,
|
||||
assert_not_compiled_message({function_name, arity}, module, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_not_compiled_message({function_name, arity}, module, extra_msg) do
|
||||
mfa = "Module.#{function_name}/#{arity}"
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
defmodule Module.Checker do
|
||||
alias Module.ParallelChecker
|
||||
|
||||
@moduledoc false
|
||||
|
||||
def verify(module, cache) do
|
||||
case prepare_module(module) do
|
||||
{:ok, map} ->
|
||||
undefined_and_deprecation_warnings = undefined_and_deprecation_warnings(map, cache)
|
||||
infer_warnings = infer_definitions(map)
|
||||
warnings = infer_warnings ++ undefined_and_deprecation_warnings
|
||||
emit_warnings(warnings)
|
||||
|
||||
:error ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_module({module, module_map}) when is_map(module_map) do
|
||||
{:ok,
|
||||
%{
|
||||
module: module,
|
||||
file: module_map.file,
|
||||
definitions: module_map.definitions,
|
||||
deprecated: module_map.deprecated,
|
||||
no_warn_undefined: no_warn_undefined(module_map.compile_opts)
|
||||
}}
|
||||
end
|
||||
|
||||
defp prepare_module({module, binary}) when is_binary(binary) do
|
||||
with {:ok, debug_info} <- debug_info(module, binary),
|
||||
{:ok, checker_info} <- checker_chunk(binary) do
|
||||
{:ok,
|
||||
%{
|
||||
module: module,
|
||||
file: debug_info.file,
|
||||
definitions: debug_info.definitions,
|
||||
deprecated: checker_info.deprecated,
|
||||
no_warn_undefined: checker_info.no_warn_undefined
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp no_warn_undefined(compile_opts) do
|
||||
for(
|
||||
{:no_warn_undefined, values} <- compile_opts,
|
||||
value <- List.wrap(values),
|
||||
do: value
|
||||
)
|
||||
end
|
||||
|
||||
defp debug_info(module, binary) do
|
||||
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} <- chunk,
|
||||
{:ok, info} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
{:ok, %{definitions: info.definitions, file: info.relative_file}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_chunk(binary) do
|
||||
with {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
deprecated = Enum.map(contents.exports, fn {fun, map} -> {fun, map.deprecated_reason} end)
|
||||
{:ok, %{deprecated: deprecated, no_warn_undefined: contents.no_warn_undefined}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp infer_definitions(map) do
|
||||
results = Module.Types.infer_definitions(map.file, map.module, map.definitions)
|
||||
Enum.flat_map(results, fn {_function, reasons} -> reasons end)
|
||||
end
|
||||
|
||||
defp undefined_and_deprecation_warnings(map, cache) do
|
||||
state = %{
|
||||
cache: cache,
|
||||
file: map.file,
|
||||
module: map.module,
|
||||
no_warn_undefined: merge_no_warn_undefined(map),
|
||||
function: nil,
|
||||
warnings: []
|
||||
}
|
||||
|
||||
state = check_definitions(map.definitions, state)
|
||||
|
||||
state.warnings
|
||||
|> merge_warnings()
|
||||
|> sort_warnings()
|
||||
end
|
||||
|
||||
defp merge_no_warn_undefined(map) do
|
||||
case Code.get_compiler_option(:no_warn_undefined) do
|
||||
:all ->
|
||||
:all
|
||||
|
||||
list when is_list(list) ->
|
||||
map.no_warn_undefined ++ list
|
||||
end
|
||||
end
|
||||
|
||||
defp check_definitions(definitions, state) do
|
||||
Enum.reduce(definitions, state, &check_definition/2)
|
||||
end
|
||||
|
||||
defp check_definition({function, _kind, meta, clauses}, state) do
|
||||
with_file_meta(%{state | function: function}, meta, fn state ->
|
||||
Enum.reduce(clauses, state, &check_clause/2)
|
||||
end)
|
||||
end
|
||||
|
||||
defp with_file_meta(%{file: original_file} = state, meta, fun) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} ->
|
||||
state = fun.(%{state | file: meta_file})
|
||||
%{state | file: original_file}
|
||||
|
||||
:error ->
|
||||
fun.(state)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_clause({_meta, args, _guards, body}, state) do
|
||||
state = check_expr(args, state)
|
||||
check_expr(body, state)
|
||||
end
|
||||
|
||||
# &Mod.fun/arity
|
||||
defp check_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, state)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
check_remote(module, fun, arity, meta, state)
|
||||
end
|
||||
|
||||
# Mod.fun(...)
|
||||
defp check_expr({{:., meta, [module, fun]}, _, args}, state)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
state = check_remote(module, fun, length(args), meta, state)
|
||||
check_expr(args, state)
|
||||
end
|
||||
|
||||
# %Module{...}
|
||||
defp check_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
|
||||
when is_atom(module) and is_list(args) do
|
||||
state = check_remote(module, :__struct__, 0, meta, state)
|
||||
check_expr(args, state)
|
||||
end
|
||||
|
||||
# Function call
|
||||
defp check_expr({left, _meta, right}, state) when is_list(right) do
|
||||
state = check_expr(right, state)
|
||||
check_expr(left, state)
|
||||
end
|
||||
|
||||
# {x, y}
|
||||
defp check_expr({left, right}, state) do
|
||||
state = check_expr(right, state)
|
||||
check_expr(left, state)
|
||||
end
|
||||
|
||||
# [...]
|
||||
defp check_expr(list, state) when is_list(list) do
|
||||
Enum.reduce(list, state, &check_expr/2)
|
||||
end
|
||||
|
||||
defp check_expr(_other, state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp check_remote(module, fun, arity, meta, state) do
|
||||
# TODO: In the future we may want to warn for modules defined
|
||||
# in the local context
|
||||
if Keyword.get(meta, :context_module, false) and state.module != module do
|
||||
state
|
||||
else
|
||||
ParallelChecker.preload_module(state.cache, module)
|
||||
check_export(module, fun, arity, meta, state)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_export(module, fun, arity, meta, state) do
|
||||
case ParallelChecker.fetch_export(state.cache, module, fun, arity) do
|
||||
{:ok, :def, reason} ->
|
||||
check_deprecated(module, fun, arity, reason, meta, state)
|
||||
|
||||
{:ok, :defmacro, reason} ->
|
||||
state = warn(meta, state, {:unrequired_module, module, fun, arity})
|
||||
check_deprecated(module, fun, arity, reason, meta, state)
|
||||
|
||||
{:error, :module} ->
|
||||
if warn_undefined?(module, fun, arity, state) do
|
||||
warn(meta, state, {:undefined_module, module, fun, arity})
|
||||
else
|
||||
state
|
||||
end
|
||||
|
||||
{:error, :function} ->
|
||||
if warn_undefined?(module, fun, arity, state) do
|
||||
exports = ParallelChecker.all_exports(state.cache, module)
|
||||
warn(meta, state, {:undefined_function, module, fun, arity, exports})
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(module, fun, arity, reason, meta, state) do
|
||||
if reason do
|
||||
warn(meta, state, {:deprecated, module, fun, arity, reason})
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Do not warn inside guards
|
||||
# TODO: Properly handle protocols
|
||||
defp warn_undefined?(_module, :__impl__, 1, _state), do: false
|
||||
defp warn_undefined?(_module, :module_info, 0, _state), do: false
|
||||
defp warn_undefined?(_module, :module_info, 1, _state), do: false
|
||||
defp warn_undefined?(:erlang, :orelse, 2, _state), do: false
|
||||
defp warn_undefined?(:erlang, :andalso, 2, _state), do: false
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, state) do
|
||||
not Enum.any?(state.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
defp warn(meta, state, warning) do
|
||||
{fun, arity} = state.function
|
||||
location = {state.file, meta[:line], {state.module, fun, arity}}
|
||||
%{state | warnings: [{__MODULE__, warning, location} | state.warnings]}
|
||||
end
|
||||
|
||||
defp merge_warnings(warnings) do
|
||||
Enum.reduce(warnings, %{}, fn {module, warning, location}, acc ->
|
||||
locations = MapSet.new([location])
|
||||
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
|
||||
end)
|
||||
end
|
||||
|
||||
defp sort_warnings(warnings) do
|
||||
warnings
|
||||
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp emit_warnings(warnings) do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
message = module.format_warning(warning)
|
||||
print_warning([message, ?\n, format_locations(locations)])
|
||||
|
||||
Enum.map(locations, fn {file, line, _mfa} ->
|
||||
{file, line, message}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
def format_warning({:undefined_module, module, fun, arity}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)"
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:undefined_function, module, fun, arity, exports}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined or private",
|
||||
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:deprecated, module, fun, arity, reason}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is deprecated. ",
|
||||
reason
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:unrequired_module, module, fun, arity}) do
|
||||
[
|
||||
"you must require ",
|
||||
inspect(module),
|
||||
" before invoking the macro ",
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
]
|
||||
end
|
||||
|
||||
defp format_locations([location]) do
|
||||
format_location(location)
|
||||
end
|
||||
|
||||
defp format_locations(locations) do
|
||||
[
|
||||
"Found at #{length(locations)} locations:\n",
|
||||
Enum.map(locations, &format_location/1)
|
||||
]
|
||||
end
|
||||
|
||||
defp format_location({file, line, {module, fun, arity}}) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line, do: [Integer.to_string(line), ": "], else: []
|
||||
mfa = Exception.format_mfa(module, fun, arity)
|
||||
[" ", file, ?:, line, mfa, ?\n]
|
||||
end
|
||||
|
||||
defp print_warning(message) do
|
||||
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,296 @@
|
||||
defmodule Module.ParallelChecker do
|
||||
@moduledoc false
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type kind() :: :def | :defmacro
|
||||
|
||||
@doc """
|
||||
Receives pairs of module maps and BEAM binaries. In parallel it verifies
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
binaries and a list of warnings from the verification.
|
||||
"""
|
||||
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer()) :: [warning()]
|
||||
def verify(compiled_modules, runtime_binaries, schedulers \\ nil) do
|
||||
compiled_maps = Enum.map(compiled_modules, fn {map, _binary} -> {map.module, map} end)
|
||||
check_modules = compiled_maps ++ runtime_binaries
|
||||
|
||||
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
|
||||
{:ok, server} = :gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
|
||||
preload_cache(get_ets(server), check_modules)
|
||||
start(server)
|
||||
|
||||
collect_results(length(check_modules), [])
|
||||
end
|
||||
|
||||
defp collect_results(0, warnings) do
|
||||
warnings
|
||||
end
|
||||
|
||||
defp collect_results(count, warnings) do
|
||||
receive do
|
||||
{__MODULE__, _module, new_warnings} ->
|
||||
collect_results(count - 1, new_warnings ++ warnings)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Preloads a module into the cache. Call this function before any other
|
||||
cache lookups for the module.
|
||||
"""
|
||||
@spec preload_module(cache(), module()) :: :ok
|
||||
def preload_module({server, ets}, module) do
|
||||
case :ets.lookup(ets, {:cached, module}) do
|
||||
[{_key, _}] -> :ok
|
||||
[] -> cache_module({server, ets}, module)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the export kind and deprecation reason for the given MFA from
|
||||
the cache. If the module does not exist return `{:error, :module}`,
|
||||
or if the function does not exist return `{:error, :function}`.
|
||||
"""
|
||||
@spec fetch_export(cache(), module(), atom(), arity()) ::
|
||||
{:ok, kind(), binary() | nil} | {:error, :function | :module}
|
||||
def fetch_export({_server, ets}, module, fun, arity) do
|
||||
case :ets.lookup(ets, {:cached, module}) do
|
||||
[{_key, true}] ->
|
||||
case :ets.lookup(ets, {:export, {module, fun, arity}}) do
|
||||
[{_key, kind, reason}] -> {:ok, kind, reason}
|
||||
[] -> {:error, :function}
|
||||
end
|
||||
|
||||
[{_key, false}] ->
|
||||
{:error, :module}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all exported functions and macros for the given module from
|
||||
the cache.
|
||||
"""
|
||||
@spec all_exports(cache(), module()) :: [{atom(), arity()}]
|
||||
def all_exports({_server, ets}, module) do
|
||||
# This is only called after we get a deprecation notice
|
||||
# so we can assume it's a cached module
|
||||
[{_key, exports}] = :ets.lookup(ets, {:all_exports, module})
|
||||
|
||||
exports
|
||||
|> Enum.map(fn {function, _kind} -> function end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
def init([modules, send_results, schedulers]) do
|
||||
ets = :ets.new(:checker_cache, [:set, :public, {:read_concurrency, true}])
|
||||
|
||||
state = %{
|
||||
ets: ets,
|
||||
waiting: %{},
|
||||
send_results: send_results,
|
||||
modules: modules,
|
||||
spawned: 0,
|
||||
schedulers: schedulers
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
|
||||
case waiting do
|
||||
%{^module => froms} ->
|
||||
waiting = Map.put(state.waiting, module, [from | froms])
|
||||
{:noreply, %{state | waiting: waiting}}
|
||||
|
||||
%{} ->
|
||||
waiting = Map.put(state.waiting, module, [])
|
||||
{:reply, true, %{state | waiting: waiting}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
|
||||
froms = Map.fetch!(waiting, module)
|
||||
Enum.each(froms, &:gen_server.reply(&1, false))
|
||||
waiting = Map.delete(waiting, module)
|
||||
{:reply, :ok, %{state | waiting: waiting}}
|
||||
end
|
||||
|
||||
def handle_call(:get_ets, _from, %{ets: ets} = state) do
|
||||
{:reply, ets, state}
|
||||
end
|
||||
|
||||
def handle_cast(:start, %{modules: []} = state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast(:start, state) do
|
||||
{:noreply, spawn_checkers(state)}
|
||||
end
|
||||
|
||||
def handle_info({__MODULE__, :done}, state) do
|
||||
state = %{state | spawned: state.spawned - 1}
|
||||
|
||||
if state.spawned == 0 and state.modules == [] do
|
||||
{:stop, :normal, state}
|
||||
else
|
||||
state = spawn_checkers(state)
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp lock(server, module) do
|
||||
:gen_server.call(server, {:lock, module}, :infinity)
|
||||
end
|
||||
|
||||
defp unlock(server, module) do
|
||||
:gen_server.call(server, {:unlock, module})
|
||||
end
|
||||
|
||||
defp get_ets(server) do
|
||||
:gen_server.call(server, :get_ets)
|
||||
end
|
||||
|
||||
defp start(server) do
|
||||
:gen_server.cast(server, :start)
|
||||
end
|
||||
|
||||
defp preload_cache(ets, modules) do
|
||||
Enum.each(modules, fn
|
||||
{_module, map} when is_map(map) -> cache_from_module_map(ets, map)
|
||||
{module, binary} when is_binary(binary) -> cache_from_chunk(ets, module, binary)
|
||||
end)
|
||||
end
|
||||
|
||||
defp spawn_checkers(%{modules: []} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp spawn_checkers(%{spawned: spawned, schedulers: schedulers} = state)
|
||||
when spawned >= schedulers do
|
||||
state
|
||||
end
|
||||
|
||||
defp spawn_checkers(%{modules: [{module, _} = verify | modules]} = state) do
|
||||
parent = self()
|
||||
ets = state.ets
|
||||
send_results_pid = state.send_results
|
||||
|
||||
spawn_link(fn ->
|
||||
warnings = Module.Checker.verify(verify, {parent, ets})
|
||||
send(send_results_pid, {__MODULE__, module, warnings})
|
||||
send(parent, {__MODULE__, :done})
|
||||
end)
|
||||
|
||||
spawn_checkers(%{state | modules: modules, spawned: state.spawned + 1})
|
||||
end
|
||||
|
||||
defp cache_module({server, ets}, module) do
|
||||
if lock(server, module) do
|
||||
cache_from_chunk(ets, module) || cache_from_info(ets, module)
|
||||
unlock(server, module)
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_chunk(ets, module) do
|
||||
case :code.get_object_code(module) do
|
||||
{^module, binary, _filename} -> cache_from_chunk(ets, module, binary)
|
||||
_other -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_chunk(ets, module, binary) do
|
||||
with {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
cache_chunk(ets, module, contents.exports)
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_module_map(ets, map) do
|
||||
exports =
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(map) ++
|
||||
definitions_to_exports(map.definitions)
|
||||
|
||||
deprecated = Map.new(map.deprecated)
|
||||
cache_info(ets, map.module, exports, deprecated)
|
||||
end
|
||||
|
||||
defp cache_from_info(ets, module) do
|
||||
if Code.ensure_loaded?(module) do
|
||||
exports = info_exports(module)
|
||||
deprecated = info_deprecated(module)
|
||||
cache_info(ets, module, exports, deprecated)
|
||||
else
|
||||
:ets.insert(ets, {{:cached, module}, false})
|
||||
end
|
||||
end
|
||||
|
||||
defp info_exports(module) do
|
||||
Map.new(
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(module) ++
|
||||
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
|
||||
Enum.map(module.__info__(:functions), &{&1, :def})
|
||||
)
|
||||
rescue
|
||||
_ -> Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))
|
||||
end
|
||||
|
||||
defp info_deprecated(module) do
|
||||
Map.new(module.__info__(:deprecated))
|
||||
rescue
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
defp cache_info(ets, module, exports, deprecated) do
|
||||
exports =
|
||||
Enum.map(exports, fn {{fun, arity}, kind} ->
|
||||
reason = Map.get(deprecated, {fun, arity})
|
||||
:ets.insert(ets, {{:export, {module, fun, arity}}, kind, reason})
|
||||
|
||||
{{fun, arity}, kind}
|
||||
end)
|
||||
|
||||
:ets.insert(ets, {{:all_exports, module}, exports})
|
||||
:ets.insert(ets, {{:cached, module}, true})
|
||||
end
|
||||
|
||||
defp cache_chunk(ets, module, exports) do
|
||||
exports =
|
||||
Enum.map(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
|
||||
:ets.insert(ets, {{:export, {module, fun, arity}}, kind, reason})
|
||||
|
||||
{{fun, arity}, kind}
|
||||
end)
|
||||
|
||||
:ets.insert(ets, {{:export, {module, :__info__, 1}}, :def, nil})
|
||||
exports = [{{:__info__, 1}, :def} | exports]
|
||||
|
||||
:ets.insert(ets, {{:all_exports, module}, exports})
|
||||
:ets.insert(ets, {{:cached, module}, true})
|
||||
end
|
||||
|
||||
defp behaviour_exports(%{is_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
|
||||
defp behaviour_exports(%{is_behaviour: false}), do: []
|
||||
|
||||
defp behaviour_exports(module) when is_atom(module) do
|
||||
if {:behaviour_info, 1} in module.module_info(:functions) do
|
||||
[{{:behaviour_info, 1}, :def}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp definitions_to_exports(definitions) do
|
||||
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
|
||||
if kind in [:def, :defmacro] do
|
||||
[{function, kind}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,387 @@
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.Helpers
|
||||
alias Module.Types.{Expr, Pattern}
|
||||
|
||||
@doc """
|
||||
Infer function definitions' types.
|
||||
"""
|
||||
def infer_definitions(file, module, defs) do
|
||||
clauses = infer_signatures(file, module, defs)
|
||||
infer_bodies(clauses)
|
||||
end
|
||||
|
||||
defp infer_signatures(file, module, defs) do
|
||||
Enum.map(defs, fn {{fun, _arity} = function, kind, meta, clauses} ->
|
||||
stack = head_stack()
|
||||
context = head_context(file, module, function)
|
||||
|
||||
clauses =
|
||||
Enum.map(clauses, fn {_meta, params, guards, body} ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], params})]}
|
||||
stack = push_expr_stack(def_expr, stack)
|
||||
|
||||
case of_head(params, guards, stack, context) do
|
||||
{:ok, _signature, context} -> {:ok, {context, body}}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end)
|
||||
|
||||
{function, clauses}
|
||||
end)
|
||||
end
|
||||
|
||||
defp infer_bodies(clauses) do
|
||||
Enum.map(clauses, fn {function, clauses} ->
|
||||
errors =
|
||||
Enum.flat_map(clauses, fn
|
||||
{:ok, {head_context, body}} ->
|
||||
stack = body_stack()
|
||||
context = body_context(head_context)
|
||||
|
||||
case Expr.of_expr(body, stack, context) do
|
||||
{:ok, _type, _context} -> []
|
||||
{:error, reason} -> [reason]
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
[reason]
|
||||
end)
|
||||
|
||||
{function, errors}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def of_head(params, guards, stack, context) do
|
||||
with {:ok, types, context} <-
|
||||
map_reduce_ok(params, context, &Pattern.of_pattern(&1, stack, &2)),
|
||||
# TODO: Check that of_guard/3 returns boolean() | :fail
|
||||
{:ok, _, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
|
||||
do: {:ok, lift_types(types, context), context}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def head_context(file, module, function) do
|
||||
%{
|
||||
# File of module
|
||||
file: file,
|
||||
# Module of definitions
|
||||
module: module,
|
||||
# Current function
|
||||
function: function,
|
||||
# Expression variable to type variable
|
||||
vars: %{},
|
||||
# Type variable to expression variable
|
||||
types_to_vars: %{},
|
||||
# Type variable to type
|
||||
types: %{},
|
||||
# Trace of all variables that have been refined to a type,
|
||||
# including the type they were refined to, why, and where
|
||||
traces: %{},
|
||||
# Counter to give type variables unique names
|
||||
counter: 0,
|
||||
# Track if a variable was infered from a type guard function such is_tuple/1
|
||||
# or a guard function that fails such as elem/2, possible values are:
|
||||
# `:guarded` when `is_tuple(x)`
|
||||
# `:fail` when `elem(x, 0)`
|
||||
# `:guarded_fail` when `is_tuple and elem(x, 0)`
|
||||
guard_sources: %{}
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def head_stack() do
|
||||
%{
|
||||
# Stack of expression we have recursed through during inference,
|
||||
# used for tracing
|
||||
expr_stack: [],
|
||||
# When false do not add a trace when a type variable is refined,
|
||||
# useful when merging contexts where the variables already have traces
|
||||
trace: true,
|
||||
# Track if we are in a context where type guard functions should
|
||||
# affect inference
|
||||
type_guards_enabled?: true,
|
||||
# Context used to determine if unification is bi-directional, :expr
|
||||
# is directional, :pattern is bi-directional
|
||||
context: :pattern
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def body_context(head_context) do
|
||||
%{
|
||||
# File of module
|
||||
file: head_context.file,
|
||||
# Module of definitions
|
||||
module: head_context.module,
|
||||
# Current function
|
||||
function: head_context.function,
|
||||
# Expression variable to type variable
|
||||
vars: head_context.vars,
|
||||
# Type variable to expression variable
|
||||
types_to_vars: head_context.types_to_vars,
|
||||
# Type variable to type
|
||||
types: head_context.types,
|
||||
# Trace of all variables that have been refined to a type,
|
||||
# including the type they were refined to, why, and where
|
||||
traces: head_context.traces,
|
||||
# Counter to give type variables unique names
|
||||
counter: head_context.counter
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def body_stack() do
|
||||
%{
|
||||
# Stack of expression we have recursed through during inference,
|
||||
# used for tracing
|
||||
expr_stack: [],
|
||||
# When false do not add a trace when a type variable is refined,
|
||||
# useful when merging contexts where the variables already have traces
|
||||
trace: true,
|
||||
# Context used to determine if unification is bi-directional, :expr
|
||||
# is directional, :pattern is bi-directional
|
||||
context: :expr
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lifts type variables to their infered types from the context.
|
||||
"""
|
||||
def lift_types(types, context) do
|
||||
context = %{
|
||||
types: context.types,
|
||||
lifted_types: %{},
|
||||
lifted_counter: 0
|
||||
}
|
||||
|
||||
{types, _context} = Enum.map_reduce(types, context, &do_lift_type/2)
|
||||
types
|
||||
end
|
||||
|
||||
@doc false
|
||||
def lift_type(type, context) do
|
||||
context = %{
|
||||
types: context.types,
|
||||
lifted_types: %{},
|
||||
lifted_counter: 0
|
||||
}
|
||||
|
||||
{type, _context} = do_lift_type(type, context)
|
||||
type
|
||||
end
|
||||
|
||||
## GUARDS
|
||||
|
||||
# TODO: Remove this and let multiple when be treated as multiple clauses,
|
||||
# meaning they will be intersection types
|
||||
defp guards_to_or([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp guards_to_or(guards) do
|
||||
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
|
||||
end
|
||||
|
||||
defp guards_to_expr([], left) do
|
||||
left
|
||||
end
|
||||
|
||||
defp guards_to_expr([guard | guards], left) do
|
||||
guards_to_expr(guards, {:when, [], [left, guard]})
|
||||
end
|
||||
|
||||
## VARIABLE LIFTING
|
||||
|
||||
# Lift type variable to its infered (hopefully concrete) types from the context
|
||||
defp do_lift_type({:var, var}, context) do
|
||||
case Map.fetch(context.lifted_types, var) do
|
||||
{:ok, lifted_var} ->
|
||||
{{:var, lifted_var}, context}
|
||||
|
||||
:error ->
|
||||
case Map.fetch(context.types, var) do
|
||||
{:ok, :unbound} ->
|
||||
new_lifted_var(var, context)
|
||||
|
||||
{:ok, type} ->
|
||||
# Remove visited types to avoid infinite loops
|
||||
# then restore after we are done recursing on vars
|
||||
types = context.types
|
||||
context = %{context | types: Map.delete(context.types, var)}
|
||||
{type, context} = do_lift_type(type, context)
|
||||
{type, %{context | types: types}}
|
||||
|
||||
:error ->
|
||||
new_lifted_var(var, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_lift_type({:tuple, types}, context) do
|
||||
{types, context} = Enum.map_reduce(types, context, &do_lift_type/2)
|
||||
{{:tuple, types}, context}
|
||||
end
|
||||
|
||||
defp do_lift_type({:map, pairs}, context) do
|
||||
{pairs, context} =
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{key, context} = do_lift_type(key, context)
|
||||
{value, context} = do_lift_type(value, context)
|
||||
{{key, value}, context}
|
||||
end)
|
||||
|
||||
{{:map, pairs}, context}
|
||||
end
|
||||
|
||||
defp do_lift_type({:list, type}, context) do
|
||||
{type, context} = do_lift_type(type, context)
|
||||
{{:list, type}, context}
|
||||
end
|
||||
|
||||
defp do_lift_type(other, context) do
|
||||
{other, context}
|
||||
end
|
||||
|
||||
defp new_lifted_var(original_var, context) do
|
||||
types = Map.put(context.lifted_types, original_var, context.lifted_counter)
|
||||
counter = context.lifted_counter + 1
|
||||
|
||||
type = {:var, context.lifted_counter}
|
||||
context = %{context | lifted_types: types, lifted_counter: counter}
|
||||
{type, context}
|
||||
end
|
||||
|
||||
## ERROR FORMATTING
|
||||
|
||||
def format_warning({:unable_unify, left, right, expr, traces}) do
|
||||
[
|
||||
"incompatible types:\n\n ",
|
||||
format_type(left),
|
||||
" !~ ",
|
||||
format_type(right),
|
||||
"\n\n",
|
||||
format_expr(expr),
|
||||
format_traces(traces),
|
||||
"Conflict found at"
|
||||
]
|
||||
end
|
||||
|
||||
defp format_expr(nil) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp format_expr(expr) do
|
||||
[
|
||||
"in expression:\n\n ",
|
||||
expr_to_string(expr),
|
||||
"\n\n"
|
||||
]
|
||||
end
|
||||
|
||||
defp format_traces([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp format_traces(traces) do
|
||||
Enum.map(traces, fn
|
||||
{var, {:type, type, expr, location}} ->
|
||||
[
|
||||
"where \"",
|
||||
Macro.to_string(var),
|
||||
"\" was given the type ",
|
||||
Module.Types.format_type(type),
|
||||
" in:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
expr_to_string(expr),
|
||||
"\n\n"
|
||||
]
|
||||
|
||||
{var1, {:var, var2, expr, location}} ->
|
||||
[
|
||||
"where \"",
|
||||
Macro.to_string(var1),
|
||||
"\" was given the same type as \"",
|
||||
Macro.to_string(var2),
|
||||
"\" in:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
expr_to_string(expr),
|
||||
"\n\n"
|
||||
]
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_location({file, line}) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line, do: [Integer.to_string(line)], else: []
|
||||
[file, ?:, line, ?\n]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_type({:union, types}) do
|
||||
"#{Enum.map_join(types, " | ", &format_type/1)}"
|
||||
end
|
||||
|
||||
def format_type({:tuple, types}) do
|
||||
"{#{Enum.map_join(types, ", ", &format_type/1)}}"
|
||||
end
|
||||
|
||||
def format_type({:list, type}) do
|
||||
"[#{format_type(type)}]"
|
||||
end
|
||||
|
||||
def format_type({:map, pairs}) do
|
||||
case List.keytake(pairs, :__struct__, 0) do
|
||||
{{:__struct__, struct}, pairs} ->
|
||||
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
|
||||
|
||||
nil ->
|
||||
"%{#{format_map_pairs(pairs)}}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_type({:atom, literal}) do
|
||||
inspect(literal)
|
||||
end
|
||||
|
||||
def format_type(atom) when is_atom(atom) do
|
||||
"#{atom}()"
|
||||
end
|
||||
|
||||
def format_type({:var, index}) do
|
||||
"var#{index}"
|
||||
end
|
||||
|
||||
defp format_map_pairs(pairs) do
|
||||
Enum.map_join(pairs, ", ", fn {left, right} ->
|
||||
"#{format_type(left)} => #{format_type(right)}"
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def expr_to_string(expr) do
|
||||
expr
|
||||
|> reverse_rewrite()
|
||||
|> Macro.to_string()
|
||||
end
|
||||
|
||||
defp reverse_rewrite(guard) do
|
||||
Macro.prewalk(guard, fn
|
||||
{:., _, [:erlang, :orelse]} -> :or
|
||||
{:., _, [:erlang, :andalso]} -> :and
|
||||
{{:., _, [mod, fun]}, _, args} -> erl_to_ex(mod, fun, args)
|
||||
other -> other
|
||||
end)
|
||||
end
|
||||
|
||||
defp erl_to_ex(mod, fun, args) do
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
|
||||
{Kernel, fun, args} -> {fun, [], args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, [], args}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
defmodule Module.Types.Expr do
|
||||
@moduledoc false
|
||||
|
||||
def of_expr(_expr, _stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
defmodule Module.Types.Helpers do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Guard function to check if an AST node is a variable.
|
||||
"""
|
||||
defmacro is_var(expr) do
|
||||
quote do
|
||||
is_tuple(unquote(expr)) and
|
||||
tuple_size(unquote(expr)) == 3 and
|
||||
is_atom(elem(unquote(expr), 0)) and
|
||||
is_atom(elem(unquote(expr), 2))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns unique identifier for the current assignment of the variable.
|
||||
"""
|
||||
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
|
||||
|
||||
@doc """
|
||||
Push expression to stack.
|
||||
|
||||
The expression stack is used to give the context where a type variable
|
||||
was refined when show a type conflict error.
|
||||
"""
|
||||
def push_expr_stack(expr, stack) do
|
||||
%{stack | expr_stack: [expr | stack.expr_stack]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
|
||||
and stops on `{:error, reason}`.
|
||||
"""
|
||||
def reduce_ok(list, acc, fun) do
|
||||
do_reduce_ok(list, acc, fun)
|
||||
end
|
||||
|
||||
defp do_reduce_ok([head | tail], acc, fun) do
|
||||
case fun.(head, acc) do
|
||||
{:ok, acc} ->
|
||||
do_reduce_ok(tail, acc, fun)
|
||||
|
||||
result when elem(result, 0) == :ok ->
|
||||
result = Tuple.delete_at(result, 0)
|
||||
do_reduce_ok(tail, result, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_reduce_ok([], acc, _fun), do: {:ok, acc}
|
||||
|
||||
@doc """
|
||||
Like `Enum.unzip/1` but only continues while `fun` returns `{:ok, elem1, elem2}`
|
||||
and stops on `{:error, reason}`.
|
||||
"""
|
||||
def unzip_ok(list) do
|
||||
do_unzip_ok(list, [], [])
|
||||
end
|
||||
|
||||
defp do_unzip_ok([{:ok, head1, head2} | tail], acc1, acc2) do
|
||||
do_unzip_ok(tail, [head1 | acc1], [head2 | acc2])
|
||||
end
|
||||
|
||||
defp do_unzip_ok([{:error, reason} | _tail], _acc1, _acc2), do: {:error, reason}
|
||||
|
||||
defp do_unzip_ok([], acc1, acc2), do: {:ok, Enum.reverse(acc1), Enum.reverse(acc2)}
|
||||
|
||||
@doc """
|
||||
Like `Enum.map/2` but only continues while `fun` returns `{:ok, elem}`
|
||||
and stops on `{:error, reason}`.
|
||||
"""
|
||||
def map_ok(list, fun) do
|
||||
do_map_ok(list, [], fun)
|
||||
end
|
||||
|
||||
defp do_map_ok([head | tail], acc, fun) do
|
||||
case fun.(head) do
|
||||
{:ok, elem} ->
|
||||
do_map_ok(tail, [elem | acc], fun)
|
||||
|
||||
result when elem(result, 0) == :ok ->
|
||||
result = Tuple.delete_at(result, 0)
|
||||
do_map_ok(tail, [result | acc], fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_map_ok([], acc, _fun), do: {:ok, Enum.reverse(acc)}
|
||||
|
||||
@doc """
|
||||
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
|
||||
and stops on `{:error, reason}`.
|
||||
"""
|
||||
def map_reduce_ok(list, acc, fun) do
|
||||
do_map_reduce_ok(list, {[], acc}, fun)
|
||||
end
|
||||
|
||||
defp do_map_reduce_ok([head | tail], {list, acc}, fun) do
|
||||
case fun.(head, acc) do
|
||||
{:ok, elem, acc} ->
|
||||
do_map_reduce_ok(tail, {[elem | list], acc}, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
|
||||
|
||||
@doc """
|
||||
Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of
|
||||
errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}`
|
||||
if there are no errors.
|
||||
"""
|
||||
def oks_or_errors(list) do
|
||||
case Enum.split_with(list, &match?({:ok, _}, &1)) do
|
||||
{oks, []} -> {:ok, Enum.map(oks, fn {:ok, ok} -> ok end)}
|
||||
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,356 @@
|
||||
defmodule Module.Types.Infer do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.Helpers
|
||||
|
||||
@doc """
|
||||
Unifies two types and returns the unified type and an updated typing context
|
||||
or an error in case of a typing conflict.
|
||||
"""
|
||||
def unify(source, target, stack, context) do
|
||||
case do_unify(source, target, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, reason} ->
|
||||
if stack.context == :pattern do
|
||||
case do_unify(target, source, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(same, same, _stack, context) do
|
||||
{:ok, same, context}
|
||||
end
|
||||
|
||||
defp do_unify({:var, var}, type, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = true)
|
||||
end
|
||||
|
||||
defp do_unify(type, {:var, var}, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = false)
|
||||
end
|
||||
|
||||
defp do_unify({:tuple, sources}, {:tuple, targets}, stack, context)
|
||||
when length(sources) == length(targets) do
|
||||
result =
|
||||
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
|
||||
unify(source, target, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, types, context} -> {:ok, {:tuple, types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:list, source}, {:list, target}, stack, context) do
|
||||
case unify(source, target, stack, context) do
|
||||
{:ok, type, context} -> {:ok, {:list, type}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
|
||||
# Since maps in patterns only support literal keys (excluding maps)
|
||||
# we can do exact type match without subtype checking
|
||||
|
||||
unique_right_pairs =
|
||||
Enum.reject(target_pairs, fn {key, _value} ->
|
||||
:lists.keyfind(key, 1, source_pairs)
|
||||
end)
|
||||
|
||||
unique_pairs = source_pairs ++ unique_right_pairs
|
||||
|
||||
# Build union of all unique key-value pairs between the maps
|
||||
result =
|
||||
map_reduce_ok(unique_pairs, context, fn {source_key, source_value}, context ->
|
||||
case :lists.keyfind(source_key, 1, target_pairs) do
|
||||
{^source_key, target_value} ->
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} -> {:ok, {source_key, value}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
||||
false ->
|
||||
{:ok, {source_key, source_value}, context}
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, pairs, context} -> {:ok, {:map, pairs}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(source, :dynamic, _stack, context) do
|
||||
{:ok, source, context}
|
||||
end
|
||||
|
||||
defp do_unify(source, target, stack, context) do
|
||||
if subtype?(source, target, context) do
|
||||
{:ok, source, context}
|
||||
else
|
||||
error({:unable_unify, source, target}, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_var(var, type, stack, context, var_source?) do
|
||||
case Map.fetch!(context.types, var) do
|
||||
:unbound ->
|
||||
context = refine_var(var, type, stack, context)
|
||||
|
||||
if recursive_type?(type, [], context) do
|
||||
if var_source? do
|
||||
error({:unable_unify, {:var, var}, type}, stack, context)
|
||||
else
|
||||
error({:unable_unify, type, {:var, var}}, stack, context)
|
||||
end
|
||||
else
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
var_type ->
|
||||
context = trace_var(var, type, stack, context)
|
||||
|
||||
unify_result =
|
||||
if var_source? do
|
||||
unify(var_type, type, stack, context)
|
||||
else
|
||||
unify(type, var_type, stack, context)
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, var_type, context} ->
|
||||
context = refine_var(var, var_type, stack, context)
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a variable to the typing context and returns its type variables.
|
||||
If the variable has already been added, return the existing type variable.
|
||||
"""
|
||||
def new_var(var, context) do
|
||||
case Map.fetch(context.vars, var_name(var)) do
|
||||
{:ok, type} ->
|
||||
{type, context}
|
||||
|
||||
:error ->
|
||||
type = {:var, context.counter}
|
||||
vars = Map.put(context.vars, var_name(var), type)
|
||||
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
|
||||
types = Map.put(context.types, context.counter, :unbound)
|
||||
traces = Map.put(context.traces, context.counter, [])
|
||||
|
||||
context = %{
|
||||
context
|
||||
| vars: vars,
|
||||
types_to_vars: types_to_vars,
|
||||
types: types,
|
||||
traces: traces,
|
||||
counter: context.counter + 1
|
||||
}
|
||||
|
||||
{type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set the type for a variable and add trace.
|
||||
"""
|
||||
def refine_var(var, type, stack, context) do
|
||||
types = Map.put(context.types, var, type)
|
||||
context = %{context | types: types}
|
||||
trace_var(var, type, stack, context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove type variable and all its traces.
|
||||
"""
|
||||
def remove_var(var, context) do
|
||||
types = Map.delete(context.types, var)
|
||||
traces = Map.delete(context.traces, var)
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(var, type, %{trace: true, expr_stack: expr_stack} = _stack, context) do
|
||||
line = get_meta(hd(expr_stack))[:line]
|
||||
trace = {type, expr_stack, {context.file, line}}
|
||||
traces = Map.update!(context.traces, var, &[trace | &1])
|
||||
%{context | traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
# Check if a variable is recursive and incompatible with itself
|
||||
# Bad: `{var} = var`
|
||||
# Good: `x = y; y = z; z = x`
|
||||
defp recursive_type?({:var, var} = parent, parents, context) do
|
||||
case Map.fetch!(context.types, var) do
|
||||
:unbound ->
|
||||
false
|
||||
|
||||
type ->
|
||||
if type in parents do
|
||||
not Enum.all?(parents, &match?({:var, _}, &1))
|
||||
else
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recursive_type?({:list, type} = parent, parents, context) do
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
|
||||
defp recursive_type?({:tuple, types} = parent, parents, context) do
|
||||
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
|
||||
end
|
||||
|
||||
defp recursive_type?({:map, pairs} = parent, parents, context) do
|
||||
Enum.any?(pairs, fn {key, value} ->
|
||||
recursive_type?(key, [parent | parents], context) or
|
||||
recursive_type?(value, [parent | parents], context)
|
||||
end)
|
||||
end
|
||||
|
||||
defp recursive_type?(_other, _parents, _context) do
|
||||
false
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the first argument is a subtype of the second argument.
|
||||
Only checks for simple and concrete types.
|
||||
"""
|
||||
def subtype?({:atom, boolean}, :boolean, _context) when is_boolean(boolean), do: true
|
||||
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
|
||||
def subtype?(:boolean, :atom, _context), do: true
|
||||
def subtype?(:float, :number, _context), do: true
|
||||
def subtype?(:integer, :number, _context), do: true
|
||||
def subtype?({:tuple, _}, :tuple, _context), do: true
|
||||
|
||||
# TODO: Lift unions to unify/3?
|
||||
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
|
||||
Enum.all?(left_types, &subtype?(&1, right_union, context))
|
||||
end
|
||||
|
||||
def subtype?(left, {:union, right_types}, context) do
|
||||
Enum.any?(right_types, &subtype?(left, &1, context))
|
||||
end
|
||||
|
||||
def subtype?(left, right, _context), do: left == right
|
||||
|
||||
@doc """
|
||||
Returns a "simplified" union using `subtype?/3` to remove redundant types.
|
||||
|
||||
Due to limitations in `subtype?/3` some overlapping types may still be
|
||||
included. For example unions with overlapping non-concrete types such as
|
||||
`{boolean()} | {atom()}` will not be merged or types with variables that
|
||||
are distinct but equivalent such as `a | b when a ~ b`.
|
||||
"""
|
||||
# TODO: Translate union of all top types to dynamic()
|
||||
def to_union(types, context) when types != [] do
|
||||
if :dynamic in types do
|
||||
:dynamic
|
||||
else
|
||||
case unique_super_types(flatten_union(types), context) do
|
||||
[type] -> type
|
||||
types -> {:union, types}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp flatten_union(types) do
|
||||
Enum.flat_map(types, fn
|
||||
{:union, types} -> flatten_union(types)
|
||||
type -> [type]
|
||||
end)
|
||||
end
|
||||
|
||||
# Filter subtypes
|
||||
# `boolean() | atom()` => `atom()`
|
||||
# `:foo | atom()` => `atom()`
|
||||
# Does not unify `true | false` => `boolean()`
|
||||
defp unique_super_types([type | types], context) do
|
||||
types = Enum.reject(types, &subtype?(&1, type, context))
|
||||
|
||||
if Enum.any?(types, &subtype?(type, &1, context)) do
|
||||
unique_super_types(types, context)
|
||||
else
|
||||
[type | unique_super_types(types, context)]
|
||||
end
|
||||
end
|
||||
|
||||
defp unique_super_types([], _context) do
|
||||
[]
|
||||
end
|
||||
|
||||
# Collect relevant information from context and traces to report error
|
||||
defp error({:unable_unify, left, right}, stack, context) do
|
||||
{fun, arity} = context.function
|
||||
line = get_meta(hd(stack.expr_stack))[:line]
|
||||
location = {context.file, line, {context.module, fun, arity}}
|
||||
|
||||
traces = type_traces(context)
|
||||
common_expr = common_super_expr(traces) || hd(stack.expr_stack)
|
||||
traces = simplify_traces(traces, context)
|
||||
|
||||
{:error, {Module.Types, {:unable_unify, left, right, common_expr, traces}, [location]}}
|
||||
end
|
||||
|
||||
defp type_traces(context) do
|
||||
Enum.flat_map(context.traces, fn {var_index, traces} ->
|
||||
expr_var = Map.fetch!(context.types_to_vars, var_index)
|
||||
Enum.map(traces, &{expr_var, &1})
|
||||
end)
|
||||
end
|
||||
|
||||
# Only use last expr from trace and tag if trace is for
|
||||
# a concrete type or type variable
|
||||
defp simplify_traces(traces, context) do
|
||||
Enum.flat_map(traces, fn {var, {type, [expr | _], location}} ->
|
||||
case type do
|
||||
{:var, var_index} ->
|
||||
var2 = Map.fetch!(context.types_to_vars, var_index)
|
||||
[{var, {:var, var2, expr, location}}]
|
||||
|
||||
_ ->
|
||||
[{var, {:type, type, expr, location}}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Find first common super expression among all traces
|
||||
defp common_super_expr([]) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp common_super_expr([{_var, {_type, expr_stack, _location}} | traces]) do
|
||||
Enum.find_value(expr_stack, fn expr ->
|
||||
common? =
|
||||
Enum.all?(traces, fn {_var, {_type, expr_stack, _location}} -> expr in expr_stack end)
|
||||
|
||||
if common? do
|
||||
expr
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_meta({_fun, meta, _args}) when is_list(meta), do: meta
|
||||
defp get_meta(_other), do: []
|
||||
end
|
||||
@@ -0,0 +1,585 @@
|
||||
defmodule Module.Types.Pattern do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.{Helpers, Infer}
|
||||
|
||||
@doc """
|
||||
Return the type and typing context of a pattern expression or an error
|
||||
in case of a typing conflict.
|
||||
"""
|
||||
# :atom
|
||||
def of_pattern(atom, _stack, context) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
end
|
||||
|
||||
# 12
|
||||
def of_pattern(literal, _stack, context) when is_integer(literal) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# 1.2
|
||||
def of_pattern(literal, _stack, context) when is_float(literal) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# "..."
|
||||
def of_pattern(literal, _stack, context) when is_binary(literal) do
|
||||
{:ok, :binary, context}
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
def of_pattern({:<<>>, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case reduce_ok(args, context, &of_binary(&1, stack, &2)) do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left | []
|
||||
def of_pattern({:|, _meta, [left_expr, []]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
of_pattern(left_expr, stack, context)
|
||||
end
|
||||
|
||||
# left | right
|
||||
def of_pattern({:|, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pattern(left_expr, stack, context) do
|
||||
{:ok, left, context} ->
|
||||
case of_pattern(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# []
|
||||
def of_pattern([], _stack, context) do
|
||||
{:ok, {:list, :dynamic}, context}
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_pattern(exprs, stack, context) when is_list(exprs) do
|
||||
stack = push_expr_stack(exprs, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left ++ right
|
||||
def of_pattern(
|
||||
{{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]} = expr,
|
||||
stack,
|
||||
context
|
||||
) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pattern(left_expr, stack, context) do
|
||||
{:ok, {:list, left}, context} ->
|
||||
case of_pattern(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# _
|
||||
def of_pattern({:_, _meta, atom}, _stack, context) when is_atom(atom) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# var
|
||||
def of_pattern(var, _stack, context) when is_var(var) do
|
||||
{type, context} = new_var(var, context)
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_pattern({left, right}, stack, context) do
|
||||
of_pattern({:{}, [], [left, right]}, stack, context)
|
||||
end
|
||||
|
||||
# {...}
|
||||
def of_pattern({:{}, _meta, exprs} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left = right
|
||||
def of_pattern({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, left_type, context} <- of_pattern(left_expr, stack, context),
|
||||
{:ok, right_type, context} <- of_pattern(right_expr, stack, context),
|
||||
do: unify(left_type, right_type, stack, context)
|
||||
end
|
||||
|
||||
# %{...}
|
||||
def of_pattern({:%{}, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} -> {:ok, {:map, pairs_to_unions(pairs, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# %_{...}
|
||||
def of_pattern(
|
||||
{:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]} = expr,
|
||||
stack,
|
||||
context
|
||||
)
|
||||
when is_atom(var_context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} ->
|
||||
pairs = [{{:atom, :__struct__}, :atom} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# %var{...}
|
||||
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when is_var(var) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, pairs, context} <- of_pairs(args, stack, context),
|
||||
{var_type, context} = new_var(var, context),
|
||||
{:ok, _, context} <- unify(var_type, :atom, stack, context) do
|
||||
pairs = [{{:atom, :__struct__}, var_type} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
end
|
||||
end
|
||||
|
||||
# %Struct{...}
|
||||
def of_pattern({:%, _meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when is_atom(module) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} ->
|
||||
pairs = [{{:atom, :__struct__}, {:atom, module}} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_pairs(pairs, stack, context) do
|
||||
map_reduce_ok(pairs, context, fn {key, value}, context ->
|
||||
with {:ok, key_type, context} <- of_pattern(key, stack, context),
|
||||
{:ok, value_type, context} <- of_pattern(value, stack, context),
|
||||
do: {:ok, {key_type, value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp pairs_to_unions(pairs, context) do
|
||||
# Maps only allow simple literal keys in patterns so
|
||||
# we do not have to do subtype checking
|
||||
|
||||
Enum.reduce(pairs, [], fn {key, value}, pairs ->
|
||||
case :lists.keyfind(key, 1, pairs) do
|
||||
{^key, {:union, union}} ->
|
||||
:lists.keystore(key, 1, pairs, {key, to_union([value | union], context)})
|
||||
|
||||
{^key, original_value} ->
|
||||
:lists.keystore(key, 1, pairs, {key, to_union([value, original_value], context)})
|
||||
|
||||
false ->
|
||||
[{key, value} | pairs]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
## GUARDS
|
||||
|
||||
# TODO: Some guards can be changed to intersection types or higher order types
|
||||
|
||||
@guard_functions %{
|
||||
{:is_atom, 1} => {[:atom], :boolean},
|
||||
{:is_binary, 1} => {[:binary], :boolean},
|
||||
{:is_bitstring, 1} => {[:binary], :boolean},
|
||||
{:is_boolean, 1} => {[:boolean], :boolean},
|
||||
{:is_float, 1} => {[:float], :boolean},
|
||||
{:is_function, 1} => {[:fun], :boolean},
|
||||
{:is_function, 2} => {[:fun, :integer], :boolean},
|
||||
{:is_integer, 1} => {[:integer], :boolean},
|
||||
{:is_list, 1} => {[{:list, :dynamic}], :boolean},
|
||||
{:is_map, 1} => {[{:map, []}], :boolean},
|
||||
{:is_map_key, 2} => {[:dynamic, {:map, []}], :dynamic},
|
||||
{:is_number, 1} => {[:number], :boolean},
|
||||
{:is_pid, 1} => {[:pid], :boolean},
|
||||
{:is_port, 1} => {[:port], :boolean},
|
||||
{:is_reference, 1} => {[:reference], :boolean},
|
||||
{:is_tuple, 1} => {[:tuple], :boolean},
|
||||
{:<, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=<", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>=, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:==, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=:=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:*, 2} => {[:number, :number], :number},
|
||||
{:+, 1} => {[:number], :number},
|
||||
{:+, 2} => {[:number, :number], :number},
|
||||
{:-, 1} => {[:number], :number},
|
||||
{:-, 2} => {[:number, :number], :number},
|
||||
{:/, 2} => {[:number, :number], :number},
|
||||
{:abs, 1} => {[:number], :number},
|
||||
{:ceil, 1} => {[:number], :integer},
|
||||
{:floor, 1} => {[:number], :integer},
|
||||
{:round, 1} => {[:number], :integer},
|
||||
{:trunc, 1} => {[:number], :integer},
|
||||
{:element, 2} => {[:integer, :tuple], :dynamic},
|
||||
{:hd, 1} => {[{:list, :dynamic}], :dynamic},
|
||||
{:length, 1} => {[{:list, :dynamic}], :integer},
|
||||
{:map_get, 2} => {[:dynamic, {:map, []}], :dynamic},
|
||||
{:map_size, 1} => {[{:map, []}], :integer},
|
||||
{:tl, 1} => {[{:list, :dynamic}], :dynamic},
|
||||
{:tuple_size, 1} => {[:tuple], :integer},
|
||||
{:node, 1} => {[{:union, [:pid, :reference, :port]}], :atom},
|
||||
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
|
||||
{:bit_size, 1} => {[:binary], :integer},
|
||||
{:byte_size, 1} => {[:binary], :integer},
|
||||
{:size, 1} => {[{:union, [:binary, :tuple]}], :boolean},
|
||||
{:div, 2} => {[:integer, :integer], :integer},
|
||||
{:rem, 2} => {[:integer, :integer], :integer},
|
||||
{:node, 0} => {[], :atom},
|
||||
{:self, 0} => {[], :pid},
|
||||
{:bnot, 1} => {[:integer], :integer},
|
||||
{:band, 2} => {[:integer, :integer], :integer},
|
||||
{:bor, 2} => {[:integer, :integer], :integer},
|
||||
{:bxor, 2} => {[:integer, :integer], :integer},
|
||||
{:bsl, 2} => {[:integer, :integer], :integer},
|
||||
{:bsr, 2} => {[:integer, :integer], :integer},
|
||||
{:xor, 2} => {[:boolean, :boolean], :boolean},
|
||||
{:not, 1} => {[:boolean], :boolean}
|
||||
|
||||
# Following guards are matched explicitly to handle
|
||||
# type guard functions such as is_atom/1
|
||||
# {:andalso, 2} => {[:boolean, :boolean], :boolean}
|
||||
# {:orelse, 2} => {[:boolean, :boolean], :boolean}
|
||||
}
|
||||
|
||||
@type_guards [
|
||||
:is_atom,
|
||||
:is_binary,
|
||||
:is_bitstring,
|
||||
:is_boolean,
|
||||
:is_float,
|
||||
:is_function,
|
||||
:is_function,
|
||||
:is_integer,
|
||||
:is_list,
|
||||
:is_map,
|
||||
:is_number,
|
||||
:is_pid,
|
||||
:is_port,
|
||||
:is_reference,
|
||||
:is_tuple
|
||||
]
|
||||
|
||||
@doc """
|
||||
Refines the type variables in the typing context using type check guards
|
||||
such as `is_integer/1`.
|
||||
"""
|
||||
def of_guard({{:., _, [:erlang, :andalso]}, _, [left, right]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
fresh_context = fresh_context(context)
|
||||
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_and(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
end
|
||||
|
||||
def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
fresh_context = fresh_context(context)
|
||||
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
end
|
||||
|
||||
def of_guard({{:., _, [:erlang, guard]}, _, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
{param_types, return_type} = guard_signature(guard, length(args))
|
||||
type_guard? = type_guard?(guard)
|
||||
|
||||
# Only check type guards in the context of and/or/not,
|
||||
# a type guard in the context of is_tuple(x) > :foo
|
||||
# should not affect the inference of x
|
||||
if not type_guard? or stack.type_guards_enabled? do
|
||||
arg_stack = %{stack | type_guards_enabled?: type_guard?}
|
||||
|
||||
with {:ok, arg_types, context} <-
|
||||
map_reduce_ok(args, context, &of_guard(&1, arg_stack, &2)),
|
||||
{:ok, context} <- unify_call(param_types, arg_types, stack, context) do
|
||||
{arg_types, guard_sources} =
|
||||
case arg_types do
|
||||
[{:var, index} | rest_arg_types] when type_guard? ->
|
||||
guard_sources =
|
||||
Map.update(context.guard_sources, index, [:guarded], &[:guarded | &1])
|
||||
|
||||
{rest_arg_types, guard_sources}
|
||||
|
||||
_ ->
|
||||
{arg_types, context.guard_sources}
|
||||
end
|
||||
|
||||
guard_sources =
|
||||
Enum.reduce(arg_types, guard_sources, fn
|
||||
{:var, index}, guard_sources ->
|
||||
Map.update(guard_sources, index, [:fail], &[:fail | &1])
|
||||
|
||||
_, guard_sources ->
|
||||
guard_sources
|
||||
end)
|
||||
|
||||
{:ok, return_type, %{context | guard_sources: guard_sources}}
|
||||
end
|
||||
else
|
||||
{:ok, return_type, context}
|
||||
end
|
||||
end
|
||||
|
||||
def of_guard(var, _stack, context) when is_var(var) do
|
||||
type = Map.fetch!(context.vars, var_name(var))
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
def of_guard(expr, stack, context) do
|
||||
# Fall back to of_pattern/3 for literals
|
||||
of_pattern(expr, stack, context)
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
types = Map.new(context.types, fn {var, _} -> {var, :unbound} end)
|
||||
traces = Map.new(context.traces, fn {var, _} -> {var, []} end)
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp unify_call(params, args, stack, context) do
|
||||
reduce_ok(Enum.zip(params, args), context, fn {param, arg}, context ->
|
||||
case unify(param, arg, stack, context) do
|
||||
{:ok, _, context} -> {:ok, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp merge_context_and(context, stack, left, right) do
|
||||
with {:ok, context} <- unify_new_types(context, stack, left),
|
||||
{:ok, context} <- unify_new_types(context, stack, right) do
|
||||
guard_sources = and_guard_sources(left.guard_sources, right.guard_sources)
|
||||
guard_sources = merge_guard_sources([context.guard_sources, guard_sources])
|
||||
{:ok, %{context | guard_sources: guard_sources}}
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_new_types(context, stack, new_context) do
|
||||
context = merge_traces(context, new_context)
|
||||
|
||||
reduce_ok(Map.to_list(new_context.types), context, fn
|
||||
{_index, :unbound}, context ->
|
||||
{:ok, context}
|
||||
|
||||
{index, new_type}, context ->
|
||||
case unify({:var, index}, new_type, %{stack | trace: false}, context) do
|
||||
{:ok, _, context} ->
|
||||
{:ok, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp merge_guard_sources(sources) do
|
||||
Enum.reduce(sources, fn left, right ->
|
||||
Map.merge(left, right, fn _index, left, right -> join_guard_source(left, right) end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp join_guard_source(left, right) do
|
||||
sources = left ++ right
|
||||
|
||||
cond do
|
||||
:fail in sources -> [:fail]
|
||||
:guarded_fail in sources -> [:guarded_fail]
|
||||
:guarded in sources -> [:guarded]
|
||||
true -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp and_guard_sources(left, right) do
|
||||
Map.merge(left, right, fn _index, left, right ->
|
||||
# When the failing guard function wont fail due to type check function before it,
|
||||
# for example: is_list(x) and length(x)
|
||||
if :guarded in left and :fail in right do
|
||||
[:guarded_fail]
|
||||
else
|
||||
join_guard_source(left, right)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp merge_traces(context, new_context) do
|
||||
traces =
|
||||
:maps.fold(
|
||||
fn index, new_traces, traces ->
|
||||
:maps.update_with(index, &(new_traces ++ &1), new_traces, traces)
|
||||
end,
|
||||
context.traces,
|
||||
new_context.traces
|
||||
)
|
||||
|
||||
%{context | traces: traces}
|
||||
end
|
||||
|
||||
defp merge_context_or(context, stack, left, right) do
|
||||
context =
|
||||
case {Map.to_list(left.types), Map.to_list(right.types)} do
|
||||
{[{index, :unbound}], [{index, type}]} ->
|
||||
refine_var(index, type, stack, context)
|
||||
|
||||
{[{index, type}], [{index, :unbound}]} ->
|
||||
refine_var(index, type, stack, context)
|
||||
|
||||
{[{index, left_type}], [{index, right_type}]} ->
|
||||
# Only include right side if left side is from type guard such as is_list(x),
|
||||
# do not refine in case of length(x)
|
||||
left_guard_sources = Map.get(left.guard_sources, index, [])
|
||||
|
||||
if :fail in left_guard_sources do
|
||||
guard_sources = Map.put(context.guard_sources, index, [:fail])
|
||||
context = %{context | guard_sources: guard_sources}
|
||||
refine_var(index, left_type, stack, context)
|
||||
else
|
||||
guard_sources =
|
||||
merge_guard_sources([
|
||||
context.guard_sources,
|
||||
left.guard_sources,
|
||||
right.guard_sources
|
||||
])
|
||||
|
||||
context = %{context | guard_sources: guard_sources}
|
||||
refine_var(index, to_union([left_type, right_type], context), stack, context)
|
||||
end
|
||||
|
||||
{left_types, _right_types} ->
|
||||
Enum.reduce(left_types, context, fn {index, left_type}, context ->
|
||||
left_guard_sources = Map.get(left.guard_sources, index, [])
|
||||
|
||||
if :fail in left_guard_sources do
|
||||
guard_sources =
|
||||
merge_guard_sources([
|
||||
context.guard_sources,
|
||||
left.guard_sources,
|
||||
right.guard_sources
|
||||
])
|
||||
|
||||
context = %{context | guard_sources: guard_sources}
|
||||
refine_var(index, left_type, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
# binary-pattern :: specifier
|
||||
defp of_binary({:"::", _meta, [expr, specifiers]} = full_expr, stack, context) do
|
||||
{expected_type, utf?} = collect_binary_type(specifiers) || {:integer, false}
|
||||
stack = push_expr_stack(full_expr, stack)
|
||||
|
||||
# Special case utf specifiers with binary literals since they allow
|
||||
# both integer and binary literals but variables are always integer
|
||||
if is_binary(expr) and utf? do
|
||||
{:ok, context}
|
||||
else
|
||||
with {:ok, type, context} <- of_pattern(expr, stack, context),
|
||||
{:ok, _type, context} <- unify(type, expected_type, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
end
|
||||
|
||||
# binary-pattern
|
||||
defp of_binary(expr, stack, context) do
|
||||
case of_pattern(expr, stack, context) do
|
||||
{:ok, type, context} when type in [:integer, :float, :binary] ->
|
||||
{:ok, context}
|
||||
|
||||
{:ok, type, _context} ->
|
||||
{:error, {:invalid_binary_type, type}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# Collect binary type specifiers,
|
||||
# from `<<pattern::integer-size(10)>>` collect `integer`
|
||||
defp collect_binary_type({:-, _meta, [left, right]}),
|
||||
do: collect_binary_type(left) || collect_binary_type(right)
|
||||
|
||||
defp collect_binary_type({:integer, _, _}), do: {:integer, false}
|
||||
defp collect_binary_type({:float, _, _}), do: {:float, false}
|
||||
defp collect_binary_type({:bits, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:bitstring, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:bytes, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:binary, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:utf8, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type({:utf16, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type({:utf32, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type(_), do: nil
|
||||
|
||||
defp guard_signature(name, arity) do
|
||||
Map.fetch!(@guard_functions, {name, arity})
|
||||
end
|
||||
|
||||
defp type_guard?(name) do
|
||||
name in @type_guards
|
||||
end
|
||||
end
|
||||
+17
-2
@@ -13,8 +13,23 @@ defmodule Node do
|
||||
@doc """
|
||||
Turns a non-distributed node into a distributed node.
|
||||
|
||||
This functionality starts the `:net_kernel` and other
|
||||
related processes.
|
||||
This functionality starts the `:net_kernel` and other related
|
||||
processes.
|
||||
|
||||
This function is rarely invoked in practice. Instead, nodes are
|
||||
named and started via the command line by using the `--sname` and
|
||||
`--name` flags. If you need to use this function to dynamically
|
||||
name a node, please make sure the `epmd` operating system process
|
||||
is running by calling `epmd -daemon`.
|
||||
|
||||
Invoking this function when the distribution has already been started,
|
||||
either via the command line interface or dynamically, will return an
|
||||
error.
|
||||
|
||||
## Examples
|
||||
|
||||
{:ok, pid} = Node.start(:example, :shortnames, 15000)
|
||||
|
||||
"""
|
||||
@spec start(node, :longnames | :shortnames, non_neg_integer) :: {:ok, pid} | {:error, term}
|
||||
def start(name, type \\ :longnames, tick_time \\ 15000) do
|
||||
|
||||
+12
-6
@@ -20,7 +20,7 @@ defmodule Path do
|
||||
|
||||
## Examples
|
||||
|
||||
### Unix
|
||||
### Unix-like operating systems
|
||||
|
||||
Path.absname("foo")
|
||||
#=> "/usr/local/foo"
|
||||
@@ -95,6 +95,8 @@ defmodule Path do
|
||||
absname(absname_join(name), cwd)
|
||||
end
|
||||
|
||||
@slash [?/, ?\\]
|
||||
|
||||
# Joins a list
|
||||
defp absname_join([name1, name2 | rest]), do: absname_join([absname_join(name1, name2) | rest])
|
||||
|
||||
@@ -110,6 +112,11 @@ defmodule Path do
|
||||
do_absname_join(rest, relativename, [?:, uc_letter + ?a - ?A], :win32)
|
||||
end
|
||||
|
||||
defp do_absname_join(<<c1, c2, rest::binary>>, relativename, [], :win32)
|
||||
when c1 in @slash and c2 in @slash do
|
||||
do_absname_join(rest, relativename, '//', :win32)
|
||||
end
|
||||
|
||||
defp do_absname_join(<<?\\, rest::binary>>, relativename, result, :win32),
|
||||
do: do_absname_join(<<?/, rest::binary>>, relativename, result, :win32)
|
||||
|
||||
@@ -188,7 +195,7 @@ defmodule Path do
|
||||
|
||||
## Examples
|
||||
|
||||
### Unix
|
||||
### Unix-like operating systems
|
||||
|
||||
Path.type("/") #=> :absolute
|
||||
Path.type("/usr/local/bin") #=> :absolute
|
||||
@@ -216,7 +223,7 @@ defmodule Path do
|
||||
|
||||
## Examples
|
||||
|
||||
### Unix
|
||||
### Unix-like operating systems
|
||||
|
||||
Path.relative("/usr/local/bin") #=> "usr/local/bin"
|
||||
Path.relative("usr/local/bin") #=> "usr/local/bin"
|
||||
@@ -254,8 +261,6 @@ defmodule Path do
|
||||
defp unix_pathtype([list | rest]) when is_list(list), do: unix_pathtype(list ++ rest)
|
||||
defp unix_pathtype(relative), do: {:relative, relative}
|
||||
|
||||
@slash [?/, ?\\]
|
||||
|
||||
defp win32_pathtype([list | rest]) when is_list(list), do: win32_pathtype(list ++ rest)
|
||||
|
||||
defp win32_pathtype([char, list | rest]) when is_list(list),
|
||||
@@ -517,6 +522,7 @@ defmodule Path do
|
||||
do_join(left, right, os_type) |> remove_dir_sep(os_type)
|
||||
end
|
||||
|
||||
defp do_join(left, "/", os_type), do: remove_dir_sep(left, os_type)
|
||||
defp do_join("", right, os_type), do: relative(right, os_type)
|
||||
defp do_join("/", right, os_type), do: "/" <> relative(right, os_type)
|
||||
|
||||
@@ -559,7 +565,7 @@ defmodule Path do
|
||||
"""
|
||||
@spec split(t) :: [binary]
|
||||
|
||||
# Work around a bug in Erlang on UNIX
|
||||
# Work around a bug in Erlang on Unix-like operating systems
|
||||
def split(""), do: []
|
||||
|
||||
def split(path) do
|
||||
|
||||
+36
-14
@@ -20,7 +20,7 @@ defmodule Port do
|
||||
:ok
|
||||
|
||||
In the example above, we have created a new port that executes the
|
||||
program `cat`. `cat` is a program available on UNIX systems that
|
||||
program `cat`. `cat` is a program available on Unix-like operating systems that
|
||||
receives data from multiple inputs and concatenates them in the output.
|
||||
|
||||
After the port was created, we sent it two commands in the form of
|
||||
@@ -124,20 +124,42 @@ defmodule Port do
|
||||
will have its stdin and stdout channels closed but **it won't be automatically
|
||||
terminated**.
|
||||
|
||||
While most UNIX command line tools will exit once its communication channels
|
||||
are closed, not all command line applications will do so. While we encourage
|
||||
graceful termination by detecting if stdin/stdout has been closed, we do not
|
||||
always have control over how third-party software terminates. In those cases,
|
||||
you can wrap the application in a script that checks for stdin. Here is such
|
||||
script in Bash:
|
||||
While most Unix command line tools will exit once its communication channels
|
||||
are closed, not all command line applications will do so. You can easily check
|
||||
this by starting the port and then shutting down the VM and inspecting your
|
||||
operating system to see if the port process is still running.
|
||||
|
||||
#!/bin/bash
|
||||
"$@" &
|
||||
pid=$!
|
||||
while read line ; do
|
||||
:
|
||||
done
|
||||
kill -KILL $pid
|
||||
While we encourage graceful termination by detecting if stdin/stdout has been
|
||||
closed, we do not always have control over how third-party software terminates.
|
||||
In those cases, you can wrap the application in a script that checks for stdin.
|
||||
Here is such script in `sh`:
|
||||
|
||||
#!/bin/sh
|
||||
|
||||
# Start the program in the background
|
||||
exec "$@" &
|
||||
pid1=$!
|
||||
|
||||
# Silence warnings from here on
|
||||
exec >/dev/null 2>&1
|
||||
|
||||
# Read from stdin in the background and
|
||||
# kill running program when stdin closes
|
||||
exec 0<&0 $(
|
||||
while read; do :; done
|
||||
kill -KILL $pid1
|
||||
) &
|
||||
pid2=$!
|
||||
|
||||
# Clean up
|
||||
wait $pid1
|
||||
ret=$?
|
||||
kill -KILL $pid2
|
||||
exit $ret
|
||||
|
||||
Note the program above hijacks stdin, so you won't be able to communicate
|
||||
with the underlying software via stdin (on the positive side, software that
|
||||
reads from stdin typically terminates when stdin closes).
|
||||
|
||||
Now instead of:
|
||||
|
||||
|
||||
@@ -583,7 +583,7 @@ defmodule Process do
|
||||
send(:test, :hello)
|
||||
#=> :hello
|
||||
send(:wrong_name, :hello)
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@spec register(pid | port, atom) :: true
|
||||
@@ -620,7 +620,7 @@ defmodule Process do
|
||||
Process.unregister(:test)
|
||||
#=> true
|
||||
Process.unregister(:wrong_name)
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@spec unregister(atom) :: true
|
||||
@@ -708,8 +708,6 @@ defmodule Process do
|
||||
@spec flag(:message_queue_data, :erlang.message_queue_data()) :: :erlang.message_queue_data()
|
||||
@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..10000) :: 0..10000
|
||||
@spec flag(:sensitive, boolean) :: boolean
|
||||
|
||||
+32
-15
@@ -163,15 +163,15 @@ defmodule Protocol do
|
||||
|
||||
* `:consolidated?` - returns whether the protocol is consolidated
|
||||
|
||||
* `:functions` - returns keyword list of protocol functions and their arities
|
||||
* `:functions` - returns a keyword list of protocol functions and their arities
|
||||
|
||||
* `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
|
||||
implementing the protocol, otherwise `:not_consolidated`
|
||||
implementing the protocol, otherwise `:not_consolidated`
|
||||
|
||||
* `:module` - the protocol module atom name
|
||||
|
||||
* `impl_for/1` - receives a structure and returns the module that
|
||||
implements the protocol for the structure, `nil` otherwise
|
||||
* `impl_for/1` - returns the module that implements the protocol for the given argument,
|
||||
`nil` otherwise
|
||||
|
||||
* `impl_for!/1` - same as above but raises an error if an implementation is
|
||||
not found
|
||||
@@ -187,6 +187,22 @@ defmodule Protocol do
|
||||
iex> Enumerable.impl_for(42)
|
||||
nil
|
||||
|
||||
In addition, every protocol implementation module contains the `__impl__/1` function. The
|
||||
function takes one of the following atoms:
|
||||
|
||||
* `:for` - returns the module responsible for the data structure of the protocol implementation
|
||||
|
||||
* `:protocol` - returns the protocol module for which this implementation is provided
|
||||
|
||||
For example, the module implementing the `Enumerable` protocol for lists is `Enumerable.List`.
|
||||
Therefore, we can invoke `__impl__/1` on this module:
|
||||
|
||||
iex(1)> Enumerable.List.__impl__(:for)
|
||||
List
|
||||
|
||||
iex(2)> Enumerable.List.__impl__(:protocol)
|
||||
Enumerable
|
||||
|
||||
## Consolidation
|
||||
|
||||
In order to cope with code loading in development, protocols in
|
||||
@@ -195,7 +211,7 @@ defmodule Protocol do
|
||||
|
||||
In order to speed up dispatching in production environments, where
|
||||
all implementations are known up-front, Elixir provides a feature
|
||||
called protocol consolidation. Consolidation directly links protocols
|
||||
called *protocol consolidation*. Consolidation directly links protocols
|
||||
to their implementations in a way that invoking a function from a
|
||||
consolidated protocol is equivalent to invoking two remote functions.
|
||||
|
||||
@@ -230,9 +246,10 @@ defmodule Protocol do
|
||||
Although doing so is not recommended as it may affect your test suite
|
||||
performance.
|
||||
|
||||
Finally note all protocols are compiled with `debug_info` set to `true`,
|
||||
regardless of the option set by `elixirc` compiler. The debug info is
|
||||
used for consolidation and it may be removed after consolidation.
|
||||
Finally, note all protocols are compiled with `debug_info` set to `true`,
|
||||
regardless of the option set by the `elixirc` compiler. The debug info is
|
||||
used for consolidation and it is removed after consolidation unless
|
||||
globally set.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@@ -458,6 +475,7 @@ defmodule Protocol do
|
||||
|
||||
defp extract_matching_by_attribute(paths, prefix, callback) do
|
||||
for path <- paths,
|
||||
path = to_charlist(path),
|
||||
file <- list_dir(path),
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
@@ -470,8 +488,6 @@ defmodule Protocol do
|
||||
end
|
||||
end
|
||||
|
||||
defp list_dir(path), do: list_dir(to_charlist(path))
|
||||
|
||||
defp extract_from_file(path, file, prefix, callback) do
|
||||
if :lists.prefix(prefix, file) and :filename.extension(file) == '.beam' do
|
||||
extract_from_beam(:filename.join(path, file), callback)
|
||||
@@ -531,13 +547,14 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp beam_protocol(protocol) do
|
||||
chunk_ids = [:debug_info, 'Docs', 'ExDp']
|
||||
chunk_ids = [:debug_info, 'Docs', 'ExCk']
|
||||
opts = [:allow_missing_chunks]
|
||||
|
||||
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
|
||||
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
|
||||
{:debug_info_v1, _backend, {:elixir_v1, info, specs}} = debug_info
|
||||
%{attributes: attributes, definitions: definitions} = info
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
|
||||
case attributes[:protocol] do
|
||||
@@ -619,14 +636,14 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp built_in_clause_for(mod, guard, protocol, meta, line) do
|
||||
x = quote(line: line, do: x)
|
||||
x = {:x, [line: line, version: -1], __MODULE__}
|
||||
guard = quote(line: line, do: :erlang.unquote(guard)(unquote(x)))
|
||||
body = load_impl(protocol, mod)
|
||||
{meta, [x], [guard], body}
|
||||
end
|
||||
|
||||
defp struct_clause_for(meta, line) do
|
||||
x = quote(line: line, do: x)
|
||||
x = {:x, [line: line, version: -1], __MODULE__}
|
||||
head = quote(line: line, do: %{__struct__: unquote(x)})
|
||||
guard = quote(line: line, do: :erlang.is_atom(unquote(x)))
|
||||
body = quote(line: line, do: struct_impl_for(unquote(x)))
|
||||
@@ -698,7 +715,7 @@ defmodule Protocol do
|
||||
nil
|
||||
end
|
||||
|
||||
# Disable dialyzer checks - before and after consolidation
|
||||
# Disable Dialyzer checks - before and after consolidation
|
||||
# the types could be more strict
|
||||
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
|
||||
|
||||
@@ -900,7 +917,7 @@ defmodule Protocol do
|
||||
"the #{inspect(protocol)} protocol has already been consolidated, an " <>
|
||||
"implementation for #{inspect(for)} has no effect. If you want to " <>
|
||||
"implement protocols after compilation or during tests, check the " <>
|
||||
"\"Consolidation\" section in the documentation for Kernel.defprotocol/2"
|
||||
"\"Consolidation\" section in the Protocol module documentation"
|
||||
|
||||
IO.warn(message, Macro.Env.stacktrace(env))
|
||||
end
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
defmodule Range do
|
||||
@moduledoc """
|
||||
Defines a range.
|
||||
|
||||
A range represents a sequence of one or many,
|
||||
ascending or descending, consecutive integers.
|
||||
Ranges represent a sequence of one or many, ascending
|
||||
or descending, consecutive integers.
|
||||
|
||||
Ranges can be either increasing (`first <= last`) or
|
||||
decreasing (`first > last`). Ranges are also always
|
||||
@@ -49,6 +47,12 @@ defmodule Range do
|
||||
|
||||
@doc """
|
||||
Creates a new range.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Range.new(-100, 100)
|
||||
-100..100
|
||||
|
||||
"""
|
||||
@spec new(integer, integer) :: t
|
||||
def new(first, last) when is_integer(first) and is_integer(last) do
|
||||
|
||||
+43
-30
@@ -149,12 +149,11 @@ defmodule Record do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> record = {User, "john", 27}
|
||||
iex> Record.is_record(record)
|
||||
true
|
||||
iex> tuple = {}
|
||||
iex> Record.is_record(tuple)
|
||||
false
|
||||
Record.is_record({User, "john", 27})
|
||||
#=> true
|
||||
|
||||
Record.is_record({})
|
||||
#=> false
|
||||
|
||||
"""
|
||||
defguard is_record(data)
|
||||
@@ -241,8 +240,8 @@ defmodule Record do
|
||||
functions for defaults.
|
||||
|
||||
Record.defrecord(:my_rec, Record.extract(...))
|
||||
#=> ** (ArgumentError) invalid value for record field fun_field,
|
||||
#=> cannot escape #Function<12.90072148/2 in :erl_eval.expr/5>.
|
||||
** (ArgumentError) invalid value for record field fun_field,
|
||||
cannot escape #Function<12.90072148/2 in :erl_eval.expr/5>.
|
||||
|
||||
To work around this error, redefine the field with your own &M.f/a function,
|
||||
like so:
|
||||
@@ -256,20 +255,11 @@ defmodule Record do
|
||||
"""
|
||||
defmacro defrecord(name, tag \\ nil, kv) do
|
||||
quote bind_quoted: [name: name, tag: tag, kv: kv] do
|
||||
defined_arity =
|
||||
Enum.find(0..2, fn arity ->
|
||||
Module.defines?(__MODULE__, {name, arity})
|
||||
end)
|
||||
|
||||
if defined_arity do
|
||||
raise ArgumentError,
|
||||
"cannot define record #{inspect(name)} because a definition #{name}/#{defined_arity} already exists"
|
||||
end
|
||||
fields = Record.__fields__(:defrecord, kv)
|
||||
Record.__validate__(__MODULE__, name, fields)
|
||||
|
||||
tag = tag || name
|
||||
|
||||
fields = Record.__fields__(:defrecord, kv)
|
||||
|
||||
defmacro unquote(name)(args \\ []) do
|
||||
Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
|
||||
end
|
||||
@@ -285,20 +275,11 @@ defmodule Record do
|
||||
"""
|
||||
defmacro defrecordp(name, tag \\ nil, kv) do
|
||||
quote bind_quoted: [name: name, tag: tag, kv: kv] do
|
||||
defined_arity =
|
||||
Enum.find(0..2, fn arity ->
|
||||
Module.defines?(__MODULE__, {name, arity})
|
||||
end)
|
||||
|
||||
if defined_arity do
|
||||
raise ArgumentError,
|
||||
"cannot define record #{inspect(name)} because a definition #{name}/#{defined_arity} already exists"
|
||||
end
|
||||
fields = Record.__fields__(:defrecordp, kv)
|
||||
Record.__validate__(__MODULE__, name, fields)
|
||||
|
||||
tag = tag || name
|
||||
|
||||
fields = Record.__fields__(:defrecordp, kv)
|
||||
|
||||
defmacrop unquote(name)(args \\ []) do
|
||||
Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
|
||||
end
|
||||
@@ -309,6 +290,38 @@ defmodule Record do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __validate__(module, name, fields) do
|
||||
error_on_duplicate_record(module, name)
|
||||
# TODO: Make it raise on v2.0
|
||||
warn_on_duplicate_key(:lists.keysort(1, fields))
|
||||
end
|
||||
|
||||
defp error_on_duplicate_record(module, name) do
|
||||
defined_arity =
|
||||
Enum.find(0..2, fn arity ->
|
||||
Module.defines?(module, {name, arity})
|
||||
end)
|
||||
|
||||
if defined_arity do
|
||||
raise ArgumentError,
|
||||
"cannot define record #{inspect(name)} because a definition #{name}/#{defined_arity} already exists"
|
||||
end
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_key([]) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_key([{key, _} | [{key, _} | _] = rest]) do
|
||||
IO.warn("duplicate key #{inspect(key)} found in record")
|
||||
warn_on_duplicate_key(rest)
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_key([_ | rest]) do
|
||||
warn_on_duplicate_key(rest)
|
||||
end
|
||||
|
||||
# Normalizes of record fields to have default values.
|
||||
@doc false
|
||||
def __fields__(type, fields) do
|
||||
|
||||
+12
-10
@@ -86,7 +86,8 @@ defmodule Regex do
|
||||
|
||||
* `:none` - does not return matching subpatterns at all
|
||||
|
||||
* `:all_names` - captures all names in the Regex
|
||||
* `:all_names` - captures all named subpattern matches in the Regex as a list
|
||||
ordered **alphabetically** by the names of the subpatterns
|
||||
|
||||
* `list(binary)` - a list of named captures to capture
|
||||
|
||||
@@ -133,10 +134,10 @@ defmodule Regex do
|
||||
shared during development is compiled on the target (such as dependencies,
|
||||
archives, and escripts) and, when running in production, the code must either
|
||||
be compiled on the target (via `mix compile` or similar) or released on the
|
||||
host (via `mix releases` or similar) with a matching OTP, OS and architecture
|
||||
as as the target.
|
||||
host (via `mix releases` or similar) with a matching OTP, operating system
|
||||
and architecture as the target.
|
||||
|
||||
If you know you are running on a different system that the current one and
|
||||
If you know you are running on a different system than the current one and
|
||||
you are doing multiple matches with the regex, you can manually invoke
|
||||
`Regex.recompile/1` or `Regex.recompile!/1` to perform a runtime version
|
||||
check and recompile the regex if necessary.
|
||||
@@ -293,7 +294,7 @@ defmodule Regex do
|
||||
|
||||
## Options
|
||||
|
||||
* `:return` - set to `:index` to return byte index and match length.
|
||||
* `:return` - when set to `:index`, returns byte index and match length.
|
||||
Defaults to `:binary`.
|
||||
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
|
||||
to see the possible capture values.
|
||||
@@ -329,7 +330,7 @@ defmodule Regex do
|
||||
|
||||
## Options
|
||||
|
||||
* `:return` - set to `:index` to return byte index and match length.
|
||||
* `:return` - when set to `:index`, returns byte index and match length.
|
||||
Defaults to `:binary`.
|
||||
|
||||
## Examples
|
||||
@@ -422,7 +423,7 @@ defmodule Regex do
|
||||
|
||||
## Options
|
||||
|
||||
* `:return` - set to `:index` to return byte index and match length.
|
||||
* `:return` - when set to `:index`, returns byte index and match length.
|
||||
Defaults to `:binary`.
|
||||
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
|
||||
to see the possible capture values.
|
||||
@@ -461,13 +462,13 @@ defmodule Regex do
|
||||
end
|
||||
|
||||
defp safe_run(
|
||||
%Regex{re_pattern: compiled, source: source, re_version: version},
|
||||
%Regex{re_pattern: compiled, source: source, re_version: version, opts: compile_opts},
|
||||
string,
|
||||
options
|
||||
) do
|
||||
case version() do
|
||||
^version -> :re.run(string, compiled, options)
|
||||
_ -> :re.run(string, source, options)
|
||||
_ -> :re.run(string, source, translate_options(compile_opts, options))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -490,7 +491,8 @@ defmodule Regex do
|
||||
affect the splitting process.
|
||||
|
||||
* `:include_captures` - when `true`, includes in the result the matches of
|
||||
the regular expression. Defaults to `false`.
|
||||
the regular expression. The matches are not counted towards the maximum
|
||||
number of parts if combined with the `:parts` option. Defaults to `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
+37
-28
@@ -198,10 +198,10 @@ defmodule Registry do
|
||||
@type match_pattern :: atom | term
|
||||
|
||||
@typedoc "A guard to be evaluated when matching on objects in a registry"
|
||||
@type guard :: {atom | term}
|
||||
@type guard :: atom | tuple
|
||||
|
||||
@typedoc "A list of guards to be evaluated when matching on objects in a registry"
|
||||
@type guards :: [guard] | []
|
||||
@type guards :: [guard]
|
||||
|
||||
@typedoc "A pattern used to representing the output format part of a match spec"
|
||||
@type body :: [atom | tuple]
|
||||
@@ -209,6 +209,14 @@ defmodule Registry do
|
||||
@typedoc "A full match spec used when selecting objects in the registry"
|
||||
@type spec :: [{match_pattern, guards, body}]
|
||||
|
||||
@typedoc "Options used for `child_spec/1` and `start_link/1`"
|
||||
@type start_option ::
|
||||
{:keys, keys}
|
||||
| {:name, registry}
|
||||
| {:partitions, pos_integer}
|
||||
| {:listeners, [atom]}
|
||||
| {:meta, [{meta_key, meta_value}]}
|
||||
|
||||
## Via callbacks
|
||||
|
||||
@doc false
|
||||
@@ -292,7 +300,7 @@ defmodule Registry do
|
||||
|
||||
The registry requires the following keys:
|
||||
|
||||
* `:keys` - choose if keys are `:unique` or `:duplicate`
|
||||
* `:keys` - chooses if keys are `:unique` or `:duplicate`
|
||||
* `:name` - the name of the registry and its tables
|
||||
|
||||
The following keys are optional:
|
||||
@@ -306,14 +314,7 @@ defmodule Registry do
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec start_link(
|
||||
keys: keys,
|
||||
name: registry,
|
||||
partitions: pos_integer,
|
||||
listeners: [atom],
|
||||
meta: meta
|
||||
) :: {:ok, pid} | {:error, term}
|
||||
when meta: [{meta_key, meta_value}]
|
||||
@spec start_link([start_option]) :: {:ok, pid} | {:error, term}
|
||||
def start_link(options) do
|
||||
keys = Keyword.get(options, :keys)
|
||||
|
||||
@@ -375,10 +376,11 @@ defmodule Registry do
|
||||
See `Supervisor`.
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
def child_spec(opts) do
|
||||
@spec child_spec([start_option]) :: Supervisor.child_spec()
|
||||
def child_spec(options) do
|
||||
%{
|
||||
id: Keyword.get(opts, :name, Registry),
|
||||
start: {Registry, :start_link, [opts]},
|
||||
id: Keyword.get(options, :name, Registry),
|
||||
start: {Registry, :start_link, [options]},
|
||||
type: :supervisor
|
||||
}
|
||||
end
|
||||
@@ -598,7 +600,8 @@ defmodule Registry do
|
||||
Optionally, it is possible to pass a list of guard conditions for more precise matching.
|
||||
Each guard is a tuple, which describes checks that should be passed by assigned part of pattern.
|
||||
For example the `$1 > 1` guard condition would be expressed as the `{:>, :"$1", 1}` tuple.
|
||||
Please note that guard conditions will work only for assigned variables like `:"$1"`, `:"$2"`, etc.
|
||||
Please note that guard conditions will work only for assigned
|
||||
variables like `:"$1"`, `:"$2"`, and so forth.
|
||||
Avoid usage of special match variables `:"$_"` and `:"$$"`, because it might not work as expected.
|
||||
|
||||
An empty list will be returned if there is no match.
|
||||
@@ -927,13 +930,13 @@ defmodule Registry do
|
||||
counter = System.unique_integer()
|
||||
true = :ets.insert(pid_ets, {self, key, key_ets, counter})
|
||||
|
||||
case register_key(kind, pid_server, key_ets, key, {key, {self, value}}) do
|
||||
{:ok, _} = ok ->
|
||||
case register_key(kind, key_ets, key, {key, {self, value}}) do
|
||||
:ok ->
|
||||
for listener <- listeners do
|
||||
Kernel.send(listener, {:register, registry, key, self, value})
|
||||
end
|
||||
|
||||
ok
|
||||
{:ok, pid_server}
|
||||
|
||||
{:error, {:already_registered, ^self}} = error ->
|
||||
true = :ets.delete_object(pid_ets, {self, key, key_ets, counter})
|
||||
@@ -946,14 +949,14 @@ defmodule Registry do
|
||||
end
|
||||
end
|
||||
|
||||
defp register_key(:duplicate, pid_server, key_ets, _key, entry) do
|
||||
defp register_key(:duplicate, key_ets, _key, entry) do
|
||||
true = :ets.insert(key_ets, entry)
|
||||
{:ok, pid_server}
|
||||
:ok
|
||||
end
|
||||
|
||||
defp register_key(:unique, pid_server, key_ets, key, entry) do
|
||||
defp register_key(:unique, key_ets, key, entry) do
|
||||
if :ets.insert_new(key_ets, entry) do
|
||||
{:ok, pid_server}
|
||||
:ok
|
||||
else
|
||||
# Notice we have to call register_key recursively
|
||||
# because we are always at odds of a race condition.
|
||||
@@ -963,11 +966,11 @@ defmodule Registry do
|
||||
{:error, {:already_registered, pid}}
|
||||
else
|
||||
:ets.delete_object(key_ets, current)
|
||||
register_key(:unique, pid_server, key_ets, key, entry)
|
||||
register_key(:unique, key_ets, key, entry)
|
||||
end
|
||||
|
||||
[] ->
|
||||
register_key(:unique, pid_server, key_ets, key, entry)
|
||||
register_key(:unique, key_ets, key, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1091,7 +1094,8 @@ defmodule Registry do
|
||||
Optionally, it is possible to pass a list of guard conditions for more precise matching.
|
||||
Each guard is a tuple, which describes checks that should be passed by assigned part of pattern.
|
||||
For example the `$1 > 1` guard condition would be expressed as the `{:>, :"$1", 1}` tuple.
|
||||
Please note that guard conditions will work only for assigned variables like `:"$1"`, `:"$2"`, etc.
|
||||
Please note that guard conditions will work only for assigned
|
||||
variables like `:"$1"`, `:"$2"`, and so forth.
|
||||
Avoid usage of special match variables `:"$_"` and `:"$$"`, because it might not work as expected.
|
||||
|
||||
Zero will be returned if there is no match.
|
||||
@@ -1158,7 +1162,8 @@ defmodule Registry do
|
||||
The second part, the guards, is a list of conditions that allow filtering the results.
|
||||
Each guard is a tuple, which describes checks that should be passed by assigned part of pattern.
|
||||
For example the `$1 > 1` guard condition would be expressed as the `{:>, :"$1", 1}` tuple.
|
||||
Please note that guard conditions will work only for assigned variables like `:"$1"`, `:"$2"`, etc.
|
||||
Please note that guard conditions will work only for assigned
|
||||
variables like `:"$1"`, `:"$2"`, and so forth.
|
||||
|
||||
The third part, the body, is a list of shapes of the returned entries. Like guards, you have access to
|
||||
assigned variables like `:"$1"`, which you can combine with hardcoded values to freely shape entries
|
||||
@@ -1320,10 +1325,14 @@ defmodule Registry.Supervisor do
|
||||
key_partition = Registry.Partition.key_name(registry, i)
|
||||
pid_partition = Registry.Partition.pid_name(registry, i)
|
||||
arg = {kind, registry, i, partitions, key_partition, pid_partition, listeners}
|
||||
worker(Registry.Partition, [pid_partition, arg], id: pid_partition)
|
||||
|
||||
%{
|
||||
id: pid_partition,
|
||||
start: {Registry.Partition, :start_link, [pid_partition, arg]}
|
||||
}
|
||||
end
|
||||
|
||||
supervise(children, strategy: strategy_for_kind(kind))
|
||||
Supervisor.init(children, strategy: strategy_for_kind(kind))
|
||||
end
|
||||
|
||||
# Unique registries have their key partition hashed by key.
|
||||
|
||||
+72
-46
@@ -107,6 +107,7 @@ defmodule Stream do
|
||||
@type index :: non_neg_integer
|
||||
|
||||
@type default :: any
|
||||
@type timer :: non_neg_integer | :infinity
|
||||
|
||||
# Require Stream.Reducers and its callbacks
|
||||
require Stream.Reducers, as: R
|
||||
@@ -505,8 +506,10 @@ defmodule Stream do
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
|
||||
"""
|
||||
@spec interval(non_neg_integer) :: Enumerable.t()
|
||||
def interval(n) when is_integer(n) and n >= 0 do
|
||||
@spec interval(timer()) :: Enumerable.t()
|
||||
def interval(n)
|
||||
when is_integer(n) and n >= 0
|
||||
when n == :infinity do
|
||||
unfold(0, fn count ->
|
||||
Process.sleep(n)
|
||||
{count, count + 1}
|
||||
@@ -787,8 +790,10 @@ defmodule Stream do
|
||||
[0]
|
||||
|
||||
"""
|
||||
@spec timer(non_neg_integer) :: Enumerable.t()
|
||||
def timer(n) when is_integer(n) and n >= 0 do
|
||||
@spec timer(timer()) :: Enumerable.t()
|
||||
def timer(n)
|
||||
when is_integer(n) and n >= 0
|
||||
when n == :infinity do
|
||||
take(interval(n), 1)
|
||||
end
|
||||
|
||||
@@ -796,13 +801,12 @@ defmodule Stream do
|
||||
Transforms an existing stream.
|
||||
|
||||
It expects an accumulator and a function that receives each stream element
|
||||
and an accumulator, and must return a tuple containing a new stream
|
||||
(often a list) with the new accumulator or a tuple with `:halt` as first
|
||||
element and the accumulator as second.
|
||||
and an accumulator. It must return a tuple, where the first element is a new
|
||||
stream (often a list) or the atom `:halt`, and the second element is the
|
||||
accumulator to be used by the next element, if any, in both cases.
|
||||
|
||||
Note: this function is similar to `Enum.flat_map_reduce/3` except the
|
||||
latter returns both the flat list and accumulator, while this one returns
|
||||
only the stream.
|
||||
Note: this function is equivalent to `Enum.flat_map_reduce/3`, except this
|
||||
function does not return the accumulator once the stream is processed.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -810,13 +814,13 @@ defmodule Stream do
|
||||
many of the functions defined in this module. For example, we can implement
|
||||
`Stream.take(enum, n)` as follows:
|
||||
|
||||
iex> enum = 1..100
|
||||
iex> enum = 1001..9999
|
||||
iex> n = 3
|
||||
iex> stream = Stream.transform(enum, 0, fn i, acc ->
|
||||
...> if acc < n, do: {[i], acc + 1}, else: {:halt, acc}
|
||||
...> end)
|
||||
iex> Enum.to_list(stream)
|
||||
[1, 2, 3]
|
||||
[1001, 1002, 1003]
|
||||
|
||||
"""
|
||||
@spec transform(Enumerable.t(), acc, fun) :: Enumerable.t()
|
||||
@@ -1256,41 +1260,37 @@ defmodule Stream do
|
||||
|
||||
def cycle(enumerable) do
|
||||
fn acc, fun ->
|
||||
inner = &do_cycle_each(&1, &2, fun)
|
||||
outer = &Enumerable.reduce(enumerable, &1, inner)
|
||||
reduce = check_cycle_first_element(outer)
|
||||
do_cycle(reduce, outer, acc)
|
||||
step = &do_cycle_step(&1, &2)
|
||||
cycle = &Enumerable.reduce(enumerable, &1, step)
|
||||
reduce = check_cycle_first_element(cycle)
|
||||
do_cycle(reduce, [], cycle, acc, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cycle(_reduce, _cycle, {:halt, acc}) do
|
||||
defp do_cycle(reduce, inner_acc, _cycle, {:halt, acc}, _fun) do
|
||||
reduce.({:halt, inner_acc})
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_cycle(reduce, cycle, {:suspend, acc}) do
|
||||
{:suspended, acc, &do_cycle(reduce, cycle, &1)}
|
||||
defp do_cycle(reduce, inner_acc, cycle, {:suspend, acc}, fun) do
|
||||
{:suspended, acc, &do_cycle(reduce, inner_acc, cycle, &1, fun)}
|
||||
end
|
||||
|
||||
defp do_cycle(reduce, cycle, acc) do
|
||||
try do
|
||||
reduce.(acc)
|
||||
catch
|
||||
{:stream_cycle, acc} ->
|
||||
{:halted, acc}
|
||||
else
|
||||
{state, acc} when state in [:done, :halted] ->
|
||||
do_cycle(cycle, cycle, {:cont, acc})
|
||||
defp do_cycle(reduce, inner_acc, cycle, {:cont, acc}, fun) do
|
||||
case reduce.({:cont, inner_acc}) do
|
||||
{:suspended, [element], new_reduce} ->
|
||||
do_cycle(new_reduce, inner_acc, cycle, fun.(element, acc), fun)
|
||||
|
||||
{:suspended, acc, continuation} ->
|
||||
{:suspended, acc, &do_cycle(continuation, cycle, &1)}
|
||||
{_, [element]} ->
|
||||
do_cycle(cycle, [], cycle, fun.(element, acc), fun)
|
||||
|
||||
{_, []} ->
|
||||
do_cycle(cycle, [], cycle, {:cont, acc}, fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cycle_each(x, acc, f) do
|
||||
case f.(x, acc) do
|
||||
{:halt, h} -> throw({:stream_cycle, h})
|
||||
{_, _} = o -> o
|
||||
end
|
||||
defp do_cycle_step(x, acc) do
|
||||
{:suspend, [x | acc]}
|
||||
end
|
||||
|
||||
defp check_cycle_first_element(reduce) do
|
||||
@@ -1333,9 +1333,9 @@ defmodule Stream do
|
||||
## Examples
|
||||
|
||||
# Although not necessary, let's seed the random algorithm
|
||||
iex> :rand.seed(:exsplus, {1, 2, 3})
|
||||
iex> :rand.seed(:exrop, {1, 2, 3})
|
||||
iex> Stream.repeatedly(&:rand.uniform/0) |> Enum.take(3)
|
||||
[0.40502929729990744, 0.45336720247823126, 0.04094511692041057]
|
||||
[0.7498295129076106, 0.06161655489244533, 0.7924073127680873]
|
||||
|
||||
"""
|
||||
@spec repeatedly((() -> element)) :: Enumerable.t()
|
||||
@@ -1384,6 +1384,21 @@ defmodule Stream do
|
||||
fn file -> File.close(file) end
|
||||
)
|
||||
|
||||
iex> Stream.resource(
|
||||
...> fn ->
|
||||
...> {:ok, pid} = StringIO.open("string")
|
||||
...> pid
|
||||
...> end,
|
||||
...> fn pid ->
|
||||
...> case IO.getn(pid, "", 1) do
|
||||
...> :eof -> {:halt, pid}
|
||||
...> char -> {[char], pid}
|
||||
...> end
|
||||
...> end,
|
||||
...> fn pid -> StringIO.close(pid) end
|
||||
...> ) |> Enum.to_list()
|
||||
["s", "t", "r", "i", "n", "g"]
|
||||
|
||||
"""
|
||||
@spec resource((() -> acc), (acc -> {[element], acc} | {:halt, acc}), (acc -> term)) ::
|
||||
Enumerable.t()
|
||||
@@ -1403,23 +1418,21 @@ defmodule Stream do
|
||||
|
||||
defp do_resource(next_acc, next_fun, {:cont, acc}, fun, after_fun) do
|
||||
try do
|
||||
# Optimize the most common cases
|
||||
case next_fun.(next_acc) do
|
||||
{[], next_acc} -> {:opt, {:cont, acc}, next_acc}
|
||||
{[v], next_acc} -> {:opt, fun.(v, acc), next_acc}
|
||||
{_, _} = other -> other
|
||||
end
|
||||
next_fun.(next_acc)
|
||||
catch
|
||||
kind, reason ->
|
||||
after_fun.(next_acc)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
{:opt, acc, next_acc} ->
|
||||
do_resource(next_acc, next_fun, acc, fun, after_fun)
|
||||
|
||||
{:halt, next_acc} ->
|
||||
do_resource(next_acc, next_fun, {:halt, acc}, fun, after_fun)
|
||||
|
||||
{[], next_acc} ->
|
||||
do_resource(next_acc, next_fun, {:cont, acc}, fun, after_fun)
|
||||
|
||||
{[v], next_acc} ->
|
||||
do_element_resource(next_acc, next_fun, acc, fun, after_fun, v)
|
||||
|
||||
{list, next_acc} when is_list(list) ->
|
||||
reduce = &Enumerable.List.reduce(list, &1, fun)
|
||||
do_list_resource(next_acc, next_fun, {:cont, acc}, fun, after_fun, reduce)
|
||||
@@ -1431,6 +1444,19 @@ defmodule Stream do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_element_resource(next_acc, next_fun, acc, fun, after_fun, v) do
|
||||
try do
|
||||
fun.(v, acc)
|
||||
catch
|
||||
kind, reason ->
|
||||
after_fun.(next_acc)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
acc ->
|
||||
do_resource(next_acc, next_fun, acc, fun, after_fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_list_resource(next_acc, next_fun, acc, fun, after_fun, reduce) do
|
||||
try do
|
||||
reduce.(acc)
|
||||
|
||||
+111
-68
@@ -2,25 +2,85 @@ import Kernel, except: [length: 1]
|
||||
|
||||
defmodule String do
|
||||
@moduledoc ~S"""
|
||||
A String in Elixir is a UTF-8 encoded binary.
|
||||
Strings in Elixir are UTF-8 encoded binaries.
|
||||
|
||||
Strings in Elixir are a sequence of Unicode characters,
|
||||
typically written between double quoted strings, such
|
||||
as `"hello"` and `"héllò"`.
|
||||
|
||||
In case a string must have a double-quote in itself,
|
||||
the double quotes must be escaped with a backslash,
|
||||
for example: `"this is a string with \"double quotes\""`.
|
||||
|
||||
You can concatenate two strings with the `<>/2` operator:
|
||||
|
||||
iex> "hello" <> " " <> "world"
|
||||
"hello world"
|
||||
|
||||
## Interpolation
|
||||
|
||||
Strings in Elixir also support interpolation. This allows
|
||||
you to place some value in the middle of a string by using
|
||||
the `#{}` syntax:
|
||||
|
||||
iex> name = "joe"
|
||||
iex> "hello #{name}"
|
||||
"hello joe"
|
||||
|
||||
Any Elixir expression is valid inside the interpolation.
|
||||
If a string is given, the string is interpolated as is.
|
||||
If any other value is given, Elixir will attempt to convert
|
||||
it to a string using the `String.Chars` protocol. This
|
||||
allows, for example, to output an integer from the interpolation:
|
||||
|
||||
iex> "2 + 2 = #{2 + 2}"
|
||||
"2 + 2 = 4"
|
||||
|
||||
In case the value you want to interpolate cannot be
|
||||
converted to a string, because it doesn't have an human
|
||||
textual representation, a protocol error will be raised.
|
||||
|
||||
## Escape characters
|
||||
|
||||
Besides allowing double-quotes to be escaped with a backslash,
|
||||
strings also support the following escape characters:
|
||||
|
||||
* `\a` - Bell
|
||||
* `\b` - Backspace
|
||||
* `\t` - Horizontal tab
|
||||
* `\n` - Line feed (New lines)
|
||||
* `\v` - Vertical tab
|
||||
* `\f` - Form feed
|
||||
* `\r` - Carriage return
|
||||
* `\e` - Command Escape
|
||||
* `\#` - Returns the `#` character itself, skipping interpolation
|
||||
* `\xNN` - A byte represented by the hexadecimal `NN`
|
||||
* `\uNNNN` - A Unicode code point represented by `NNNN`
|
||||
|
||||
Note it is generally not advised to use `\xNN` in Elixir
|
||||
strings, as introducing an invalid byte sequence would
|
||||
make the string invalid. If you have to introduce a
|
||||
character by its hexdecimal representation, it is best
|
||||
to work with Unicode code points, such as `\uNNNN`. In fact,
|
||||
understanding Unicode code points can be essential when doing
|
||||
low-level manipulations of string, so let's explore them in
|
||||
detail next.
|
||||
|
||||
## Code points and grapheme cluster
|
||||
|
||||
The functions in this module act according to the Unicode
|
||||
Standard, version 11.0.0.
|
||||
Standard, version 12.1.0.
|
||||
|
||||
As per the standard, a code point is a single Unicode Character,
|
||||
which may be represented by one or more bytes.
|
||||
|
||||
For example, the code point "é" is two bytes:
|
||||
|
||||
iex> byte_size("é")
|
||||
2
|
||||
|
||||
However, this module returns the proper length:
|
||||
For example, although the code point "é" is a single character,
|
||||
its underlying representation uses two bytes:
|
||||
|
||||
iex> String.length("é")
|
||||
1
|
||||
iex> byte_size("é")
|
||||
2
|
||||
|
||||
Furthermore, this module also presents the concept of grapheme cluster
|
||||
(from now on referenced as graphemes). Graphemes can consist of multiple
|
||||
@@ -49,11 +109,8 @@ defmodule String do
|
||||
|
||||
In general, the functions in this module rely on the Unicode
|
||||
Standard, but do not contain any of the locale specific behaviour.
|
||||
|
||||
More information about graphemes can be found in the [Unicode
|
||||
Standard Annex #29](https://www.unicode.org/reports/tr29/).
|
||||
The current Elixir version implements Extended Grapheme Cluster
|
||||
algorithm.
|
||||
|
||||
For converting a binary to a different encoding and for Unicode
|
||||
normalization mechanisms, see Erlang's `:unicode` module.
|
||||
@@ -74,7 +131,7 @@ defmodule String do
|
||||
|
||||
* `Kernel.binary_part/3` - retrieves part of the binary
|
||||
* `Kernel.bit_size/1` and `Kernel.byte_size/1` - size related functions
|
||||
* `Kernel.is_bitstring/1` and `Kernel.is_binary/1` - type checking function
|
||||
* `Kernel.is_bitstring/1` and `Kernel.is_binary/1` - type-check function
|
||||
* Plus a number of functions for working with binaries (bytes)
|
||||
in the [`:binary` module](http://www.erlang.org/doc/man/binary.html)
|
||||
|
||||
@@ -160,9 +217,16 @@ defmodule String do
|
||||
As we have seen above, code points can be inserted into
|
||||
a string by their hexadecimal code:
|
||||
|
||||
"ol\u0061\u0301" #=>
|
||||
iex> "ol\u00E1"
|
||||
"olá"
|
||||
|
||||
Finally, to convert a String into a list of integers
|
||||
code points, usually known as "char lists", you can call
|
||||
`Strig.to_charlist`:
|
||||
|
||||
iex> String.to_charlist("olá")
|
||||
[111, 108, 225]
|
||||
|
||||
## Self-synchronization
|
||||
|
||||
The UTF-8 encoding is self-synchronizing. This means that
|
||||
@@ -180,7 +244,7 @@ defmodule String do
|
||||
responsible to check the validity of the encoding. `String.chunk/2`
|
||||
can be used for breaking a string into valid and invalid parts.
|
||||
|
||||
## Patterns
|
||||
## Compile binary patterns
|
||||
|
||||
Many functions in this module work with patterns. For example,
|
||||
`String.split/2` can split a string into multiple strings given
|
||||
@@ -212,7 +276,7 @@ defmodule String do
|
||||
"""
|
||||
@type t :: binary
|
||||
|
||||
@typedoc "A UTF-8 code point. It may be one or more bytes."
|
||||
@typedoc "A single Unicode code point encoded in UTF-8. It may be one or more bytes."
|
||||
@type codepoint :: t
|
||||
|
||||
@typedoc "Multiple code points that may be perceived as a single character by readers"
|
||||
@@ -391,13 +455,13 @@ defmodule String do
|
||||
For example, take the grapheme "é" which is made of the characters
|
||||
"e" and the acute accent. The following will split the string into two parts:
|
||||
|
||||
iex> String.split(String.normalize("é", :nfd), "e")
|
||||
iex> String.split(:unicode.characters_to_nfd_binary("é"), "e")
|
||||
["", "́"]
|
||||
|
||||
However, if "é" is represented by the single character "e with acute"
|
||||
accent, then it will split the string into just one part:
|
||||
|
||||
iex> String.split(String.normalize("é", :nfc), "e")
|
||||
iex> String.split(:unicode.characters_to_nfc_binary("é"), "e")
|
||||
["é"]
|
||||
|
||||
"""
|
||||
@@ -420,15 +484,18 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
def split(string, pattern, []) when is_tuple(pattern) or is_binary(string) do
|
||||
:binary.split(string, pattern, [:global])
|
||||
end
|
||||
|
||||
def split(string, pattern, options) when is_binary(string) do
|
||||
parts = Keyword.get(options, :parts, :infinity)
|
||||
trim = Keyword.get(options, :trim, false)
|
||||
pattern = maybe_compile_pattern(pattern)
|
||||
split_each(string, pattern, trim, parts_to_index(parts))
|
||||
|
||||
case {parts, trim} do
|
||||
{:infinity, false} ->
|
||||
:binary.split(string, pattern, [:global])
|
||||
|
||||
_ ->
|
||||
pattern = maybe_compile_pattern(pattern)
|
||||
split_each(string, pattern, trim, parts_to_index(parts))
|
||||
end
|
||||
end
|
||||
|
||||
defp parts_to_index(:infinity), do: 0
|
||||
@@ -607,34 +674,8 @@ defmodule String do
|
||||
normalize(string1, :nfd) == normalize(string2, :nfd)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts all characters in `string` to Unicode normalization
|
||||
form identified by `form`.
|
||||
|
||||
## Forms
|
||||
|
||||
The supported forms are:
|
||||
|
||||
* `:nfd` - Normalization Form Canonical Decomposition.
|
||||
Characters are decomposed by canonical equivalence, and
|
||||
multiple combining characters are arranged in a specific
|
||||
order.
|
||||
|
||||
* `:nfc` - Normalization Form Canonical Composition.
|
||||
Characters are decomposed and then recomposed by canonical equivalence.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.normalize("yêṩ", :nfd)
|
||||
"yêṩ"
|
||||
|
||||
iex> String.normalize("leña", :nfc)
|
||||
"leña"
|
||||
|
||||
"""
|
||||
# TODO: Fully deprecate it on v1.10
|
||||
@doc deprecated:
|
||||
"Use :unicode.characters_to_nfc_binary/1 or :unicode.characters_to_nfd_binary/1 instead"
|
||||
@doc false
|
||||
@deprecated "Use :unicode.characters_to_nfc_binary/1 or :unicode.characters_to_nfd_binary/1 instead"
|
||||
def normalize(string, form)
|
||||
|
||||
def normalize(string, :nfd) do
|
||||
@@ -1303,7 +1344,7 @@ defmodule String do
|
||||
"a-b,c"
|
||||
|
||||
The pattern may also be a list of strings and the replacement may also
|
||||
be a function that receives the matched patterns:
|
||||
be a function that receives the matches:
|
||||
|
||||
iex> String.replace("a,b,c", ["a", "c"], fn <<char>> -> <<char + 1>> end)
|
||||
"b,b,d"
|
||||
@@ -1317,8 +1358,7 @@ defmodule String do
|
||||
|
||||
Notice we had to escape the backslash escape character (i.e., we used `\\N`
|
||||
instead of just `\N` to escape the backslash; same thing for `\\g{N}`). By
|
||||
giving `\0`, one can inject the whole matched pattern in the replacement
|
||||
string.
|
||||
giving `\0`, one can inject the whole match in the replacement string.
|
||||
|
||||
A compiled pattern can also be given:
|
||||
|
||||
@@ -1340,35 +1380,38 @@ defmodule String do
|
||||
"""
|
||||
@spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), keyword) :: t
|
||||
def replace(subject, pattern, replacement, options \\ [])
|
||||
when is_binary(subject) and
|
||||
(is_binary(replacement) or is_function(replacement, 1)) and
|
||||
is_list(options) do
|
||||
replace_guarded(subject, pattern, replacement, options)
|
||||
end
|
||||
|
||||
def replace(subject, %{__struct__: Regex} = regex, replacement, options)
|
||||
when is_binary(replacement) or is_function(replacement, 1) do
|
||||
defp replace_guarded(subject, %{__struct__: Regex} = regex, replacement, options) do
|
||||
Regex.replace(regex, subject, replacement, options)
|
||||
end
|
||||
|
||||
def replace(subject, "", "", _) when is_binary(subject) do
|
||||
defp replace_guarded(subject, "", "", _) do
|
||||
subject
|
||||
end
|
||||
|
||||
def replace(subject, "", replacement, options)
|
||||
when is_binary(subject) and is_binary(replacement) do
|
||||
defp replace_guarded(subject, "", replacement_binary, options)
|
||||
when is_binary(replacement_binary) do
|
||||
if Keyword.get(options, :global, true) do
|
||||
IO.iodata_to_binary([replacement | intersperse_bin(subject, replacement)])
|
||||
IO.iodata_to_binary([replacement_binary | intersperse_bin(subject, replacement_binary)])
|
||||
else
|
||||
replacement <> subject
|
||||
replacement_binary <> subject
|
||||
end
|
||||
end
|
||||
|
||||
def replace(subject, "", replacement, options)
|
||||
when is_binary(subject) and is_function(replacement, 1) do
|
||||
defp replace_guarded(subject, "", replacement_fun, options) do
|
||||
if Keyword.get(options, :global, true) do
|
||||
IO.iodata_to_binary([replacement.("") | intersperse_fun(subject, replacement)])
|
||||
IO.iodata_to_binary([replacement_fun.("") | intersperse_fun(subject, replacement_fun)])
|
||||
else
|
||||
IO.iodata_to_binary([replacement.("") | subject])
|
||||
IO.iodata_to_binary([replacement_fun.("") | subject])
|
||||
end
|
||||
end
|
||||
|
||||
def replace(subject, pattern, replacement, options) when is_binary(subject) do
|
||||
defp replace_guarded(subject, pattern, replacement, options) do
|
||||
if insert = Keyword.get(options, :insert_replaced) do
|
||||
IO.warn(
|
||||
"String.replace/4 with :insert_replaced option is deprecated. " <>
|
||||
@@ -2250,7 +2293,7 @@ defmodule String do
|
||||
Passing a string that does not represent an integer leads to an error:
|
||||
|
||||
String.to_integer("invalid data")
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@spec to_integer(String.t()) :: integer
|
||||
@@ -2292,7 +2335,7 @@ defmodule String do
|
||||
3.0
|
||||
|
||||
String.to_float("3")
|
||||
#=> ** (ArgumentError) argument error
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@spec to_float(String.t()) :: float
|
||||
|
||||
+36
-26
@@ -21,14 +21,19 @@ defmodule StringIO do
|
||||
`string` will be the initial input of the newly created
|
||||
device.
|
||||
|
||||
If the `:capture_prompt` option is set to `true`,
|
||||
prompts (specified as arguments to `IO.get*` functions)
|
||||
are captured in the output.
|
||||
|
||||
The device will be created and sent to the function given.
|
||||
When the function returns, the device will be closed. The final
|
||||
result will be a tuple with `:ok` and the result of the function.
|
||||
|
||||
## Options
|
||||
|
||||
* `:capture_prompt` - if set to `true`, prompts (specified as
|
||||
arguments to `IO.get*` functions) are captured in the output.
|
||||
Defaults to `false`.
|
||||
|
||||
* `:encoding` (since v1.10.0) - encoding of the IO device. Allowed
|
||||
values are `:unicode` (default) and `:latin1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> StringIO.open("foo", [], fn pid ->
|
||||
@@ -169,7 +174,8 @@ defmodule StringIO do
|
||||
@impl true
|
||||
def init({string, options}) do
|
||||
capture_prompt = options[:capture_prompt] || false
|
||||
{:ok, %{input: string, output: "", capture_prompt: capture_prompt}}
|
||||
encoding = options[:encoding] || :unicode
|
||||
{:ok, %{encoding: encoding, input: string, output: "", capture_prompt: capture_prompt}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -197,7 +203,7 @@ defmodule StringIO do
|
||||
|
||||
defp io_request(from, reply_as, req, state) do
|
||||
{reply, state} = io_request(req, state)
|
||||
io_reply(from, reply_as, to_reply(reply))
|
||||
io_reply(from, reply_as, reply)
|
||||
state
|
||||
end
|
||||
|
||||
@@ -245,12 +251,16 @@ defmodule StringIO do
|
||||
get_line(encoding, "", state)
|
||||
end
|
||||
|
||||
defp io_request({:setopts, [encoding: encoding]}, state) when encoding in [:latin1, :unicode] do
|
||||
{:ok, %{state | encoding: encoding}}
|
||||
end
|
||||
|
||||
defp io_request({:setopts, _opts}, state) do
|
||||
{{:error, :enotsup}, state}
|
||||
end
|
||||
|
||||
defp io_request(:getopts, state) do
|
||||
{{:ok, [binary: true, encoding: :unicode]}, state}
|
||||
{[binary: true, encoding: state.encoding], state}
|
||||
end
|
||||
|
||||
defp io_request({:get_geometry, :columns}, state) do
|
||||
@@ -271,10 +281,10 @@ defmodule StringIO do
|
||||
|
||||
## put_chars
|
||||
|
||||
defp put_chars(encoding, chars, req, %{output: output} = state) do
|
||||
case :unicode.characters_to_binary(chars, encoding, :unicode) do
|
||||
defp put_chars(encoding, chars, req, state) do
|
||||
case :unicode.characters_to_binary(chars, encoding, state.encoding) do
|
||||
string when is_binary(string) ->
|
||||
{:ok, %{state | output: output <> string}}
|
||||
{:ok, %{state | output: state.output <> string}}
|
||||
|
||||
{_, _, _} ->
|
||||
{{:error, req}, state}
|
||||
@@ -308,22 +318,25 @@ defmodule StringIO do
|
||||
{chars, rest}
|
||||
end
|
||||
|
||||
defp get_chars(input, encoding, count) do
|
||||
try do
|
||||
case :file_io_server.count_and_find(input, count, encoding) do
|
||||
{buf_count, split_pos} when buf_count < count or split_pos == :none ->
|
||||
{input, ""}
|
||||
|
||||
{_buf_count, split_pos} ->
|
||||
<<chars::binary-size(split_pos), rest::binary>> = input
|
||||
{chars, rest}
|
||||
end
|
||||
catch
|
||||
:exit, :invalid_unicode ->
|
||||
{:error, :invalid_unicode}
|
||||
defp get_chars(input, :unicode, count) do
|
||||
with {:ok, count} <- split_at(input, count, 0) do
|
||||
<<chars::binary-size(count), rest::binary>> = input
|
||||
{chars, rest}
|
||||
end
|
||||
end
|
||||
|
||||
defp split_at(_, 0, acc),
|
||||
do: {:ok, acc}
|
||||
|
||||
defp split_at(<<h::utf8, t::binary>>, count, acc),
|
||||
do: split_at(t, count - 1, acc + byte_size(<<h::utf8>>))
|
||||
|
||||
defp split_at(<<_, _::binary>>, _count, _acc),
|
||||
do: {:error, :invalid_unicode}
|
||||
|
||||
defp split_at(<<>>, _count, acc),
|
||||
do: {:ok, acc}
|
||||
|
||||
## get_line
|
||||
|
||||
defp get_line(encoding, prompt, %{input: input} = state) do
|
||||
@@ -445,7 +458,4 @@ defmodule StringIO do
|
||||
defp io_reply(from, reply_as, reply) do
|
||||
send(from, {:io_reply, reply_as, reply})
|
||||
end
|
||||
|
||||
defp to_reply(list) when is_list(list), do: IO.chardata_to_string(list)
|
||||
defp to_reply(other), do: other
|
||||
end
|
||||
|
||||
@@ -108,8 +108,8 @@ defmodule Supervisor do
|
||||
The child specification describes how the supervisor starts, shuts down,
|
||||
and restarts child processes.
|
||||
|
||||
The child specification contains 6 keys. The first two are required,
|
||||
and the remaining ones are optional:
|
||||
The child specification is a map which contains 6 elements. The first two keys
|
||||
in the following list are required, and the remaining ones are optional:
|
||||
|
||||
* `:id` - any term used to identify the child specification
|
||||
internally by the supervisor; defaults to the given module.
|
||||
@@ -123,16 +123,16 @@ defmodule Supervisor do
|
||||
should be restarted (see the "Restart values" section below).
|
||||
This key is optional and defaults to `:permanent`.
|
||||
|
||||
* `:shutdown` - an atom that defines how a child process should be
|
||||
terminated (see the "Shutdown values" section below). This key
|
||||
is optional and defaults to `5000` if the type is `:worker` or
|
||||
* `:shutdown` - an integer or atom that defines how a child process should
|
||||
be terminated (see the "Shutdown values" section below). This key
|
||||
is optional and defaults to `5_000` if the type is `:worker` or
|
||||
`:infinity` if the type is `:supervisor`.
|
||||
|
||||
* `:type` - specifies that the child process is a `:worker` or a
|
||||
`:supervisor`. This key is optional and defaults to `:worker`.
|
||||
|
||||
There is a sixth key, `:modules`, that is rarely changed. It is set
|
||||
automatically based on the value in `:start`.
|
||||
There is a sixth key, `:modules`, which is optional and is rarely changed.
|
||||
It is set automatically based on the `:start` value.
|
||||
|
||||
Let's understand what the `:shutdown` and `:restart` options control.
|
||||
|
||||
@@ -306,7 +306,6 @@ defmodule Supervisor do
|
||||
following options:
|
||||
|
||||
* `:id` - the child specification identifier, defaults to the current module
|
||||
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
|
||||
* `:restart` - when the supervisor should be restarted, defaults to `:permanent`
|
||||
|
||||
The `@doc` annotation immediately preceding `use Supervisor` will be
|
||||
@@ -369,12 +368,7 @@ defmodule Supervisor do
|
||||
In the above, process termination refers to unsuccessful termination, which
|
||||
is determined by the `:restart` option.
|
||||
|
||||
There is also a deprecated strategy called `:simple_one_for_one` which
|
||||
has been replaced by the `DynamicSupervisor`. The `:simple_one_for_one`
|
||||
supervisor was similar to `:one_for_one` but suits better when dynamically
|
||||
attaching children. Many functions in this module behaved slightly
|
||||
differently when this strategy was used. See the `DynamicSupervisor` module
|
||||
for more information and migration strategies.
|
||||
To dynamically supervise children, see `DynamicSupervisor`.
|
||||
|
||||
### Name registration
|
||||
|
||||
@@ -451,7 +445,7 @@ defmodule Supervisor do
|
||||
import Supervisor.Spec
|
||||
@behaviour Supervisor
|
||||
|
||||
if Module.get_attribute(__MODULE__, :doc) == nil do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -501,10 +495,7 @@ defmodule Supervisor do
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc "Option values used by the `start*` functions"
|
||||
@type option :: {:name, name} | init_option()
|
||||
|
||||
@typedoc "Options used by the `start*` functions"
|
||||
@type options :: [option, ...]
|
||||
@type option :: {:name, name}
|
||||
|
||||
@typedoc "The supervisor reference"
|
||||
@type supervisor :: pid | name | {atom, node}
|
||||
@@ -558,7 +549,7 @@ defmodule Supervisor do
|
||||
process and exits not only on crashes but also if the parent process exits
|
||||
with `:normal` reason.
|
||||
"""
|
||||
@spec start_link([:supervisor.child_spec() | {module, term} | module], options) ::
|
||||
@spec start_link([:supervisor.child_spec() | {module, term} | module], [option | init_option]) ::
|
||||
{:ok, pid} | {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
def start_link(children, options) when is_list(children) do
|
||||
{sup_opts, start_opts} = Keyword.split(options, [:strategy, :max_seconds, :max_restarts])
|
||||
@@ -589,8 +580,7 @@ defmodule Supervisor do
|
||||
## Options
|
||||
|
||||
* `:strategy` - the supervision strategy option. It can be either
|
||||
`:one_for_one`, `:rest_for_one`, `:one_for_all`, or the deprecated
|
||||
`:simple_one_for_one`.
|
||||
`:one_for_one`, `:rest_for_one`, or `:one_for_all`
|
||||
|
||||
* `:max_restarts` - the maximum number of restarts allowed in
|
||||
a time frame. Defaults to `3`.
|
||||
@@ -603,12 +593,23 @@ defmodule Supervisor do
|
||||
description of the available strategies.
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
# TODO: Warn if simple_one_for_one strategy is used on Elixir v1.10
|
||||
@spec init([:supervisor.child_spec() | {module, term} | module], [init_option]) :: {:ok, tuple}
|
||||
def init(children, options) when is_list(children) and is_list(options) do
|
||||
unless strategy = options[:strategy] do
|
||||
raise ArgumentError, "expected :strategy option to be given"
|
||||
end
|
||||
strategy =
|
||||
case options[:strategy] do
|
||||
nil ->
|
||||
raise ArgumentError, "expected :strategy option to be given"
|
||||
|
||||
:simple_one_for_one ->
|
||||
IO.warn(
|
||||
":simple_one_for_one strategy is deprecated, please use DynamicSupervisor instead"
|
||||
)
|
||||
|
||||
:simple_one_for_one
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
|
||||
intensity = Keyword.get(options, :max_restarts, 3)
|
||||
period = Keyword.get(options, :max_seconds, 5)
|
||||
@@ -762,7 +763,7 @@ defmodule Supervisor do
|
||||
# It is important to keep the 2-arity spec because it is a catch
|
||||
# all to start_link(children, options).
|
||||
@spec start_link(module, term) :: on_start
|
||||
@spec start_link(module, term, GenServer.options()) :: on_start
|
||||
@spec start_link(module, term, [option]) :: on_start
|
||||
def start_link(module, init_arg, options \\ []) when is_list(options) do
|
||||
case Keyword.get(options, :name) do
|
||||
nil ->
|
||||
@@ -815,14 +816,18 @@ defmodule Supervisor do
|
||||
returns `{:error, error}` where `error` is a term containing information about
|
||||
the error and child specification.
|
||||
"""
|
||||
@spec start_child(supervisor, :supervisor.child_spec() | {module, term} | module | [term]) ::
|
||||
@spec start_child(supervisor, :supervisor.child_spec() | {module, term} | module) ::
|
||||
on_start_child
|
||||
def start_child(supervisor, {_, _, _, _, _, _} = child_spec) do
|
||||
call(supervisor, {:start_child, child_spec})
|
||||
end
|
||||
|
||||
# TODO: Deprecate this clause on Elixir v1.10
|
||||
def start_child(supervisor, args) when is_list(args) do
|
||||
# TODO: Deprecate in v1.11
|
||||
# IO.warn(
|
||||
# "Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead"
|
||||
# )
|
||||
|
||||
call(supervisor, {:start_child, args})
|
||||
end
|
||||
|
||||
@@ -844,12 +849,15 @@ defmodule Supervisor do
|
||||
specification for the given child ID, this function returns
|
||||
`{:error, :not_found}`.
|
||||
"""
|
||||
@spec terminate_child(supervisor, term()) :: :ok | {:error, error}
|
||||
when error: :not_found | :simple_one_for_one
|
||||
@spec terminate_child(supervisor, term()) :: :ok | {:error, :not_found}
|
||||
def terminate_child(supervisor, child_id)
|
||||
|
||||
# TODO: Deprecate this clause on Elixir v1.10
|
||||
def terminate_child(supervisor, pid) when is_pid(pid) do
|
||||
# TODO: Deprecate in v1.11
|
||||
# IO.warn(
|
||||
# "Supervisor.terminate_child/2 with a PID is deprecated, please use DynamicSupervisor instead"
|
||||
# )
|
||||
|
||||
call(supervisor, {:terminate_child, pid})
|
||||
end
|
||||
|
||||
@@ -868,7 +876,7 @@ defmodule Supervisor do
|
||||
current process is running or being restarted.
|
||||
"""
|
||||
@spec delete_child(supervisor, term()) :: :ok | {:error, error}
|
||||
when error: :not_found | :simple_one_for_one | :running | :restarting
|
||||
when error: :not_found | :running | :restarting
|
||||
def delete_child(supervisor, child_id) do
|
||||
call(supervisor, {:delete_child, child_id})
|
||||
end
|
||||
@@ -896,7 +904,7 @@ defmodule Supervisor do
|
||||
or if it fails, this function returns `{:error, error}`.
|
||||
"""
|
||||
@spec restart_child(supervisor, term()) :: {:ok, child} | {:ok, child, term} | {:error, error}
|
||||
when error: :not_found | :simple_one_for_one | :running | :restarting | term
|
||||
when error: :not_found | :running | :restarting | term
|
||||
def restart_child(supervisor, child_id) do
|
||||
call(supervisor, {:restart_child, child_id})
|
||||
end
|
||||
@@ -951,7 +959,7 @@ defmodule Supervisor do
|
||||
workers: non_neg_integer
|
||||
}
|
||||
def count_children(supervisor) do
|
||||
call(supervisor, :count_children) |> :maps.from_list()
|
||||
call(supervisor, :count_children) |> Map.new()
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -109,9 +109,6 @@ defmodule Supervisor.Spec do
|
||||
@moduledoc deprecated:
|
||||
"Use the new child specifications outlined in the Supervisor module instead"
|
||||
|
||||
# TODO: Deprecate all functions in this module on Elixir v1.9.
|
||||
# Also deprecate entry in Supervisor.Default.
|
||||
|
||||
@typedoc "Supported strategies"
|
||||
@type strategy :: :simple_one_for_one | :one_for_one | :one_for_all | :rest_for_one
|
||||
|
||||
@@ -169,6 +166,7 @@ defmodule Supervisor.Spec do
|
||||
max_restarts: non_neg_integer,
|
||||
max_seconds: pos_integer
|
||||
) :: {:ok, tuple}
|
||||
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
def supervise(children, options) do
|
||||
unless strategy = options[:strategy] do
|
||||
raise ArgumentError, "expected :strategy option to be given"
|
||||
@@ -236,6 +234,8 @@ defmodule Supervisor.Spec do
|
||||
function: atom,
|
||||
modules: modules
|
||||
) :: spec
|
||||
# TODO: Deprecate on v1.11
|
||||
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
def worker(module, args, options \\ []) do
|
||||
child(:worker, module, args, options)
|
||||
end
|
||||
@@ -269,6 +269,8 @@ defmodule Supervisor.Spec do
|
||||
function: atom,
|
||||
modules: modules
|
||||
) :: spec
|
||||
# TODO: Deprecate on v1.11
|
||||
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
def supervisor(module, args, options \\ []) do
|
||||
options = Keyword.put_new(options, :shutdown, :infinity)
|
||||
child(:supervisor, module, args, options)
|
||||
|
||||
+20
-12
@@ -186,7 +186,7 @@ defmodule System do
|
||||
* `:build` - the Elixir version, short Git revision hash and
|
||||
Erlang/OTP release it was compiled with
|
||||
* `:date` - a string representation of the ISO8601 date and time it was built
|
||||
* `:opt_release` - OTP release it was compiled with
|
||||
* `:otp_release` - OTP release it was compiled with
|
||||
* `:revision` - short Git revision hash. If Git was not available at building
|
||||
time, it is set to `""`
|
||||
* `:version` - the Elixir version
|
||||
@@ -312,7 +312,9 @@ defmodule System do
|
||||
"""
|
||||
@spec user_home() :: String.t() | nil
|
||||
def user_home do
|
||||
:elixir_config.get(:home)
|
||||
{:ok, [[home] | _]} = :init.get_argument(:home)
|
||||
encoding = :file.native_name_encoding()
|
||||
:unicode.characters_to_binary(home, encoding, encoding)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -335,7 +337,7 @@ defmodule System do
|
||||
1. the directory named by the TMPDIR environment variable
|
||||
2. the directory named by the TEMP environment variable
|
||||
3. the directory named by the TMP environment variable
|
||||
4. `C:\TMP` on Windows or `/tmp` on Unix
|
||||
4. `C:\TMP` on Windows or `/tmp` on Unix-like operating systems
|
||||
5. as a last resort, the current working directory
|
||||
|
||||
Returns `nil` if none of the above are writable.
|
||||
@@ -396,7 +398,7 @@ defmodule System do
|
||||
|
||||
The handler always executes in a different process from the one it was
|
||||
registered in. As a consequence, any resources managed by the calling process
|
||||
(ETS tables, open files, etc.) won't be available by the time the handler
|
||||
(ETS tables, open files, and others) won't be available by the time the handler
|
||||
function is invoked.
|
||||
|
||||
The function must receive the exit status code as an argument.
|
||||
@@ -411,8 +413,8 @@ defmodule System do
|
||||
Locates an executable on the system.
|
||||
|
||||
This function looks up an executable program given
|
||||
its name using the environment variable PATH on Unix
|
||||
and Windows. It also considers the proper executable
|
||||
its name using the environment variable PATH on Windows and Unix-like
|
||||
operating systems. It also considers the proper executable
|
||||
extension for each operating system, so for Windows it will try to
|
||||
lookup files with `.com`, `.cmd` or similar extensions.
|
||||
"""
|
||||
@@ -527,6 +529,8 @@ defmodule System do
|
||||
|
||||
For more information, see `:os.getpid/0`.
|
||||
"""
|
||||
# TODO: deprecate permanently on v1.13
|
||||
@doc deprecated: "Use System.pid/0 instead"
|
||||
@spec get_pid() :: binary
|
||||
def get_pid, do: IO.iodata_to_binary(:os.getpid())
|
||||
|
||||
@@ -634,7 +638,7 @@ defmodule System do
|
||||
Returns the operating system PID for the current Erlang runtime system instance.
|
||||
|
||||
Returns a string containing the (usually) numerical identifier for a process.
|
||||
On UNIX, this is typically the return value of the `getpid()` system call.
|
||||
On Unix-like operating systems, this is typically the return value of the `getpid()` system call.
|
||||
On Windows, the process ID as returned by the `GetCurrentProcessId()` system
|
||||
call is used.
|
||||
|
||||
@@ -910,10 +914,14 @@ defmodule System do
|
||||
`convert_time_unit/3` accepts an additional time unit (other than the
|
||||
ones in the `t:time_unit/0` type) called `:native`. `:native` is the time
|
||||
unit used by the Erlang runtime system. It's determined when the runtime
|
||||
starts and stays the same until the runtime is stopped. To determine what
|
||||
the `:native` unit amounts to in a system, you can call this function to
|
||||
convert 1 second to the `:native` time unit (i.e.,
|
||||
`System.convert_time_unit(1, :second, :native)`).
|
||||
starts and stays the same until the runtime is stopped, but could differ
|
||||
the next time the runtime is started on the same machine. For this reason,
|
||||
you should use this function to convert `:native` time units to a predictable
|
||||
unit before you display them to humans.
|
||||
|
||||
To determine how many seconds the `:native` unit represents in your current
|
||||
runtime, you can can call this function to convert 1 second to the `:native`
|
||||
time unit: `System.convert_time_unit(1, :second, :native)`.
|
||||
"""
|
||||
@spec convert_time_unit(integer, time_unit | :native, time_unit | :native) :: integer
|
||||
def convert_time_unit(time, from_unit, to_unit) do
|
||||
@@ -940,7 +948,7 @@ defmodule System do
|
||||
time and the Erlang VM system time.
|
||||
|
||||
The result is returned in the given time unit `unit`. The returned
|
||||
offset, added to an Erlang monotonic time (e.g., obtained with
|
||||
offset, added to an Erlang monotonic time (for instance, one obtained with
|
||||
`monotonic_time/1`), gives the Erlang system time that corresponds
|
||||
to that monotonic time.
|
||||
"""
|
||||
|
||||
@@ -97,7 +97,6 @@ defmodule Task do
|
||||
`child_spec/1` can be customized with the following options:
|
||||
|
||||
* `:id` - the child specification identifier, defaults to the current module
|
||||
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
|
||||
* `:restart` - when the child should be restarted, defaults to `:temporary`
|
||||
* `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down
|
||||
|
||||
@@ -258,7 +257,7 @@ defmodule Task do
|
||||
@doc false
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
if Module.get_attribute(__MODULE__, :doc) == nil do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
|
||||
@@ -96,7 +96,26 @@ defmodule Task.Supervised do
|
||||
:erlang.raise(:exit, value, __STACKTRACE__)
|
||||
|
||||
kind, value ->
|
||||
log(owner, mfa, {log_value(kind, value), __STACKTRACE__})
|
||||
{fun, args} = get_running(mfa)
|
||||
|
||||
:logger.error(
|
||||
%{
|
||||
label: {Task.Supervisor, :terminating},
|
||||
report: %{
|
||||
name: get_from(owner),
|
||||
starter: self(),
|
||||
function: fun,
|
||||
args: args,
|
||||
reason: {log_value(kind, value), __STACKTRACE__}
|
||||
}
|
||||
},
|
||||
%{
|
||||
domain: [:otp, :elixir],
|
||||
error_logger: %{tag: :error_msg},
|
||||
report_cb: &__MODULE__.format_report/1
|
||||
}
|
||||
)
|
||||
|
||||
:erlang.raise(kind, value, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
@@ -104,16 +123,24 @@ defmodule Task.Supervised do
|
||||
defp log_value(:throw, value), do: {:nocatch, value}
|
||||
defp log_value(_, value), do: value
|
||||
|
||||
defp log(owner, mfa, reason) do
|
||||
{fun, args} = get_running(mfa)
|
||||
|
||||
@doc false
|
||||
def format_report(%{
|
||||
label: {Task.Supervisor, :terminating},
|
||||
report: %{
|
||||
name: name,
|
||||
starter: starter,
|
||||
function: fun,
|
||||
args: args,
|
||||
reason: reason
|
||||
}
|
||||
}) do
|
||||
message =
|
||||
'** Task ~p terminating~n' ++
|
||||
'** Started from ~p~n' ++
|
||||
'** When function == ~p~n' ++
|
||||
'** arguments == ~p~n' ++ '** Reason for termination == ~n' ++ '** ~p~n'
|
||||
|
||||
:error_logger.format(message, [self(), get_from(owner), fun, args, get_reason(reason)])
|
||||
{message, [starter, name, fun, args, get_reason(reason)]}
|
||||
end
|
||||
|
||||
defp get_from({node, pid_or_name, _pid}) when node == node(), do: pid_or_name
|
||||
|
||||
@@ -26,7 +26,7 @@ defmodule Task.Supervisor do
|
||||
|
||||
@typedoc "Option values used by `start_link`"
|
||||
@type option ::
|
||||
Supervisor.option()
|
||||
DynamicSupervisor.option()
|
||||
| {:restart, :supervisor.restart()}
|
||||
| {:shutdown, :supervisor.shutdown()}
|
||||
|
||||
@@ -78,14 +78,21 @@ defmodule Task.Supervisor do
|
||||
give them directly to `start_child` and `async`.
|
||||
"""
|
||||
@spec start_link([option]) :: Supervisor.on_start()
|
||||
# TODO: Deprecate passing restart and shutdown here on Elixir v1.10.
|
||||
def start_link(options \\ []) do
|
||||
{restart, options} = Keyword.pop(options, :restart, :temporary)
|
||||
{shutdown, options} = Keyword.pop(options, :shutdown, 5000)
|
||||
{restart, options} = Keyword.pop(options, :restart)
|
||||
{shutdown, options} = Keyword.pop(options, :shutdown)
|
||||
|
||||
if restart || shutdown do
|
||||
IO.warn(
|
||||
":restart and :shutdown options in Task.Supervisor.start_link/1 " <>
|
||||
"are deprecated. Please pass those options on start_child/3 instead"
|
||||
)
|
||||
end
|
||||
|
||||
keys = [:max_children, :max_seconds, :max_restarts]
|
||||
{sup_opts, start_opts} = Keyword.split(options, keys)
|
||||
DynamicSupervisor.start_link(__MODULE__, {{restart, shutdown}, sup_opts}, start_opts)
|
||||
restart_and_shutdown = {restart || :temporary, shutdown || 5000}
|
||||
DynamicSupervisor.start_link(__MODULE__, {restart_and_shutdown, sup_opts}, start_opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -417,8 +424,10 @@ defmodule Task.Supervisor do
|
||||
|
||||
defp start_child_with_spec(supervisor, args, restart, shutdown) do
|
||||
# TODO: This only exists because we need to support reading restart/shutdown
|
||||
# from two different places. Remove this and the associated clause in DynamicSupervisor
|
||||
# on Elixir v2.0
|
||||
# from two different places. Remove this, the init function and the associated
|
||||
# clause in DynamicSupervisor on Elixir v2.0
|
||||
# TODO: Once we do this, we can also make it so the task arguments are never
|
||||
# sent to the supervisor if the restart is temporary
|
||||
GenServer.call(supervisor, {:start_task, args, restart, shutdown}, :infinity)
|
||||
end
|
||||
|
||||
|
||||
@@ -31,16 +31,17 @@ defmodule Tuple do
|
||||
|
||||
The functions in this module that add and remove elements from tuples are
|
||||
rarely used in practice, as they typically imply tuples are being used as
|
||||
collections. To append to a tuple, it is preferable to use pattern matching:
|
||||
collections. To append to a tuple, it is preferable to extract the elements
|
||||
from the old tuple with pattern matching, and then create a new tuple:
|
||||
|
||||
tuple = {:ok, :example}
|
||||
|
||||
# Avoid
|
||||
Tuple.insert_at(tuple, 2, %{})
|
||||
result = Tuple.insert_at(tuple, 2, %{})
|
||||
|
||||
# Prefer
|
||||
{:ok, atom} = tuple
|
||||
{:ok, atom, %{}}
|
||||
result = {:ok, atom, %{}}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
+53
-13
@@ -74,7 +74,7 @@ defmodule URI do
|
||||
Encodes an enumerable into a query string.
|
||||
|
||||
Takes an enumerable that enumerates as a list of two-element
|
||||
tuples (e.g., a map or a keyword list) and returns a string
|
||||
tuples (for instance, a map or a keyword list) and returns a string
|
||||
in the form of `key1=value1&key2=value2...` where keys and
|
||||
values are URL encoded as per `encode_www_form/1`.
|
||||
|
||||
@@ -234,7 +234,7 @@ defmodule URI do
|
||||
the following characters are unreserved:
|
||||
|
||||
* Alphanumeric characters: `A-Z`, `a-z`, `0-9`
|
||||
* `~`, `_`, `-`
|
||||
* `~`, `_`, `-`, `.`
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -251,7 +251,8 @@ defmodule URI do
|
||||
Checks if `character` is allowed unescaped in a URI.
|
||||
|
||||
This is the default used by `URI.encode/2` where both
|
||||
reserved and unreserved characters are kept unescaped.
|
||||
[reserved](`char_reserved?/1`) and [unreserved characters](`char_unreserved?/1`)
|
||||
are kept unescaped.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -271,14 +272,15 @@ defmodule URI do
|
||||
so-called unreserved characters, which have the same meaning both
|
||||
escaped and unescaped, won't be escaped by default.
|
||||
|
||||
See `encode_www_form` if you are interested in escaping reserved
|
||||
See `encode_www_form/1` if you are interested in escaping reserved
|
||||
characters too.
|
||||
|
||||
This function also accepts a `predicate` function as an optional
|
||||
argument. If passed, this function will be called with each byte
|
||||
in `string` as its argument and should return a truthy value (anything other
|
||||
than `false` or `nil`) if the given byte should be left as is, or return a
|
||||
falsy value (`false` or `nil`) if the character should be escaped.
|
||||
falsy value (`false` or `nil`) if the character should be escaped. Defaults
|
||||
to `URI.char_unescaped?/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -452,18 +454,38 @@ defmodule URI do
|
||||
|
||||
def parse(string) when is_binary(string) do
|
||||
# From https://tools.ietf.org/html/rfc3986#appendix-B
|
||||
# Parts: 12 3 4 5 6 7 8 9
|
||||
regex = ~r{^(([a-z][a-z0-9\+\-\.]*):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?}i
|
||||
|
||||
parts = Regex.run(regex, string)
|
||||
|
||||
destructure [_, _, scheme, _, authority, path, query_with_question_mark, _, _, fragment],
|
||||
destructure [
|
||||
_full,
|
||||
# 1
|
||||
_scheme_with_colon,
|
||||
# 2
|
||||
scheme,
|
||||
# 3
|
||||
authority_with_slashes,
|
||||
# 4
|
||||
_authority,
|
||||
# 5
|
||||
path,
|
||||
# 6
|
||||
query_with_question_mark,
|
||||
# 7
|
||||
_query,
|
||||
# 8
|
||||
_fragment_with_hash,
|
||||
# 9
|
||||
fragment
|
||||
],
|
||||
parts
|
||||
|
||||
scheme = nillify(scheme)
|
||||
authority = nillify(authority)
|
||||
path = nillify(path)
|
||||
query = nillify_query(query_with_question_mark)
|
||||
{userinfo, host, port} = split_authority(authority)
|
||||
{authority, userinfo, host, port} = split_authority(authority_with_slashes)
|
||||
|
||||
scheme = scheme && String.downcase(scheme)
|
||||
port = port || (scheme && default_port(scheme))
|
||||
@@ -484,16 +506,24 @@ defmodule URI do
|
||||
defp nillify_query(_other), do: nil
|
||||
|
||||
# Split an authority into its userinfo, host and port parts.
|
||||
defp split_authority(string) do
|
||||
defp split_authority("") do
|
||||
{nil, nil, nil, nil}
|
||||
end
|
||||
|
||||
defp split_authority("//") do
|
||||
{"", nil, "", nil}
|
||||
end
|
||||
|
||||
defp split_authority("//" <> authority) do
|
||||
regex = ~r/(^(.*)@)?(\[[a-zA-Z0-9:.]*\]|[^:]*)(:(\d*))?/
|
||||
components = Regex.run(regex, string || "")
|
||||
components = Regex.run(regex, authority)
|
||||
|
||||
destructure [_, _, userinfo, host, _, port], components
|
||||
userinfo = nillify(userinfo)
|
||||
host = if nillify(host), do: host |> String.trim_leading("[") |> String.trim_trailing("]")
|
||||
port = if nillify(port), do: String.to_integer(port)
|
||||
|
||||
{userinfo, host, port}
|
||||
{authority, userinfo, host, port}
|
||||
end
|
||||
|
||||
# Regex.run returns empty strings sometimes. We want
|
||||
@@ -506,10 +536,12 @@ defmodule URI do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> URI.to_string(URI.parse("http://google.com"))
|
||||
iex> uri = URI.parse("http://google.com")
|
||||
iex> URI.to_string(uri)
|
||||
"http://google.com"
|
||||
|
||||
iex> URI.to_string(%URI{scheme: "foo", host: "bar.baz"})
|
||||
iex> uri = URI.parse("foo://bar.baz")
|
||||
iex> URI.to_string(uri)
|
||||
"foo://bar.baz"
|
||||
|
||||
Note that when creating this string representation, the `:authority` value will be
|
||||
@@ -624,6 +656,14 @@ defmodule URI do
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: URI do
|
||||
def to_string(%{host: host, authority: authority, path: path} = uri)
|
||||
when (host != nil or authority != nil) and is_binary(path) and
|
||||
path != "" and binary_part(path, 0, 1) != "/" do
|
||||
raise ArgumentError,
|
||||
":path in URI must be nil or an absolute path if :host or :authority are given, " <>
|
||||
"got: #{inspect(uri)}"
|
||||
end
|
||||
|
||||
def to_string(%{scheme: scheme, port: port, path: path, query: query, fragment: fragment} = uri) do
|
||||
uri =
|
||||
case scheme && URI.default_port(scheme) do
|
||||
|
||||
@@ -5,8 +5,8 @@ defmodule Version do
|
||||
A version is a string in a specific format or a `Version`
|
||||
generated after parsing via `Version.parse/1`.
|
||||
|
||||
`Version` parsing and requirements follow
|
||||
[SemVer 2.0 schema](https://semver.org/).
|
||||
Although Elixir projects are not required to follow SemVer,
|
||||
they must follow the format outlined on [SemVer 2.0 schema](https://semver.org/).
|
||||
|
||||
## Versions
|
||||
|
||||
@@ -29,7 +29,7 @@ defmodule Version do
|
||||
## Struct
|
||||
|
||||
The version is represented by the `Version` struct and fields
|
||||
are named according to SemVer: `:major`, `:minor`, `:patch`,
|
||||
are named according to SemVer 2.0: `:major`, `:minor`, `:patch`,
|
||||
`:pre`, and `:build`.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -8,11 +8,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.9 | Bug fixes and security patches
|
||||
1.10 | Bug fixes and security patches
|
||||
1.9 | Security patches only
|
||||
1.8 | Security patches only
|
||||
1.7 | Security patches only
|
||||
1.6 | Security patches only
|
||||
1.5 | Security patches only
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
@@ -52,6 +52,7 @@ Elixir version | Supported Erlang/OTP versions
|
||||
1.7 | 19 - 22
|
||||
1.8 | 20 - 22
|
||||
1.9 | 20 - 22
|
||||
1.10 | 21 - 22
|
||||
|
||||
While Elixir often adds compatibility to new Erlang/OTP versions on released branches, such as support for Erlang/OTP 20 in v1.4.5, those releases usually contain the minimum changes for Elixir to run without errors. Only the next minor release, in this case v1.5.0, does effectively leverage the new features provided by the latest Erlang/OTP release.
|
||||
|
||||
@@ -73,51 +74,64 @@ The first column is the version the feature was hard deprecated. The second colu
|
||||
|
||||
Version | Deprecated feature | Replaced by (available since)
|
||||
:-------| :-------------------------------------------------- | :---------------------------------------------------------------
|
||||
[v1.9] | Passing `:insert_replaced` to `String.replace/4` | Use `:binary.replace/4` (v1.0)
|
||||
[v1.10] | `Code.ensure_compiled?/1` | `Code.ensure_compiled/1` (v1.0)
|
||||
[v1.10] | `Code.load_file/2` | `Code.require_file/2` (v1.0) or `Code.compile_file/2` (v1.7)
|
||||
[v1.10] | `Code.loaded_files/0` | `Code.required_files/0` (v1.7)
|
||||
[v1.10] | `Code.unload_file/1` | `Code.unrequire_files/1` (v1.7)
|
||||
[v1.10] | Passing non-chardata to `Logger.log/2` | Explicitly convert to string with `to_string/1` (v1.0)
|
||||
[v1.10] | `:compile_time_purge_level` in `Logger` app environment | `:compile_time_purge_matching` in `Logger` app environment (v1.7)
|
||||
[v1.10] | `Supervisor.Spec.supervise/2` | The new child specs outlined in `Supervisor` (v1.5)
|
||||
[v1.10] | `String.normalize/2` | `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` (Erlang/OTP 20)
|
||||
[v1.10] | `:simple_one_for_one` strategy in `Supervisor` | `DynamicSupervisor` (v1.6)
|
||||
[v1.10] | `:restart` and `:shutdown` in `Task.Supervisor.start_link/1` | `:restart` and `:shutdown` in `Task.Supervisor.start_child/3` (v1.6)
|
||||
[v1.9] | Enumerable keys in `Map.drop/2`, `Map.split/2`, and `Map.take/2` | Call `Enum.to_list/1` on the second argument before hand (v1.0)
|
||||
[v1.9] | `Mix.Project.load_paths/1` | `Mix.Project.compile_path/1` (v1.0)
|
||||
[v1.9] | `--detached` in CLI | `--erl "-detached"` (v1.0)
|
||||
[v1.9] | Passing `:insert_replaced` to `String.replace/4` | Use `:binary.replace/4` (v1.0)
|
||||
[v1.8] | Passing a non-empty list to `:into` in [`for`](`Kernel.SpecialForms.for/1`) | `Kernel.++/2` or `Keyword.merge/2` (v1.0)
|
||||
[v1.8] | Passing a non-empty list to `Enum.into/2` | `Kernel.++/2` or `Keyword.merge/2` (v1.0)
|
||||
[v1.8] | Passing a non-empty list to `:into` in `for` | `Kernel.++/2` or `Keyword.merge/2` (v1.0)
|
||||
[v1.8] | `:seconds`, `:milliseconds`, etc. as time units | `:second`, `:millisecond`, etc. (v1.4)
|
||||
[v1.8] | Time units in its plural form, such as: `:seconds`, `:milliseconds`, and the like | Use the singular form, such as: `:second`, `:millisecond`, and so on (v1.4)
|
||||
[v1.8] | `Inspect.Algebra.surround/3` | `Inspect.Algebra.concat/2` and `Inspect.Algebra.nest/2` (v1.0)
|
||||
[v1.8] | `Inspect.Algebra.surround_many/6` | `Inspect.Algebra.container_doc/6` (v1.6)
|
||||
[v1.9] | `--detached` in `Kernel.CLI` | `--erl "-detached"` (v1.0)
|
||||
[v1.8] | `Kernel.ParallelCompiler.files/2` | `Kernel.ParallelCompiler.compile/2` (v1.6)
|
||||
[v1.8] | `Kernel.ParallelCompiler.files_to_path/2` | `Kernel.ParallelCompiler.compile_to_path/2` (v1.6)
|
||||
[v1.8] | `Kernel.ParallelRequire.files/2` | `Kernel.ParallelCompiler.require/2` (v1.6)
|
||||
[v1.8] | `System.cwd/0` and `System.cwd!/0` | `File.cwd/0` and `File.cwd!/0` (v1.0)
|
||||
[v1.8] | Returning `{:ok, contents}` or `:error` from `Mix.Compilers.Erlang.compile/6`'s callback | Return `{:ok, contents, warnings}` or `{:error, errors, warnings}` (v1.6)
|
||||
[v1.8] | `System.cwd/0` and `System.cwd!/0` | `File.cwd/0` and `File.cwd!/0` (v1.0)
|
||||
[v1.7] | `Code.get_docs/2` | `Code.fetch_docs/1` (v1.7)
|
||||
[v1.7] | Calling `super/1` on GenServer callbacks | Implenting the behaviour explicitly without calling `super/1` (v1.0)
|
||||
[v1.7] | `Enum.chunk/2`[`/3/4`](`Enum.chunk/4`) | `Enum.chunk_every/2`[`/3/4`](`Enum.chunk_every/4`) (v1.5)
|
||||
[v1.7] | `not left in right` | [`left not in right`](`Kernel.in/2`) (v1.5)
|
||||
[v1.7] | `Enum.chunk/2,3,4` | `Enum.chunk_every/2` and [`Enum.chunk_every/3,4`](`Enum.chunk_every/4`) (v1.5)
|
||||
[v1.7] | Calling `super/1` in`GenServer` callbacks | Implenting the behaviour explicitly without calling `super/1` (v1.0)
|
||||
[v1.7] | [`not left in right`](`Kernel.in/2`) | [`left not in right`](`Kernel.in/2`) (v1.5)
|
||||
[v1.7] | `Registry.start_link/3` | `Registry.start_link/1` (v1.5)
|
||||
[v1.7] | `Stream.chunk/2`[`/3/4`](`Stream.chunk/4`) | `Stream.chunk_every/2`[`/3/4`](`Stream.chunk_every/4`) (v1.5)
|
||||
[v1.7] | `Stream.chunk/2,3,4` | `Stream.chunk_every/2` and [`Stream.chunk_every/3,4`](`Stream.chunk_every/4`) (v1.5)
|
||||
[v1.6] | `Enum.partition/2` | `Enum.split_with/2` (v1.4)
|
||||
[v1.6] | `Keyword.replace/3` | `Keyword.fetch/2` + `Keyword.put/3` (v1.0)
|
||||
[v1.6] | `Macro.unescape_tokens/1/2` | Use `Enum.map/2` to traverse over the arguments (v1.0)
|
||||
[v1.6] | `Module.add_doc/6` | `@doc` module attribute (v1.0)
|
||||
[v1.6] | `Macro.unescape_tokens/1,2` | Use `Enum.map/2` to traverse over the arguments (v1.0)
|
||||
[v1.6] | `Map.replace/3` | `Map.fetch/2` + `Map.put/3` (v1.0)
|
||||
[v1.6] | `Range.range?/1` | Pattern match on `_.._` (v1.0)
|
||||
[v1.6] | `Module.add_doc/6` | [`@doc`](`Module`) module attribute (v1.0)
|
||||
[v1.6] | `Range.range?/1` | Pattern match on [`_.._`](`Kernel.../2`) (v1.0)
|
||||
[v1.5] | `()` to mean `nil` | `nil` (v1.0)
|
||||
[v1.5] | `char_list/0` type | `t:charlist/0` type (v1.3)
|
||||
[v1.5] | `Atom.to_char_list/1` | `Atom.to_charlist/1` (v1.3)
|
||||
[v1.5] | `Enum.filter_map/3` | `Enum.filter/2` + `Enum.map/2` or [`for`](`Kernel.SpecialForms.for/1`) comprehensions (v1.0)
|
||||
[v1.5] | `Float.to_char_list/1` | `Float.to_charlist/1` (v1.3)
|
||||
[v1.5] | `GenEvent` module | `Supervisor` and `GenServer` (v1.0);<br/>[`GenStage`](https://hex.pm/packages/gen_stage) (v1.3);<br/>[`:gen_event`](http://www.erlang.org/doc/man/gen_event.html) (Erlang/OTP 17)
|
||||
[v1.5] | `Integer.to_char_list/1/2` | `Integer.to_charlist/1` and `Integer.to_charlist/2` (v1.3)
|
||||
[v1.5] | `<%=` in middle and end expressions in `EEx` | Use `<%` (`<%=` is allowed only in start expressions) (v1.0)
|
||||
[v1.5] | `:as_char_lists` value in `t:Inspect.Opts.t/0` type | `:as_charlists` value (v1.3)
|
||||
[v1.5] | `:char_lists` key in `t:Inspect.Opts.t/0` type | `:charlists` key (v1.3)
|
||||
[v1.5] | `Integer.to_char_list/1,2` | `Integer.to_charlist/1` and `Integer.to_charlist/2` (v1.3)
|
||||
[v1.5] | `Kernel.to_char_list/1` | `Kernel.to_charlist/1` (v1.3)
|
||||
[v1.5] | `List.Chars.to_char_list/1` | `List.Chars.to_charlist/1` (v1.3)
|
||||
[v1.5] | `@compile {:parse_transform, _}` in `Module` | *None*
|
||||
[v1.5] | `Stream.filter_map/3` | `Stream.filter/2` + `Stream.map/2` (v1.0)
|
||||
[v1.5] | `String.ljust/3` and `String.rjust/3` | Use `String.pad_leading/3` and `String.pad_trailing/3` with a binary padding (v1.3)
|
||||
[v1.5] | `String.strip/1` and `String.strip/2` | `String.trim/1` and `String.trim/2` (v1.3)
|
||||
[v1.5] | `String.lstrip/1` and `String.rstrip/1` | `String.trim_leading/1` and `String.trim_trailing/1` (v1.3)
|
||||
[v1.5] | `String.lstrip/2` and `String.rstrip/2` | Use `String.trim_leading/2` and `String.trim_trailing/2` with a binary as second argument (v1.3)
|
||||
[v1.5] | `String.strip/1` and `String.strip/2` | `String.trim/1` and `String.trim/2` (v1.3)
|
||||
[v1.5] | `String.to_char_list/1` | `String.to_charlist/1` (v1.3)
|
||||
[v1.5] | `()` to mean `nil` | `nil` (v1.0)
|
||||
[v1.5] | `char_list/0` type | `t:charlist/0` type (v1.3)
|
||||
[v1.5] | `:char_lists` key in `t:Inspect.Opts.t/0` type | `:charlists` key (v1.3)
|
||||
[v1.5] | `:as_char_lists` value in `t:Inspect.Opts.t/0` type | `:as_charlists` value (v1.3)
|
||||
[v1.5] | `@compile {:parse_transform, _}` in `Module` | *None*
|
||||
[v1.5] | EEx: `<%=` in middle and end expressions | Use `<%` (`<%=` is allowed only on start expressions) (v1.0)
|
||||
[v1.4] | [Anonymous functions](`Kernel.SpecialForms.fn/1`) with no expression after `->` | Use an expression or explicitly return `nil` (v1.0)
|
||||
[v1.4] | Support for making [private functions](`Kernel.defp/2`) overridable | Use [public functions](`Kernel.def/2`) (v1.0)
|
||||
[v1.4] | Variable used as function call | Use parentheses (v1.0)
|
||||
[v1.4] | `Access.key/1` | `Access.key/2` (v1.3)
|
||||
[v1.4] | `Behaviour` module | `@callback` module attribute (v1.0)
|
||||
[v1.4] | `Enum.uniq/2` | `Enum.uniq_by/2` (v1.2)
|
||||
@@ -125,30 +139,27 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.4] | `Float.to_string/2` | `:erlang.float_to_binary/2` (Erlang/OTP 17)
|
||||
[v1.4] | `HashDict` module | `Map` (v1.2)
|
||||
[v1.4] | `HashSet` module | `MapSet` (v1.1)
|
||||
[v1.4] | Multi-letter aliases in `OptionParser` | Use single-letter aliases (v1.0)
|
||||
[v1.4] | `Set` module | `MapSet` (v1.1)
|
||||
[v1.4] | `Stream.uniq/2` | `Stream.uniq_by/2` (v1.2)
|
||||
[v1.4] | `IEx.Helpers.import_file/2` | `IEx.Helpers.import_file_if_available/1` (v1.3)
|
||||
[v1.4] | `Mix.Utils.camelize/1` | `Macro.camelize/1` (v1.2)
|
||||
[v1.4] | `Mix.Utils.underscore/1` | `Macro.underscore/1` (v1.2)
|
||||
[v1.4] | Variable used as function call | Use parentheses (v1.0)
|
||||
[v1.4] | Anonymous functions with no expression after `->` | Use an expression or explicitly return `nil` (v1.0)
|
||||
[v1.4] | Support for making private functions overridable | Use public functions (v1.0)
|
||||
[v1.4] | Multi-letter aliases in `OptionParser` | Use single-letter aliases (v1.0)
|
||||
[v1.4] | `Set` module | `MapSet` (v1.1)
|
||||
[v1.4] | `Stream.uniq/2` | `Stream.uniq_by/2` (v1.2)
|
||||
[v1.3] | `\x{X*}` inside strings/sigils/charlists | `\uXXXX` or `\u{X*}` (v1.1)
|
||||
[v1.3] | `Dict` module | `Keyword` (v1.0) or `Map` (v1.2)
|
||||
[v1.3] | `:append_first` option in `Kernel.defdelegate/2` | Define the function explicitly (v1.0)
|
||||
[v1.3] | Map/dictionary as 2nd argument in `Enum.group_by/3` | `Enum.reduce/3` (v1.0)
|
||||
[v1.3] | `Keyword.size/1` | `Kernel.length/1` (v1.0)
|
||||
[v1.3] | `Map.size/1` | `Kernel.map_size/1` (v1.0)
|
||||
[v1.3] | `/r` option in `Regex` | `/U` (v1.1)
|
||||
[v1.3] | `Set` behaviour | `MapSet` data structure (v1.1)
|
||||
[v1.3] | `String.valid_character?/1` | `String.valid?/1` (v1.0)
|
||||
[v1.3] | `Task.find/2` | Use direct message matching (v1.0)
|
||||
[v1.3] | `:append_first` option in `Kernel.defdelegate/2` | Define the function explicitly (v1.0)
|
||||
[v1.3] | `/r` option in `Regex` | `/U` (v1.1)
|
||||
[v1.3] | `\x{X*}` inside strings/sigils/charlists | `\uXXXX` or `\u{X*}` (v1.1)
|
||||
[v1.3] | Map/dictionary as 2nd argument in `Enum.group_by/3` | `Enum.reduce/3` (v1.0)
|
||||
[v1.3] | Non-map as 2nd argument in `URI.decode_query/2` | Use a map (v1.0)
|
||||
[v1.2] | `Dict` behaviour | `MapSet` data structure (v1.1)
|
||||
[v1.1] | `?\xHEX` | `0xHEX` (v1.0)
|
||||
[v1.1] | `Access` protocol | `Access` behaviour (v1.1)
|
||||
[v1.1] | `as: true \| false` in `alias/2` and `require/2` | *None*
|
||||
[v1.1] | `?\xHEX` | `0xHEX` (v1.0)
|
||||
|
||||
[v1.1]: https://github.com/elixir-lang/elixir/blob/v1.1/CHANGELOG.md#4-deprecations
|
||||
[v1.2]: https://github.com/elixir-lang/elixir/blob/v1.2/CHANGELOG.md#changelog-for-elixir-v12
|
||||
@@ -158,4 +169,5 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.6]: https://github.com/elixir-lang/elixir/blob/v1.6/CHANGELOG.md#4-deprecations
|
||||
[v1.7]: https://github.com/elixir-lang/elixir/blob/v1.7/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.8]: https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.9]: https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.9]: https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.10]: https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md#4-hard-deprecations
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
# Guards
|
||||
|
||||
Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed.
|
||||
|
||||
Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere. It also allows the compiler to optimize the code related to guards efficiently.
|
||||
|
||||
## List of allowed expressions
|
||||
|
||||
You can find the built-in list of guards [in the `Kernel` module](Kernel.html#guards). Here is an overview:
|
||||
|
||||
* comparison operators ([`==`](`==/2`), [`!=`](`!=/2`), [`===`](`===/2`), [`!==`](`!==/2`),
|
||||
[`>`](`>/2`), [`>=`](`>=/2`), [`<`](`</2`), [`<=`](`<=/2`))
|
||||
* strictly boolean operators ([`and`](`and/2`), [`or`](`or/2`), [`not`](`not/1`)). Note [`&&`](`&&/2`), [`||`](`||/2`), and [`!`](`!/1`) sibling operators are **not allowed** as they're not *strictly* boolean - meaning they don't require arguments to be booleans
|
||||
* arithmetic unary and binary operators ([`+`](`+/1`), [`-`](`-/1`), [`+`](`+/2`), [`-`](`-/2`), [`*`](`*/2`), [`/`](`//2`))
|
||||
* [`in`](`in/2`) and [`not in`](`in/2`) operators (as long as the right-hand side is a list or a range)
|
||||
* "type-check" functions ([`is_list/1`](`is_list/1`), [`is_number/1`](`is_number/1`), etc.)
|
||||
* functions that work on built-in datatypes ([`abs/1`](`abs/1`), [`map_size/1`](`map_size/1`), etc.)
|
||||
|
||||
The module `Bitwise` also includes a handful of [Erlang bitwise operations as guards](Bitwise.html#guards).
|
||||
|
||||
Macros constructed out of any combination of the above guards are also valid guards - for example, `Integer.is_even/1`. For more information, see the "Defining custom guard expressions" section shown below.
|
||||
|
||||
## Why guards
|
||||
|
||||
Let's see an example of a guard used in a function clause:
|
||||
|
||||
```elixir
|
||||
def empty_map?(map) when map_size(map) == 0, do: true
|
||||
def empty_map?(map) when is_map(map), do: false
|
||||
```
|
||||
|
||||
Guards start with the `when` keyword, which is followed by a boolean expression (we will define the grammar of guards more formally later on).
|
||||
|
||||
Writing the `empty_map?/1` function by only using pattern matching would not be possible (as pattern matching on `%{}` would match *every* map, not empty maps).
|
||||
|
||||
## Where guards can be used
|
||||
|
||||
In the example above, we show how guards can be used in function clauses. There are several constructs that allow guards; for example:
|
||||
|
||||
* function clauses:
|
||||
|
||||
```elixir
|
||||
def foo(term) when is_integer(term), do: term
|
||||
def foo(term) when is_float(term), do: round(term)
|
||||
```
|
||||
|
||||
* [`case`](`case/2`) expressions:
|
||||
|
||||
```elixir
|
||||
case x do
|
||||
1 -> :one
|
||||
2 -> :two
|
||||
n when is_integer(n) and n > 2 -> :larger_than_two
|
||||
end
|
||||
```
|
||||
|
||||
* anonymous functions ([`fn`](`fn/1`)s):
|
||||
|
||||
```elixir
|
||||
larger_than_two? = fn
|
||||
n when is_integer(n) and n > 2 -> true
|
||||
n when is_integer(n) -> false
|
||||
end
|
||||
```
|
||||
|
||||
* custom guards can also be defined with `defguard/1` and `defguardp/1`.
|
||||
A custom guard is always defined based on existing guards.
|
||||
|
||||
Other constructs are [`for`](`for/1`), [`with`](`with/1`), [`try/rescue/catch/else`](`try/1`), and the `match?/2`.
|
||||
|
||||
## Failing guards
|
||||
|
||||
In guards, when functions would normally raise exceptions, they cause the guard to fail instead.
|
||||
For example, the `length/1` function only works with lists. If we use it with anything else, a runtime error is raised:
|
||||
|
||||
```elixir
|
||||
iex> length("hello")
|
||||
** (ArgumentError) argument error
|
||||
```
|
||||
|
||||
However, when used in guards, the corresponding clause simply fails to match:
|
||||
|
||||
```elixir
|
||||
iex> case "hello" do
|
||||
...> something when length(something) > 0 ->
|
||||
...> :length_worked
|
||||
...> _anything_else ->
|
||||
...> :length_failed
|
||||
...> end
|
||||
:length_failed
|
||||
```
|
||||
|
||||
In many cases, we can take advantage of this. In the code above, we used `length/1` to both check that the given thing is a list *and* check some properties of its length (instead of using `is_list(something) and length(something) > 0`).
|
||||
|
||||
## Defining custom guard expressions
|
||||
|
||||
As mentioned before, only the expressions listed in this page are allowed in guards. However, we can take advantage of macros to write custom guards that can simplify our programs or make them more domain-specific. At the end of the day, what matters is that the *output* of the macros (which is what will be compiled) boils down to a combinations of the allowed expressions.
|
||||
|
||||
Let's look at a quick case study: we want to check that a function argument is an even or odd integer. With pattern matching, this is impossible to do since there are infinite integers, and thus we can't pattern match on the single even/odd numbers. Let's focus on checking for even numbers since checking for odd ones is almost identical.
|
||||
|
||||
Such a guard would look like this:
|
||||
|
||||
```elixir
|
||||
def my_function(number) when is_integer(number) and rem(number, 2) == 0 do
|
||||
# do stuff
|
||||
end
|
||||
```
|
||||
|
||||
This would be repetitive to write every time we need this check, so, as mentioned at the beginning of this section, we can abstract this away using a macro. Remember that defining a function that performs this check wouldn't work because we can't use custom functions in guards. Use `defguard` and `defguardp` to create guard macros. Here's an example:
|
||||
|
||||
```elixir
|
||||
defmodule MyInteger do
|
||||
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
|
||||
end
|
||||
```
|
||||
|
||||
and then:
|
||||
|
||||
```elixir
|
||||
import MyInteger, only: [is_even: 1]
|
||||
|
||||
def my_function(number) when is_even(number) do
|
||||
# do stuff
|
||||
end
|
||||
```
|
||||
|
||||
While it's possible to create custom guards with macros, it's recommended to define them using `defguard` and `defguardp` which perform additional compile-time checks.
|
||||
|
||||
## Multiple guards in the same clause
|
||||
|
||||
There exists an additional way to simplify a chain of `or`s in guards: Elixir supports writing "multiple guards" in the same clause. This:
|
||||
|
||||
```elixir
|
||||
def foo(term) when is_integer(term) or is_float(term) or is_nil(term),
|
||||
do: :maybe_number
|
||||
def foo(_other),
|
||||
do: :something_else
|
||||
```
|
||||
|
||||
can be alternatively written as:
|
||||
|
||||
```elixir
|
||||
def foo(term)
|
||||
when is_integer(term)
|
||||
when is_float(term)
|
||||
when is_nil(term) do
|
||||
:maybe_number
|
||||
end
|
||||
|
||||
def foo(_other) do
|
||||
:something_else
|
||||
end
|
||||
```
|
||||
|
||||
If each guard expression always returns a boolean, the two forms are equivalent. However, recall that if any function call in a guard raises an exception, the entire guard fails. So this function will not detect empty tuples:
|
||||
|
||||
```elixir
|
||||
defmodule Check do
|
||||
# If given a tuple, map_size/1 will raise, and tuple_size/1 will not be evaluated
|
||||
def empty?(val) when map_size(val) == 0 or tuple_size(val) == 0, do: true
|
||||
def empty?(_val), do: false
|
||||
end
|
||||
|
||||
Check.empty?(%{}) #=> true
|
||||
Check.empty?({}) #=> false # true was expected!
|
||||
```
|
||||
|
||||
This could be corrected by ensuring that no exception is raised, either via type checks like `is_map(val) and map_size(val) == 0`, or by checking equality instead, like `val == %{}`.
|
||||
|
||||
It could also be corrected by using multiple guards, so that if an exception causes one guard to fail, the next one is evaluated.
|
||||
|
||||
```elixir
|
||||
defmodule Check do
|
||||
# If given a tuple, map_size/1 will raise, and the second guard will be evaluated
|
||||
def empty?(val)
|
||||
when map_size(val) == 0
|
||||
when tuple_size(val) == 0,
|
||||
do: true
|
||||
|
||||
def empty?(_val), do: false
|
||||
end
|
||||
|
||||
Check.empty?(%{}) #=> true
|
||||
Check.empty?({}) #=> true
|
||||
```
|
||||
@@ -144,6 +144,34 @@ The application environment should be reserved only for configurations that are
|
||||
|
||||
For all remaining scenarios, libraries should not force their users to use the application environment for configuration. If the user of a library believes that certain parameter should be configured globally, then they can wrap the library functionality with their own application environment configuration.
|
||||
|
||||
### Avoid compile-time application configuration
|
||||
|
||||
Assuming you need to use the application configuration and you cannot avoid it as explained in the previous section, you should also avoid compile-time application configuration. For example, instead of doing this:
|
||||
|
||||
```elixir
|
||||
@http_client Application.fetch_env!(:my_app, :http_client)
|
||||
|
||||
def request(path) do
|
||||
@http_client.request(path)
|
||||
end
|
||||
```
|
||||
|
||||
you should do this:
|
||||
|
||||
```elixir
|
||||
def request(path) do
|
||||
http_client().request(path)
|
||||
end
|
||||
|
||||
defp http_client() do
|
||||
Application.fetch_env!(:my_app, :http_client)
|
||||
end
|
||||
```
|
||||
|
||||
That's because by reading the application in the module body and storing it in a module attribute, we are effectively reading the configuration at compile-time, which may become an issue when configuring the system later.
|
||||
|
||||
If, for some reason, you must read the application environment at compile time, use `Application.compile_env/2`. Read [the "Compile-time environment" section of the Application docs](Application.html#module-compile-time-environment) for more information.
|
||||
|
||||
### Avoid `use` when an `import` is enough
|
||||
|
||||
A library should not provide `use MyLib` functionality if all `use MyLib` does is to `import`/`alias` the module itself. For example, this is an anti-pattern:
|
||||
@@ -202,7 +230,7 @@ When you absolutely have to use a macro, make sure that a macro is not the only
|
||||
|
||||
A developer must never use a process for code organization purposes. A process must be used to model runtime properties such as:
|
||||
|
||||
* Mutable state and access to shared resources (such as ETS, files, etc.)
|
||||
* Mutable state and access to shared resources (such as ETS, files, and others)
|
||||
* Concurrency and distribution
|
||||
* Initialization, shutdown and restart logic (as seen in supervisors)
|
||||
* System messages such as timer messages and monitoring events
|
||||
|
||||
@@ -4,7 +4,7 @@ This document covers some naming conventions in Elixir code, from casing to punc
|
||||
|
||||
## Casing
|
||||
|
||||
Elixir developers must use `snake_case` when defining variables, function names, module attributes, etc.:
|
||||
Elixir developers must use `snake_case` when defining variables, function names, module attributes, and the like:
|
||||
|
||||
some_map = %{this_is_a_key: "and a value"}
|
||||
is_map(some_map)
|
||||
@@ -65,7 +65,7 @@ The version without `!` is preferred when you want to handle different outcomes
|
||||
{:error, reason} -> # handle the error caused by `reason`
|
||||
end
|
||||
|
||||
However, if you expect the outcome to always to be successful (e.g. if you expect the file always to exist), the bang variation can be more convenient and will raise a more helpful error message (than a failed pattern match) on failure.
|
||||
However, if you expect the outcome to always to be successful (for instance, if you expect the file always to exist), the bang variation can be more convenient and will raise a more helpful error message (than a failed pattern match) on failure.
|
||||
|
||||
More examples of paired functions: `Base.decode16/2` and `Base.decode16!/2`, `File.cwd/0` and `File.cwd!/0`.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user