Compare commits

...
Author SHA1 Message Date
José Valim 545dddf138 Release v1.20.2 2026-06-23 14:02:47 +02:00
Łukasz Samson bc30e7d696 Include bitstring in domain keys (#15516)
Fixes #15515
2026-06-23 10:44:27 +02:00
José Valim 73130012bb Better organize tests 2026-06-21 21:36:40 +02:00
Łukasz Samson f55abe5ff9 Correctly render list with negative leaves (#15514)
BDD leaves represent non empty lists

Fixes #15513.
2026-06-21 21:34:37 +02:00
Łukasz Samson 394ab4bead Return error from compatible_intersection with none (#15512)
Match fast path behaviour with `compatible?/2`
Fixes #15511
2026-06-21 21:33:45 +02:00
Łukasz Samson f491a7eee4 Report empty_list: true on dynamic() in list_of (#15506)
Fixes #15505
2026-06-19 21:43:35 +02:00
José Valim a64284921e Fix optimized map difference with open key 2026-06-19 21:42:31 +02:00
Łukasz Samson 6bef18d523 Preserve optional static part on difference (#15503)
Fixes #15502
2026-06-18 09:22:29 +02:00
José Valim 8516737ea0 Avoid unecessary umbrella recompilation when a path dependency's manifest is newer but unchanged, closes #15454 2026-06-17 15:56:22 +02:00
José Valim c52eb2a323 opt_union -> union 2026-06-17 08:56:07 +02:00
Łukasz Samson 527fb26bcd Fix definition of term_or_dynamic_optional type (#15498)
Fixes #15497
2026-06-17 08:47:50 +02:00
José Valim 5c5909c2a1 Preserve empty list in dynamic list type constructor
Closes #15488.
2026-06-15 20:30:22 +02:00
José Valim 3c7ee9ff20 Handle empty function type in fun_apply
Closes #15486.
2026-06-15 18:48:29 +02:00
José Valim 51dce31a54 Preserve file meta from each clause in type system warnings 2026-06-15 16:03:50 +02:00
José Valim 0dd4cb4e0b Update CHANGELOG 2026-06-15 13:58:58 +02:00
Łukasz Samson 4d1de1d971 Fix missed optimization of Kernel.put_elem/3 (#15480)
?reorder added in 87582af54 used wrong arity and wrong erlang call
2026-06-15 12:49:18 +02:00
Jean Klingler b098c0406c Fix bin comprehensions when option is used (#15473)
* Fix bin comprehensions - static size

* Fix bin comprehensions - pinned var size

* Fix bin comprehensions - operations

* Add tests for nested generators
2026-06-15 17:34:06 +09:00
José Valim 1ba39f3501 Raise better error messages when expected structs disappear, closes #15472 2026-06-14 13:46:51 +02:00
Jean Klingler 0f9072a1d4 Cleaner error message for unquote when quote is used in a pattern (#15469) 2026-06-13 21:16:40 +09:00
José Valim 6dcc4cd2dc Simplify precision tracking in bitstrings 2026-06-13 12:11:32 +02:00
José Valim 40b6f012fe Fix env outdated deps compilation
We should only avoid cleaning for path dependencies.
Fetch dependencies should go through the previous process.

Closes #15467.
2026-06-13 12:11:31 +02:00
José Valim 68267627dc Track reused variables in bitstring patterns 2026-06-13 11:17:37 +02:00
José Valim 57766dd29b Use cyclic instead of recursive, add more examples, closes #15462 2026-06-12 21:01:29 +02:00
José Valim 1938d8a957 Ignore additional callbacks in protocol implementation, closes #15456 2026-06-12 21:01:28 +02:00
José Valim 3bebe1599a Log times to type check modules when profiling 2026-06-12 13:57:20 +02:00
Łukasz Samson 2beac77406 Add missing required key to __info__(:struct) type (#15453) 2026-06-10 16:41:24 +02:00
José Valim 85a23a1244 Document false positives, closes #15451 2026-06-10 09:27:42 +02:00
José Valim 97c1e1ec0c Release v1.20.1 2026-06-09 15:36:40 +02:00
José Valim 64e6707233 Limit version numbers to 14 bytes
This avoids parsing too large integers.

CVE-2026-49762
GHSA-w2h8-8x3g-278p
2026-06-09 15:30:13 +02:00
José Valim 657a7b394a Add docs about URI encoding/to_string 2026-06-09 15:30:08 +02:00
José Valim ac76c108ce Validate paths and files when extracting archives 2026-06-09 15:30:03 +02:00
José Valim 2e629aa414 Clarify security considerations and disclaimers to archive/escript install 2026-06-09 15:30:00 +02:00
José Valim 2f2fc9a7f9 Cap width in strftime to 1024 characters 2026-06-09 15:29:55 +02:00
Łukasz Samson bb12270c6b Add capture_arg to Code.Fragment.cursor_context spec (#15441) 2026-06-09 15:29:49 +02:00
Aaron Tinio 6d2e8241c9 Honor --no-compile in mix format (#15443) 2026-06-07 21:34:50 +02:00
José Valim a371208104 Tag lists member as returning boolean 2026-06-07 11:07:18 +02:00
José Valim 3a366c2f0f Deal with empty canonical on doc generation 2026-06-05 15:08:07 +02:00
José Valim d33ea6e963 require_file releases the file if compilation fails 2026-06-04 19:42:49 +02:00
José Valim 853b7cd719 Release v1.20.0 2026-06-03 19:38:11 +02:00
José Valim 0bc04768bf Squash CHANGELOG 2026-06-02 18:45:40 +02:00
Jean Klingler c354a77946 Never suggest back invalid characters in tokenizer hint (#15421) 2026-05-27 10:23:43 +02:00
Jean Klingler 766ece7e3f Inline in empty list in erlang pass (#15413) 2026-05-24 09:06:11 +09:00
José Valim 8bc9a2ed15 Avoid false positives on cascading warnings
Only emit warnings for missing exports in defined
modules to avoid false positives with modules from
the current project which may not have been defined
yet.
2026-05-22 10:41:54 +02:00
José Valim 7150d493ad Improve tracking of g df within the type system 2026-05-22 10:14:46 +02:00
José Valim 7a9ea30d9c Revert expansion of or/and in guards
This partially reverts aad63173 as it caused
massive AST to generate when expanding guards
with `expr in [...]` checks in them, as each
`or` became a full-blown case expression.

Alternative approaches will be explored in
future releases.
2026-05-22 09:51:29 +02:00
José Valim 3a038d1763 Wrap quote in a function that will implement recursive types
Closes #15410.
2026-05-22 09:51:29 +02:00
José Valim e5e66831ac Avoid redundant emptiness checks on list/tuple emptiness (#15412) 2026-05-22 09:51:29 +02:00
José Valim 285c15f1ab Release v1.20.0-rc.6 2026-05-21 13:45:11 +02:00
José Valim 909903962d Branch out v1.20 2026-05-21 13:34:14 +02:00
José Valim 1f19a053cf Improve coverage of large heads with more complex code 2026-05-21 13:32:19 +02:00
José Valim af6d6b2cbe Mark stale fetchable deps for compile before convergence
A fetched git dependency could report {:ok, vsn} from its
stale .app before Mix checked the stored SCM manifest lock.
If the same dependency then appeared again through a stricter
transitive requirement, the converger would mark it as
:divergedreq instead of :compile.

Validate the SCM manifest while loading the dependency so
fetchable deps with a stale stored lock are marked for
compilation before convergence reuses their status.
Add regression tests for the transitive git case and for
the direct converger case.

Closes #15406.
2026-05-21 12:54:55 +02:00
José Valim 4dd6c3cf6a Revert "Allow overriding specific dependencies in override (#15193)"
Hex uses the flag in a way that's not visibile to Mix,
so we have false positives. Closes #15407.

This reverts commit bc7af21c02.
2026-05-21 10:17:05 +02:00
José Valim 862ff780b8 Report clearer errors for violations on !, closes #15404 2026-05-20 16:09:24 +02:00
José Valim edc61ce9c0 Remove hardacoded line 2026-05-20 14:32:06 +02:00
José Valim d1e91dee7f Do not propagate variable refinement during inference 2026-05-20 13:39:33 +02:00
José Valim 7e7b4a8f4e Indent lists 2026-05-20 13:35:41 +02:00
José Valim f9573159ab Clarify cases optimization apply 2026-05-20 12:54:03 +02:00
José Valim 3d4bffe097 Remove duplicate nodes in BDD when performing unions
Our inference algorithm deduce types for each clause,
subtracting previous clauses. For a function with three
clauses, the type of each clause is roughtly:

    c1
    c2 - c1
    c3 - c2 - c1

To compute the domain, we union these clauses, leading
to large types that mix unions and differences. By removing
repeated nodes, the union of the entries above become:

    c1 or c2 or c3
2026-05-20 12:41:24 +02:00
José Valim 694930ec25 Add some initial resources to types page 2026-05-20 09:55:20 +02:00
José Valim 4725e1100c Update CHANGELOG 2026-05-19 18:59:36 +02:00
José Valim 76fbc0ccbc Do not discard variables in cond
When traversing the case for the reverse arrow, if there are
no changes, the case-expr are not re-executed, which meant
variables would not be redefined.

Closes #15402.
Closes #15403.
2026-05-19 13:15:58 +02:00
José Valim 28734f03d0 Clarify aliases is part of the next chapter, closes #15401 2026-05-18 20:50:15 +02:00
José Valim 0dd4399293 Simplify literal differences to avoid repacking 2026-05-17 15:57:31 +02:00
José Valim 0aa8c1fc2c Fix optimization in map leaf difference 2026-05-17 15:57:31 +02:00
Jean Klingler 315279701a Fix dialyzer opaqueness violation with MapSet (#15399) 2026-05-17 19:45:56 +09:00
José Valim 57b1685cac Collect diagnostics across clauses in rescue, closes #15307 2026-05-16 18:12:45 +02:00
José Valim 6962dc2d91 Deprecate xref: [exclude: ...] 2026-05-16 13:24:12 +02:00
Evadne Wu ca13b750ce Fix parse_error input spec (#15393) 2026-05-16 13:20:23 +02:00
José Valim c3e1aa706d Infer signatures across application boundaries, closes #15360 2026-05-16 13:14:06 +02:00
Evadne Wu b58e843528 Scope ExUnit diff width tests (#15396) 2026-05-16 01:27:33 +02:00
Łukasz Samson 0418986004 Fix Mix.Task.requirements/1 spec (#15394) 2026-05-15 21:45:22 +02:00
Evadne Wu 210f6fc3ac Clarify File.cp_r directory permission docs (#15392) 2026-05-15 19:27:24 +02:00
Jonatan Kłosko 536e53b173 Respect --raise when mix test --warnings-as-errors passes with warnings (#15390) 2026-05-15 10:30:28 +02:00
José Valim 64d381bd05 Avoid warnings when editor is not set 2026-05-14 21:55:35 +02:00
José Valim 87582af546 Preserve evaluation order when rewriting (#15389)
This was done by moving the rewriting which change evaluation order to the Erlang pass,
otherwise would be hard to rewrite back in the type checker reports.

Closes #15310.
2026-05-14 21:16:55 +02:00
Guillaume Duboc 63708d313c Type system support for put_elem/3 (#15388) 2026-05-14 20:45:33 +02:00
Guillaume Duboc e7a5a667bf Make tuple_insert_at preserve static part of a type (#15387) 2026-05-14 20:44:54 +02:00
Guillaume Duboc 367e38cbe6 Fix if_set dropping static part of type (#15379) 2026-05-14 20:44:22 +02:00
José Valim cb4020e692 Simplify handling of negative indexes 2026-05-14 18:25:58 +02:00
Łukasz Samson 909b099d44 Fix dynamic-value error wrapping in tuple_insert_at/3 (#15382)
Propagate `badtuple` and `badindex` error from `tuple_insert_at_checked` to the caller instead of wrapping it in `dynamic`
2026-05-14 16:23:46 +02:00
José Valim 7495a03535 Revert "Improve Keyword performance (#15378)" (#15386)
This reverts commit d50b0321ab.
2026-05-14 16:18:50 +02:00
José Valim 5509b95437 Group clauses by return, closes #15371 2026-05-14 15:38:47 +02:00
Łukasz Samson 0792999f63 Fix invalid error message on tuple operations with negative index (#15380) 2026-05-14 15:38:28 +02:00
Peter Ullrich d50b0321ab Improve Keyword performance (#15378) 2026-05-14 15:23:00 +02:00
José Valim 41332ac3d6 Silence warnings on charlist formatting 2026-05-14 14:03:32 +02:00
José Valim 46f7b37916 Ignore eval failure warnings from Erlang
Closes #15372.
2026-05-14 13:59:28 +02:00
José Valim 6b114ee7c1 Avoid false positives when comparing structs with dynamic
Closes #15370
2026-05-14 13:53:25 +02:00
Guillaume Duboc 71148aa953 Tuple operations performance boost + fix logic bug (#15376) 2026-05-14 12:44:18 +02:00
José Valim b35e210321 Properly format or/and in type errors
See #15369.
2026-05-14 12:43:28 +02:00
Cohen Carlisle 56336feb2f Mirror Map.filter/2 perf note in Map.reject/2 (#15373) 2026-05-13 23:23:03 +02:00
José Valim bf47be54bc Add a note about searching existing isues 2026-05-13 18:57:19 +02:00
José Valim aad63173b8 Clarify semantics of defguard, closes #15284 2026-05-13 18:53:13 +02:00
José Valim 6050b8400a Release v1.20.0-rc.5 2026-05-13 16:15:18 +02:00
José Valim 42a005e4df Bump CI to OTP 29 (#15367) 2026-05-13 14:52:22 +02:00
José Valim c2d39715ff Update CHANGELOG 2026-05-13 11:45:50 +02:00
Guillaume Duboc 472a49d84a Preserve static parts in dynamic container descriptors (#15363) 2026-05-13 11:45:23 +02:00
dependabot[bot] a631da4fb0 Bump github/codeql-action from 4.35.2 to 4.35.3 (#15365)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.2 to 4.35.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...e46ed2cbd01164d986452f91f178727624ae40d7)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 10:37:01 +02:00
dependabot[bot] 7f0670e574 Bump DavidAnson/markdownlint-cli2-action from 23.1.0 to 23.2.0 (#15364)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 23.1.0 to 23.2.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/6b51ade7a9e4a75a7ad929842dd298a3804ebe8b...ded1f9488f68a970bc66ea5619e13e9b52e601cd)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 23.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 10:36:13 +02:00
José Valim 8bdd041122 Add missing exclamation mark 2026-05-13 10:35:57 +02:00
José Valim 8f264f3ced Include you must disclose the use of AI 2026-05-13 10:31:32 +02:00
José Valim 0eabf48524 Refine types from case on inference 2026-05-12 20:15:16 +02:00
Guillaume Duboc b0db1a26e1 Performance boost on map operations + bugfix (#15362)
Add the two important optimization paths for negative-map projection: value-side projection for map fetch/get and shape-side projection for map put/update.

Keep regression coverage for projected negative maps.
2026-05-12 18:50:17 +02:00
José Valim 35eeab3511 Fix regression on default inference, closes #15359 2026-05-12 14:59:53 +02:00
José Valim aae39c87dc Fix warnings 2026-05-12 14:49:36 +02:00
José Valim 8a2c815c85 Propagate hd/tl checks across clauses, closes #15358 2026-05-12 14:06:08 +02:00
José Valim 0b9b5b962b Improve type system docs 2026-05-12 10:58:11 +02:00
José Valim 41353c6cf8 Infer precise types for default arguments 2026-05-12 10:40:25 +02:00
José Valim 7e49585d26 Use inlined functions to reduce compiled module size 2026-05-11 23:36:21 +02:00
Peter Ullrich 7081e7223e Add SWAR versions of Base validations (#15357) 2026-05-11 23:29:52 +02:00
José Valim 3e3ce13c1d Deal with raising clauses in cond 2026-05-11 12:45:42 +02:00
José Valim 4816d3773a Perform occurrence typing in cond 2026-05-11 11:51:03 +02:00
José Valim 5b8b8e3589 Reintroduce escaped trailing newlines in heredocs in Macro.to_string/2 (#15355)
Closes #15354.
2026-05-11 10:18:49 +02:00
Cohen Carlisle aafc248179 fix ===/2 arity in docs (#15353) 2026-05-10 09:34:03 +09:00
Łukasz Samson b8723fea1e Add missing spec for documented deps property in Mix.Dep (#15351) 2026-05-08 22:42:20 +02:00
José Valim 9412f483d7 Simplify logger test 2026-05-08 22:31:09 +02:00
Łukasz Samson 8a35ffa811 Reject negative Duration in to_timeout (#15352)
Previously a negative timeout value violating the spec was returned
2026-05-08 22:28:48 +02:00
Łukasz Samson bb21320928 Persist log level to app env in Logger.configure/1 (#15348)
Fixes #15347
2026-05-08 11:02:04 +02:00
Łukasz Samson a5435a6ead Reject invalid config shapes in Config.validate!/2 (#15349)
Previous code used `Enum.all?` and silently skipped invalid pairs
2026-05-08 11:01:32 +02:00
José Valim 35497755b5 Use non_executable_binary_to_term on loopback pubsub 2026-05-06 23:42:55 +02:00
Łukasz Samson c3f7e3473e Raise in Stream.cycle when enumerable reduce call yields no elements (#15344) 2026-05-06 12:22:53 +02:00
Łukasz Samson 68c413f76c Improve ambiguous import error message (#15335) 2026-05-06 12:22:34 +02:00
Łukasz Samson 16f5174580 Add missing String.count/2 clause for empty pattern list (#15342) 2026-05-06 09:26:19 +02:00
José Valim 1254dae84f Avoid checking date validity twice 2026-05-06 08:28:52 +02:00
dependabot[bot] 651bdb485d Bump DavidAnson/markdownlint-cli2-action from 23.0.0 to 23.1.0 (#15345)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 23.0.0 to 23.1.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/ce4853d43830c74c1753b39f3cf40f71c2031eb9...6b51ade7a9e4a75a7ad929842dd298a3804ebe8b)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 23.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 08:07:22 +02:00
José Valim 46ebf7cc83 Include additional AI usage guidelines 2026-05-04 19:35:58 +02:00
dependabot[bot] abf9ba1ae2 Bump github/codeql-action from 4.35.1 to 4.35.2 (#15321)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.1 to 4.35.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...95e58e9a2cdfd71adc6e0353d5c52f41a045d225)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 18:56:28 +02:00
José Valim 46811eccc9 Optimize difference of two equal bdds 2026-05-04 14:15:40 +02:00
José Valim 9199e5595c Ensure consistent ordering for closed elements 2026-05-04 13:01:51 +02:00
José Valim b8a29e0c1a Use :erlang.phash2 with tuples as it is much faster 2026-05-04 12:32:24 +02:00
José Valim ab5b33d8ac Avoid allocating tuples on bdd_map/bdd_reduce 2026-05-04 12:19:31 +02:00
Guillaume Duboc c376c0feb6 Hash-cons BDD descriptors (#15332) 2026-05-04 11:55:53 +02:00
Guillaume Duboc 5858e7c470 Fix wrong intersection with union of dynamic() (#15340) 2026-05-04 10:20:51 +02:00
Guillaume Duboc 2fd350672b Fix truthiness implementation (#15339) 2026-05-04 10:18:58 +02:00
José Valim 002e382794 Raise when loading struct info 2026-05-04 10:09:24 +02:00
José Valim 46841a22e4 Test we consider external variables as not precise, fix CI 2026-05-04 09:52:24 +02:00
José Valim bdf8e4481d Refine reverse arrows when only one clause remains 2026-05-04 01:24:42 +02:00
José Valim 3120d4539b Type checked struct operations must not deadlock the compiler 2026-05-04 01:24:42 +02:00
José Valim facde14525 Inline struct loading 2026-05-04 01:24:42 +02:00
José Valim 40e930607a Use definition instead of origin in meta 2026-05-04 01:24:42 +02:00
Peter Ullrich 03c123a149 Stop iterating over list when slice range ends (#15336) 2026-05-02 23:36:18 +02:00
Guillaume Duboc 81975a2de1 Fix descr edge cases (#15338) 2026-05-02 23:15:51 +02:00
Peter Ullrich 133dfa8d46 Provide multiple validate clauses for base16 (#15337)
It is consistent with the other implementations.
2026-05-02 22:56:52 +02:00
Łukasz Samson 39d646f459 Include sigils option in invalid import error message (#15334) 2026-05-01 23:04:20 +02:00
Łukasz Samson ad262dc8e4 Fix Kernel.ParallelCompiler.compile_opts each_cycle spec (#15333)
The callback invocation and default are both 0-arity.
2026-05-01 22:51:08 +02:00
Peter Ullrich a0e1604fe6 In EEX Compiler, flatten expr list only once, not on every iteration (#15331) 2026-05-01 18:13:52 +02:00
Peter Ullrich 11f7d8f84a Refactor Float.round/2 algorithm for better performance (#15329)
The previous implementation noted the trade-off:

```elixir
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
```

It computed `m * power_of_5(count)` where `count` could grow to ~104, producing 250-bit bignums for a single division. The cost grew with both float magnitude and target precision.

### Benchmark results

11 different float workloads, measured with Benchee on Apple M2. Reported times are the median μs of one iteration over all 11 workloads.

precision | current | cox (this PR) | native_double | cox vs current | native_double vs current | native_double vs cox |
|---|---|---|---|---|---|---|
| 0 | 0.25 μs | 0.25 μs | 0.25 μs | 1.1× | 1.1× | ~tied |
| 1 | 2.42 μs | 0.33 μs | 0.33 μs | **7.1×** | 7.0× | ~tied |
| 2 | 2.67 μs | 0.63 μs | 0.33 μs | **3.8×** | 7.7× | 2.0× |
| 3 | 4.29 μs | 0.63 μs | 0.33 μs | **5.8×** | 11.9× | 2.1× |
| 5 | 5.50 μs | 0.67 μs | 0.33 μs | **6.8×** | 15.6× | 2.3× |
| 8 | 7.21 μs | 0.96 μs | 0.38 μs | **7.5×** | 19.5× | 2.6× |
| 12 | 7.75 μs | 0.96 μs | 0.38 μs | **8.2×** | 20.3× | 2.5× |
| 15 | 8.58 μs | 1.58 μs | 0.38 μs | **5.3×** | 22.8× | 4.3× |

Median μs per iteration over the 11-float mixed workload (per-call median ≈ value / 11).

- **`current`** is the bignum-based implementation we're replacing
- **`cox`** is this PR
- **`native_double`** is included as a "what does this look like in pure native float arithmetic" reference (`:erlang.round(f * pow) / pow`) - it's 2-4× faster than Cox but incorrect on tie inputs because `5.5675 * 1000` rounds to `5567.5` in IEEE due to double-rounding (multiplication round + integer round), then bumps to `5568`. It's why we can't simply use the obvious approach. Cox preserves exact semantics by working on the float's true binary representation.

#### Floats benchmarked

```
floats = [
  {"small_pi",       3.141592653589793},   # typical small value
  {"common_money",   5.5675},               # tie-boundary case
  {"large_e10",      1.2345e10},            # large magnitude
  {"tiny_e_neg8",    1.2345e-8},            # very small magnitude
  {"near_zero",      0.00001},              # close to zero
  {"negative",       -123.456789},          # negative sign
  {"integer_valued", 12.0},                 # exactly representable integer
  {"tie_half",       12.5},                 # half-tie
  {"tie_5675",       5.5675},               # documented tie example
  {"large_e30",      1.234e30},             # very large
  {"max_finite_ish", 1.7e308}               # near max double
]
```
2026-05-01 12:59:07 +02:00
Łukasz Samson f4e1b34617 Fix incorrect guard type refinement with length/map_size and != operator (#15327)
Previously `empty_list()` and `empty_map` were returned for `length(x) != n` and `map_size(x) != n` when n > 0. Now the types are widened to `list()` and `open_map()`
2026-04-30 18:30:07 +02:00
José Valim 55a3899e75 Add more tests around precision 2026-04-29 22:10:46 +02:00
Łukasz Samson ada2817d72 Fix invalid type inference on size compare in guards (#15324) 2026-04-29 22:06:23 +02:00
Guillaume Duboc 89e0106afc Fix map union optimization for open maps (#15322) 2026-04-29 08:48:16 +02:00
Łukasz Samson e39a1ca796 Consistently return path as binary in relative_to_cwd (#15316)
Previously if path parameter was passed in as chardata it was returned as is on `:file.get_cwd` failure
All `relative_to` and `join` code paths normalize to binary
2026-04-28 22:51:09 +02:00
Łukasz Samson 007efde2d5 Fix reentrancy of Code.eval_* (#15306)
Keep env and dbg callbacks on stacks in process dict

Closes #15303
2026-04-28 19:38:28 +02:00
Guillaume Duboc eb8bbc9c29 Fix map difference union optimization (#15319) 2026-04-28 19:12:34 +02:00
José Valim c5332235ab Perform all side-effect in order in in/2 2026-04-27 12:25:49 +02:00
José Valim e5d8b00e5b Remove maps:put/3 inlining, as it is done by the Erlang compiler, closes #15313 2026-04-27 12:03:24 +02:00
José Valim 2ba06cc40b Make preserving dir permissions opt-in, closes #15308 (#15312) 2026-04-26 19:08:23 +02:00
José Valim 68aae43703 Revert "Add each_directory to File.rm_rf, closes #15308 (#15311)"
This reverts commit d02e61d75e.

It did not address the original root problem.
2026-04-26 18:02:46 +02:00
Rafał Studnicki 2019de2d82 Switch {:duplicate, :key} key_ets to ordered_set with composite keys (#15304)
Switch {:duplicate, :key} key_ets to ordered_set

Change init_key_ets to create an ordered_set table for {:duplicate, :key}
and add ordered boolean to Partition state.
2026-04-26 11:12:04 +02:00
José Valim d02e61d75e Add each_directory to File.rm_rf, closes #15308 (#15311) 2026-04-26 10:39:46 +02:00
dependabot[bot] f2f2da9b40 Bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3 (#15300)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.2 to 0.5.3.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/71321a20a9ded102f6e9ce5718a2fcec2c4f70d8...b1d7e1fb5de872772f31590499237e7cce841e8e)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:37:48 +02:00
dependabot[bot] 13d412864c Bump actions/upload-artifact from 7.0.0 to 7.0.1 (#15299)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 09:34:15 +02:00
Łukasz Samson 76d3fbad88 Disallow raw CR line ending in strings, comments and after ? (#15307) 2026-04-26 09:32:12 +02:00
José Valim b8a9dc60a5 Explicitly raise instead of calling assert(false) 2026-04-25 17:47:25 +02:00
José Valim 870d28fe64 Fix regression where we inferred the wrong type in a case branch that raises 2026-04-25 16:55:40 +02:00
José Valim 2d6e61c4f5 Compute negated types from precise none() branches 2026-04-22 22:11:26 +02:00
Łukasz Samson 81dc36568b Enrich EEx.SyntaxError with error metadata (#15297)
Fixes #15296
2026-04-22 16:58:24 +02:00
Łukasz Samson 99e0fe50a3 Fix Macro.Env.define_require/4 spec (#15302)
It may error when called with invalid `as` option
2026-04-22 15:34:15 +02:00
Łukasz Samson 8a4915008d Fix Macro.expand_literals/3 spec (#15301) 2026-04-22 15:32:40 +02:00
José Valim 0fe5b13320 Perform reverse arrows on with even when not precise 2026-04-22 12:18:03 +02:00
José Valim 808075d87a Implement reverse arrows for with 2026-04-21 20:17:08 +02:00
Łukasz Samson c14f3c7acb Fix arity of Logger.configure in warning message (#15298) 2026-04-21 22:04:04 +09:00
Łukasz Samson 5e7190c39f Fix static_atoms_encoder docs and spec (#15294)
Fixes #15293
2026-04-21 13:42:33 +02:00
Łukasz Samson 4ad7497d0e Consistently validate column option passed to parser (#15295)
Apply the same guard `line` option uses
2026-04-21 13:41:04 +02:00
Dinesh Anbazhagan 25cbbfe5d2 Fix errors in getting-started and mix-and-otp guides (#15289) 2026-04-20 20:34:28 +02:00
José Valim a5e19ad385 Support --output in mix deps.tree and mix app.tree, closes #15288 2026-04-20 08:17:12 +02:00
José Valim 983e033863 Do not touch sources when external resources change
Instead we store the changed files in the checkpoint.

This also address an issue where Elixir would say
"mtime was set in the future" when compiled too quickly.

See 7d521de. Closes #15277.
2026-04-18 22:26:29 +02:00
José Valim 890d9bbf00 Type checking improvements
* Ensure we do not accumulate previous information across clauses
* Avoid union(a, not a) when building domains
* Ensure union with an open map/tuples returns the open map/tuple
* Remove deep traversal on bdd leaf intersection which was slowing
  compilation of projects like open_api_spex

Closes #15285.
2026-04-18 18:13:48 +02:00
José Valim 20fef8cd1c Change directory permissions after writing, closes #15286 2026-04-18 09:11:57 +02:00
Guillaume Duboc 9231515db8 Bdd negation and difference optimizations (#15287) 2026-04-17 17:24:44 +02:00
José Valim d5e032efd1 Increase inspect limit to help print deeply nested data structures 2026-04-17 08:50:19 +02:00
Łukasz Samson 5e44c78fff Fix compiler crash on invalid nested bitstring (#15280) 2026-04-16 15:32:48 +02:00
Lukasz Samson 4d56bdd898 Do not reset quote level on calls in has_unquotes/2
The bug was introduced in 146fb4e3ff

Signed-off-by: José Valim <jose.valim@gmail.com>
2026-04-16 13:49:37 +02:00
José Valim 2e9981d98c Raise when splicing cons without head, closes #15282 2026-04-16 13:48:08 +02:00
Wojtek Mach ed478e816b Don't crash when inspecting OTP 29 native records (#15278) 2026-04-16 13:32:57 +02:00
José Valim 370e7db14a Ensure compiler modules are returned on failures, closes #15279 2026-04-16 08:26:16 +02:00
José Valim d67cb9634b Add performance regression test for open and closed maps 2026-04-15 21:07:24 +02:00
José Valim a37db60f78 Move :open check inside leaf_bdd_intersection 2026-04-15 19:38:04 +02:00
José Valim 116f8f472f Also optimize bottoms in leaf bdd intersections 2026-04-15 17:11:56 +02:00
José Valim 9c1bd76b26 Remove expected intersection as now track underscores 2026-04-15 17:11:56 +02:00
José Valim 3cb0b51836 Add AI contributions guidelines to CONTRIBUTING.md
Inspired by the recent Linux guidelines.
2026-04-15 14:08:30 +02:00
Gary Rennie 5b97e9c132 Show remaining runs when using --repeat-until-failure (#15270)
The --repeat-until-failure can be very useful for finding flaky tests,
however sometimes it is hard to gauge how long it has left to run before
it can be considered successful.

This commit adds a small output showing the number of remaining runs
when the `--repeat-until-failure` flag is used.

It looks like:

```
Running ExUnit with seed: 382462, max_cases: 32, remaining_runs: 3
Excluding tags: [windows: true]

.......
Finished in 0.02 seconds (0.00s async, 0.02s sync)

Result: 7 passed
Running ExUnit with seed: 422319, max_cases: 32, remaining_runs: 2
Excluding tags: [windows: true]

.......
Finished in 0.00 seconds (0.00s async, 0.00s sync)
```
2026-04-15 09:46:55 +02:00
José Valim 8ba8ad709f Document halt responsibilities for resources
Closes #15265.
Closes #15266.
2026-04-15 09:09:58 +02:00
José Valim 37dc125f2a Check both expand and dispatch are not in stacktrace 2026-04-15 09:01:42 +02:00
José Valim 9fb8fb1216 Add comments justifying use of subtyping, closes #15273 2026-04-15 08:59:22 +02:00
Łukasz Samson 5a1fa351dc Fix invalid module reference in prune_stacktrace (#15272)
Closes #15271
2026-04-15 08:59:08 +02:00
Jean Klingler 20cd669a0d Optimize Keyword.validate/2 to avoid building new list (#15269) 2026-04-15 07:06:59 +09:00
José ValimandGuillaume Duboc 03b45e521d Remove redundant line empty when we eliminate negations (#15196)
Co-authored-by: Guillaume Duboc <guilduboc@gmail.com>
2026-04-13 21:41:32 +02:00
José Valim bcfefa18ed Simplify Enum.dedup for lists 2026-04-13 21:37:57 +02:00
Łukasz Samson b7a832ea32 Add missing catch-all in :elixir_import.is_sigil/1 (#15264)
Fixes a compiler crash when importing a module with
only: :sigils option when the imported module exports
non sigil symbols with `sigil_` prefix.

Closes #15263.
2026-04-13 20:25:33 +02:00
José Valim 8011552eac Remove ?break handling duplication 2026-04-13 19:42:28 +02:00
José Valim ebb7c73353 Improve inference across for and try 2026-04-13 19:31:08 +02:00
Łukasz Samson 898f1a2f69 Reject unsupported newlines in tokenizer ASCII fast path (#15262) 2026-04-13 19:30:31 +02:00
José Valim 603602e67b Implement reverse arrows for case (#15260) 2026-04-13 15:21:08 +02:00
Emiliano Mancuso 92221937d3 Return {0, -1} for missing files in last_modified_and_size/1, closes #15258 (#15259) 2026-04-13 08:25:49 +02:00
José Valim 245066b405 Relax handling of __exception__
We were used to assert it to be true but,
given the value is not used in any shape,
we can relax the definition and simply assert
on the presence of the key.

Closes #15251.
2026-04-11 17:42:01 +02:00
José Valim 4d30464ec4 Avoid conflicts between internal vars and empty vars, closes #15246 2026-04-11 17:25:12 +02:00
José Valim 139da8fa5f Only check for lock files in non-path dependencies (see #15158) 2026-04-11 16:55:54 +02:00
José Valim 8fdef5c48f Properly track variables inside comprehensions size, closes #15247 2026-04-11 12:24:44 +02:00
José Valim d0514dfd15 Clean up typing changes 2026-04-11 12:17:16 +02:00
Guillaume Duboc 325608b518 Avoid applying map update callbacks to absent branches (#15248) 2026-04-11 12:06:29 +02:00
Guillaume Duboc c4275e39b2 Reject empty args in dynamic fun_apply (#15254) 2026-04-11 12:00:15 +02:00
Andrea Leopardi 1bf7bb0682 Fix param name in docs for NaiveDateTime functions (#15249) 2026-04-11 10:20:55 +02:00
dependabot[bot] 7d5d1e27ca Bump erlef/setup-beam from 1.23.0 to 1.24.0 (#15242)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.23.0 to 1.24.0.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/ee09b1e59bb240681c382eb1f0abc6a04af72764...fc68ffb90438ef2936bbb3251622353b3dcb2f93)

---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:20:37 +02:00
dependabot[bot] 50cc7ef182 Bump github/codeql-action from 4.32.4 to 4.35.1 (#15241)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.4 to 4.35.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/89a39a4e59826350b863aa6b6252a07ad50cf83e...c10b8064de6f491fea524254123dbe5e09572f13)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:19:42 +02:00
dependabot[bot] 5ec106a5e2 Bump DavidAnson/markdownlint-cli2-action from 22.0.0 to 23.0.0 (#15240)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 22.0.0 to 23.0.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/07035fd053f7be764496c0f8d8f9f41f98305101...ce4853d43830c74c1753b39f3cf40f71c2031eb9)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 23.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:19:36 +02:00
dependabot[bot] 5c845c796c Bump azure/trusted-signing-action from 1.1.0 to 1.2.0 (#15225)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/87c2e83e6868da99d3380aa309851b32ed9a8346...b443cf8ea4124818d2ea9f043cba29fc3ec47b16)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 10:19:21 +02:00
Guillaume Duboc 469acb110f Fix guard equality with short-circuit operators
Signed-off-by: José Valim <jose.valim@gmail.com>

Closes #15253.
Closes #15257.
2026-04-11 10:16:59 +02:00
José Valim 7061c37b88 Add --no-compile to mix format
Closes #15256.
2026-04-10 12:19:25 +02:00
Nelson Vides 74bb90a3cd SWAR-optimize ASCII fast paths in String.length and String.slice (#15255)
Add 56-bit SWAR (SIMD Within A Register) acceleration to skip_length/2
and byte_size_remaining_at/2, processing 8 ASCII bytes per iteration
instead of one. Uses the Mycroft zero-byte detection algorithm to
validate that 7+1 bytes are all ASCII with no \r in a single guard.

This mirrors the approach taken in OTP's string module
(OTP https://github.com/erlang/otp/pull/10948).
2026-04-09 23:22:54 +02:00
Lukasz Samson 5d794ab17c Pass correct bitstring meta during expansion
Closes #15244
2026-04-08 18:42:12 +02:00
José Valim c1256f161c Clarify shut down order in ExUnit, closes #15236 2026-04-05 09:46:06 +02:00
José Valim 8f40bf117a Add "mix source MODULE" to print or open a given module/function location (#15234) 2026-04-04 21:11:25 +02:00
José Valim 8d691697ff Support types and callbacks in mix help 2026-04-04 20:01:18 +02:00
José Valim 6f2e24ca63 Add source/1 to IEx 2026-04-04 19:55:26 +02:00
José Valim 8cc693864a Improve docs for start_link on supervisor and genserver, closes #15232 2026-04-04 11:17:19 +02:00
José Valim 0f8baa3041 Remove test with race condition (already covered in warnings_test.exs) 2026-04-04 11:17:19 +02:00
Jonatan Kłosko 017162a975 Do not call fsync in lock implementation (#15229) 2026-04-02 15:40:45 +02:00
Jean Klingler f74b3648e2 Correct mention to after_compile/2 in changelog (#15224) 2026-03-31 20:56:29 +09:00
dependabot[bot] 28b6616a59 Bump actions/download-artifact from 8.0.0 to 8.0.1 (#15214)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:22:44 +02:00
dependabot[bot] 7c2d4f3d8e Bump actions/attest-sbom from 4.0.0 to 4.1.0 (#15213)
Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/actions/attest-sbom/releases)
- [Changelog](https://github.com/actions/attest-sbom/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-sbom/compare/07e74fc4e78d1aad915e867f9a094073a9f71527...c604332985a26aa8cf1bdc465b92731239ec6b9e)

---
updated-dependencies:
- dependency-name: actions/attest-sbom
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:22:28 +02:00
dependabot[bot] ba4eb73734 Bump azure/login from 2.3.0 to 3.0.0 (#15212)
Bumps [azure/login](https://github.com/azure/login) from 2.3.0 to 3.0.0.
- [Release notes](https://github.com/azure/login/releases)
- [Commits](https://github.com/azure/login/compare/a457da9ea143d694b1b9c7c869ebb04ebe844ef5...532459ea530d8321f2fb9bb10d1e0bcf23869a43)

---
updated-dependencies:
- dependency-name: azure/login
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:22:03 +02:00
dependabot[bot] fbece98a5d Bump erlef/setup-beam from 1.20.4 to 1.23.0 (#15211)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.20.4 to 1.23.0.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/e6d7c94229049569db56a7ad5a540c051a010af9...ee09b1e59bb240681c382eb1f0abc6a04af72764)

---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:21:42 +02:00
José Valim 9e4eb3dc49 Release v1.20.0-rc.4 2026-03-31 09:55:32 +02:00
José Valim 2916f20189 Keep warnings on map/remote dispatch with/without parens
Closes #15219.
2026-03-30 10:27:58 +02:00
José Valim 03b9fde67a Allow test names longer than 255 characters 2026-03-29 09:13:46 +02:00
cui f16d2a32c1 Remove range overlap in Logger truncation (#15221) 2026-03-27 10:27:23 +01:00
Eksperimental cdbf9b7394 Improve function argument naming, specs and guards in Mix (#15216) 2026-03-26 12:18:15 +01:00
Jean Klingler a58a40ce48 Purge and delete modules if after_verify/2 callback fails (#15220) 2026-03-26 20:08:02 +09:00
José Valim a24d3ff221 Avoid false positives when diffing bitstrings, closes #15204 2026-03-25 08:20:05 +01:00
Eksperimental b77e65dd8a Improve funcion signatures and guard consistency in Module (#15215)
Before function signatures showed arguments as: arg, tuple, tuples.
Now they appear as: definition, definitions.

Guards has been improved and standardized, along with variable names.
2026-03-25 08:19:24 +01:00
José Valim 37bbba9d58 Document map_get behaviour, do not split term types, closes #15209 2026-03-25 08:06:50 +01:00
José Valim 2a7a37f598 Add reverse arrows for min/max, closes #15210 2026-03-25 08:00:32 +01:00
José Valim 972df19e13 Work around compiler bug in Erlang/OTP 27+ 2026-03-24 19:50:31 +01:00
José Valim 02deafde99 Avoid warnings in docs 2026-03-24 19:32:57 +01:00
José Valim bafb68db8e Another attempt to work around Erlang/OTP compiler bug 2026-03-24 19:14:50 +01:00
José Valim 24ebb96a27 Expand case by hand to avoid bug in Erlang/OTP 27+ 2026-03-24 19:05:40 +01:00
José Valim 8eb702aea3 Add fast path for two leafs 2026-03-24 18:43:32 +01:00
José Valim 415cd20100 Allow difference formulas to apply across unions 2026-03-24 18:32:33 +01:00
José Valim 5979c42de7 Fix rendering of regex docs 2026-03-24 11:32:51 +01:00
José Valim 5949bad336 Fix pretty printing of map difference 2026-03-23 22:24:00 +01:00
José Valim cfd62cec73 Propagate fields on difference only once, closes #15207 2026-03-23 22:17:50 +01:00
José Valim 447427a141 Update CHANGELOG 2026-03-23 13:02:38 +01:00
José Valim a9ce579829 Revert structs into export time dependencies 2026-03-23 12:52:50 +01:00
José Valim 8df8f92030 Fix introduction to the next topic in some pages, closes #15205 2026-03-22 20:04:55 +01:00
dependabot[bot] b5d66beae7 Bump zizmorcore/zizmor-action from 0.5.0 to 0.5.2 (#15200)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.0 to 0.5.2.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d...71321a20a9ded102f6e9ce5718a2fcec2c4f70d8)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 19:52:52 +01:00
José Valim e17069e559 Protocol should not add deps on implementations, closes #15202 2026-03-19 10:17:04 +01:00
José Valim 4aa837d255 Remove unused optimization branch in difference 2026-03-18 17:25:08 +01:00
José Valim d8c921fabc Refine hd/tl in guards, closes #15198 2026-03-17 09:36:20 +01:00
José Valim 9c00059f18 Further optimize map differences and fix bug in map_update with negated keys 2026-03-16 23:27:44 +01:00
José Valim 630d5ca629 Fix typos in code comments 2026-03-16 23:27:44 +01:00
Panagiotis Nezis 5bf4df5520 Raise ArgumentError in Keyword.from_keys/2 for non-atom keys (#15197) 2026-03-16 18:30:53 +01:00
João Duarte a7b2d6c679 Fix --warnings-as-errors not catching misnamed test file warnings (#15194) 2026-03-16 18:30:08 +01:00
José Valim 4064905654 Do not perform structural operations on leaf difference 2026-03-15 17:30:50 +01:00
José Valim 6cb9140d9f Remove unnecessary empty checks when computing previous types 2026-03-15 15:31:19 +01:00
José Valim bffb6965a6 Implement one key difference for leaf bdd difference
This allows us to implement previous as a series of
unions, which performs better across all cases.

Closes #15195.
2026-03-15 15:18:47 +01:00
José Valim c39d9ea762 Add more comments to descr, avoid unnecessary traversals 2026-03-15 13:24:03 +01:00
José Valim 98468bd8e3 Further optimize leaf differences 2026-03-15 12:36:35 +01:00
José Valim c8c7abc3b2 Do not check previous if we know args types and previous are disjoint 2026-03-14 19:33:23 +01:00
José Valim 690323e174 Perform fewer args_to_domain conversions 2026-03-14 19:01:19 +01:00
José Valim 528f8e7d7b Return boolean for is_integer/3 with all integers 2026-03-14 11:17:17 +01:00
José Valim 127901b8f8 Add type signature for :erlang.is_integer/3 2026-03-14 11:16:03 +01:00
José Valim aec5d53284 Optimize tuple_fetch, closes #15154 2026-03-14 11:00:40 +01:00
dependabot[bot] 445895c98a Bump actions/attest-sbom from 3.0.0 to 4.0.0 (#15177)
Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/actions/attest-sbom/releases)
- [Changelog](https://github.com/actions/attest-sbom/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-sbom/compare/4651f806c01d8637787e274ac3bdf724ef169f34...07e74fc4e78d1aad915e867f9a094073a9f71527)

---
updated-dependencies:
- dependency-name: actions/attest-sbom
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 09:50:35 +01:00
dependabot[bot] e9adf01e3f Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#15176)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 09:50:21 +01:00
José Valim bc7af21c02 Allow overriding specific dependencies in override (#15193)
Closes #14082. Closes #14156.
2026-03-14 09:48:39 +01:00
José Valim 7945446cb4 Allow preserving sigil metadata in container cursor to quoted
Closes #14243.
2026-03-13 20:03:34 +01:00
José Valim 2e7a49d426 Abort diffing on binary patterns, closes #15191 2026-03-13 18:25:07 +01:00
José Valim 5bad452d0a Show undefined function errors even on variable failure, closes #13465 2026-03-13 17:07:37 +01:00
José Valim ec8156aad9 Tiny cleanup on sigil chapter 2026-03-13 11:53:10 +01:00
Jonatan Kłosko 7967ff985b Add :dbg_callback option to Code eval functions (#15189) 2026-03-13 08:13:01 +01:00
Jonatan Kłosko fb2657aa55 Fix compile env change triggering full recompilation of path dependencies (#15188) 2026-03-13 08:11:01 +01:00
Gonzalo R 4021272d42 Fix missing space typo in compiler error message (#15186) 2026-03-12 21:45:00 +01:00
Philipp 28ffbcfe84 Add nil as documented valid option for GenServer :name (#15187) 2026-03-12 20:37:58 +01:00
José Valim f9310524b1 Revert "Fix unnecessary recompilation when dbg_callback is modified at runtime (#15007)" (#15185)
This reverts commit c80a3b1a0e.
2026-03-12 12:50:48 +01:00
José Valim 57b0e29135 Add a build lock around protocol consolidation in umbrellas, closes #15133 2026-03-12 12:45:38 +01:00
José Valim b18637fb97 Treat modules with --no-debug-info as uncached, closes #15159
Additionally make sure --debug-info is part of the cache key.
2026-03-12 11:12:26 +01:00
José Valim 7f3d95b60c Use config files to pass project state mix deps.partition, closes #15181 2026-03-12 10:24:17 +01:00
José Valim df0553bd08 Do not raise false positive on not is_struct checks, closes #15183 2026-03-12 09:59:54 +01:00
José Valim 3452e6ab2f Ensure compilation of sibling deps do not mark path deps as changed, closes #15158 2026-03-11 17:02:00 +01:00
Panagiotis Nezis ababaf8b08 Fix URI.merge leaking :+ marker when base path is empty string (#15179)
When merging URIs where the base has `path: ""`, the internal `:+` marker
used to track the join point between base and relative paths was being
converted to a literal "+" in the output.
2026-03-11 12:07:04 +01:00
José Valim a064a00601 Fix CI and improve ExUnit results 2026-03-11 11:19:51 +01:00
Andrea Leopardi 352bc46c8e Improve ExUnit failure/success output (#15168) 2026-03-11 08:27:58 +01:00
José Valim 74d61833ff Add docs on native coverage 2026-03-11 08:13:53 +01:00
Panagiotis Nezis 9b80ab584d Fix admonition blocks in Module docs (#15175) 2026-03-10 17:56:20 +01:00
Jean Klingler 526d00f949 Always set ERLC_OPTS=warnings_as_errors in CI (#15174) 2026-03-10 22:33:17 +09:00
Guillaume Duboc 51207406d0 Clarify and test gradual function application (#15173)
* Implement gradual function domains and tighten function application

Add fun_domain/1 to compute function domains for static, dynamic, and mixed function types, including explicit :badfun and {:badarity, ...} outcomes when domain extraction is not well-defined.

Refine gradual function application internals by separating static and dynamic normalization paths, documenting behavior for purely dynamic and mixed cases, and clarifying domain/compatibility handling.

Expand descr tests with gradual-application edge cases and a dedicated fun_domain test suite covering static, dynamic, mixed, and arity-error scenarios.

* Refactor function application and domain handling

* Remove fun_domain, recover coverage via fun_apply tests

fun_domain was only used in tests; its behavior is fully observable
through fun_apply (wrong-domain arg → :badarg, right-domain → :ok).
Removed it and converted the domain tests to fun_apply assertions,
adding a few previously uncovered cases along the way: pure non-fun
badfun, union-arg-type enforcement, 2-arity, mixed-arity badarity
for both static and dynamic unions, and the mixed static+dynamic
gradual domain case.

* Remove dead helpers introduced alongside fun_domain

fun_single_arity, fun_single_arity_pair, fun_non_empty_arities_of,
and fun_non_empty_arities are now unreachable. Also revert
dynamic_fun_top? to the simpler map == %{} check — the semantic
emptiness scan added in the refactor was only needed to handle
the same edge cases fun_domain was handling.

* Move mixed-arity badarity check to static side of fun_normalize_both

fun_normalize itself stays simple (no arity cross-check). The check
for other non-empty arities now lives in fun_normalize_both via
fun_other_non_empty_arities/2, and only fires for the static
component. This means:

- union(fun/1, fun/2) applied with 1 arg → {:badarity, [1, 2]}
  (static: we know one branch will always fail at runtime)

- union(dynamic_fun/1, dynamic_fun/2) applied with 1 arg →
  {:ok, dynamic(integer())} (dynamic: picks matching-arity arrows,
  wraps in dynamic() to reflect that the other branch may fail)

* Add tests for mixed-arity union behavior

Static union: badarity regardless of which arity is called with,
with the called arity listed first followed by the others.

Dynamic union: picks the matching-arity arrows and wraps in dynamic(),
gives badarity only when no arity matches at all, and falls back to
dynamic() when the arg is outside the domain but dynamically compatible.
2026-03-10 13:39:54 +01:00
Jean Klingler 78a5151865 Add support for [:raw] opts in File.read (#15172) 2026-03-10 20:07:07 +09:00
Andrea Leopardi c464eb89c5 Add pointer to CLI config in Mix.Task.preferred_cli_env/1 (#15171) 2026-03-10 12:00:22 +01:00
Jean Klingler 10902df940 More cleanup of older OTP versions (#15170)
* More cleanup since we require OTP 27

* Remove mentions to OTP25- in docs

* Use same versions in Getting Started as in Mix guide
2026-03-10 19:49:17 +09:00
José Valim 44e0541df1 Avoid unnecessary concat in tuple elim size 2026-03-10 11:10:58 +01:00
Jean Klingler 71642e381a Add Process.get_label/1 to expose :proc_lib.get_label (#15169) 2026-03-10 19:01:10 +09:00
Panagiotis Nezis d0ce39e1ce Fix incorrect typespecs in Module (#15167)
- Fix `module_info(:nifs)` typespec to return `[function_info]` instead of `module()`
- Fix `Module.overridables_in/1` typespec to return `[definition]` instead of `[atom]`
2026-03-10 10:06:04 +01:00
José Valim e3c74e1e1e Simplify tuple union implementation 2026-03-10 09:48:08 +01:00
dependabot[bot] c184b82b5f Bump actions/download-artifact from 7.0.0 to 8.0.0 (#15126)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/37930b1c2abaa49bbe596cd826c3c89aef350131...70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 09:01:25 +01:00
José Valim 90b5266ca7 Do not expand negations when pretty printing tuples, see #15154 2026-03-10 08:44:12 +01:00
José Valim 382b5b06de Convert domains to also use orddict 2026-03-10 08:17:00 +01:00
Jean Klingler 2c54f9a64a More cleanup: remove compatibility code for OTP26- (#15166)
* Remove compatibility code for OTP26-

* Remove support for application/erlang+html docs v1

* Remove workaround for bug in older OTP versions

* Update OTP version checks

* Remove OTP25- skipping in tests

* More test cleanup
2026-03-10 16:12:36 +09:00
Jean Klingler 2817a70680 Remove mentions to OTP27+ versions from docs, comments and tests (#15165)
* Remove mentions of "since OTP27+"

* Remove test code for OTP26-
2026-03-10 15:39:03 +09:00
Panagiotis Nezis acd76b68ab Fix PartitionSupervisor.count_children/1 and stop/3 to support via tuples (#15161)
The `name()` type for PartitionSupervisor includes `{:via, module(), term()}`
tuples, but `count_children/1` and `stop/3` only accepted atoms, raising
`FunctionClauseError` for via tuples.
2026-03-10 07:24:37 +01:00
Panagiotis Nezis 6f9afad9ca Fix Integer.extended_gcd/2 returning negative GCD for zero base cases (#15163)
When one argument is 0 and the other is negative, `extended_gcd/2`
was returning a negative GCD, violating both the @spec (which
declares `non_neg_integer`) and consistency with `gcd/2`.
2026-03-10 07:24:29 +01:00
Jean Klingler 13024f43c0 Remove OTP26 support and add OTP29-rc (#15164)
* Update OTP compatibility table

* Add OTP 29.0 rc to CI, remove 26.0

* Remove :elxiir_json polyfill

* Remove :elixir_utils.jaro_similarity polyfill

* defdelegate directly to :proc_lib.set_label/2
2026-03-10 15:10:19 +09:00
Panagiotis Nezis 8914dcdcbb Fix Integer.undigits/2 to validate negative out-of-range digits (#15160)
The validation was asymmetric - it rejected positive digits >= base
but silently accepted negative digits <= -base. This fix adds the
missing check for negative out-of-range digits.
2026-03-09 20:35:10 +01:00
José Valim 2db87ebc70 Release v1.20.0-rc.3 2026-03-09 19:49:28 +01:00
Panagiotis Nezis 5e7671a035 Fix Float.parse/1 inconsistent error handling for non-scientific notation overflow (#15157)
`Float.parse/1` was returning `:error` for overflow in scientific notation (e.g.,
"1.0e400") but raising `ArgumentError` for equivalent overflow in non-scientific
notation (e.g., a 310-digit number). This violated the documented behavior that
states `Float.parse/1` should return `:error` when the float exceeds the maximum
size.
2026-03-09 19:30:26 +01:00
Panagiotis Nezis 52c58c72d3 Preserve directory permissions in File.cp_r/3 (#15155)
The documentation states that `cp_r/3` maintains "modes" when copying, but
directory permissions were not being preserved - only file permissions.
2026-03-09 14:40:00 +01:00
José Valim d56669789f Process fields even when structs are unknown, closes #15156 2026-03-09 14:38:54 +01:00
José Valim 92e1830bf7 Continue traversing tuple leafs only if subtyping is a possibility 2026-03-09 14:07:56 +01:00
José Valim 4468a12222 Do not compute previous subtype during inference 2026-03-09 12:11:55 +01:00
Panagiotis Nezis c6820a1297 Fix File.cp_r/3 infinite loop with symlink cycles (#15152)
When `dereference_symlinks: true` was set and there was a symlink cycle
(e.g., a -> b -> a), the function would infinitely recurse. This fix
tracks visited paths using a `MapSet` and returns `{:error, :eloop, path}`
when a cycle is detected.
2026-03-09 10:42:29 +01:00
José Valim 93734a5c86 Provide better tuple elimitation in differences 2026-03-09 09:57:46 +01:00
José Valim e5811283af Avoid double negation removal pass on map pretty printing 2026-03-09 09:11:06 +01:00
José Valim 2639028da8 Do not always compute one key difference in map difference 2026-03-09 09:03:42 +01:00
José Valim 9add53318a Avoid cascading unions in map difference 2026-03-09 08:56:29 +01:00
José Valim 27aadff38d Tackle empty intersections in map fetch key 2026-03-09 08:20:45 +01:00
José Valim 64a5c1742f Document why binary_part operation is safe 2026-03-08 23:46:17 +01:00
José Valim b98d823c54 Simplify differences (#15153)
The previous implementation was incorrectly nesting differences.
So if we had four clauses, the four clause was computing the type as:

    clause4 - (clause3 - clause2 - clause1) - (clause2 - clause1) - clause1

While it should have been:

    clause4 - clause3 - clause2 - clause1

This means that instead of `n` differences, we had an arithmetic progression
of `n * (n + 1) / 2` differences.
2026-03-08 23:02:28 +01:00
Panagiotis Nezis ea672ccd81 Fix spec of PartitionSupervisor.partitions/1 (#15151) 2026-03-08 22:53:51 +01:00
José Valim 4ad3e0afb9 Fix map_difference_strategy with optional keys 2026-03-08 11:38:24 +01:00
José Valim a5e83ebc04 Use separate strategy function for differences 2026-03-08 11:03:51 +01:00
Panagiotis Nezis dd1f52917c Fix File.cp_r/3 infinite loop when copying into subdirectory of source (#15148)
When destination is a subdirectory of source, `cp_r` would enter an
infinite loop because the newly created destination directory would
be included in the file listing during recursion. This can **cause disk
exhaustion** if large files are present under source.

This adds a validation check that returns `{:error, :einval, destination}`
when copying on a subdir of the source path.

Also document typical error reasons including the new `:einval` case.
2026-03-08 11:03:10 +01:00
Jean Klingler 07e6e1a985 Warn when defining @type record(), fixes CI on OTP29 (#15150) 2026-03-08 17:10:16 +09:00
José Valim 663c4ab57e Make type of map_bdd_to_dnf conversion explicit 2026-03-07 23:05:13 +01:00
José Valim 0dbdadae4a Revert previous strategy to use multiple differences 2026-03-07 22:59:55 +01:00
José Valim d4495ae22a Use different strategy to remove negations in map access 2026-03-07 22:58:30 +01:00
José Valim 09c90ee5fc Reuse map_leaf_strategy on difference 2026-03-07 22:58:30 +01:00
Panagiotis Nezis bbe700151e Fix File.Stream Enumerable.count for files without trailing newline (#15147)
The optimized count implementation was counting newline characters
instead of actual lines, returning incorrect results for files that
don't end with a newline (which is common for source code files).

For example, a file with content "line1\nline2\nline3" (3 lines)
was returning 2 instead of 3.

The fix tracks the last byte read and adds 1 at EOF if the file
has content but doesn't end with a newline.
2026-03-07 11:54:01 +01:00
Panagiotis Nezis d61ba915b0 Raise ArgumentError on invalid binding in Code.eval_string/3 (#15146)
Previously, passing a non-list binding to `Code.eval_string/3` would crash
deep in Erlang code with a confusing `FunctionClauseError` in
`:elixir_erl_var.load_binding/6`.
2026-03-07 10:01:59 +01:00
Daniel Kukula 2973af6c97 Filter modules during collection in IEx autocomplete (#15143)
Instead of collecting all modules as strings, sorting the full list, and
then linearly scanning for matching prefixes, filter modules during
collection and only sort the (much smaller) set of matches.

Benchmarked with 261 loaded modules, compared against the original code
(before the previous two optimizations) and the current code:

  match_modules("Elixir.Enum"):
    before PR:  333 µs, 13.6 KB heap
    current:     91 µs,  1.6 KB heap
    proposed:    30 µs,  0.5 KB heap  (11x faster, 26x less heap)

  match_modules("er"):
    before PR:  345 µs, 13.8 KB heap
    current:     97 µs,  2.9 KB heap
    proposed:    39 µs,  0.4 KB heap  (9x faster, 33x less heap)
2026-03-07 09:12:36 +01:00
Panagiotis Nezis 47d72d94d8 Fix Enum.slice/2 for ranges with step > 1 sliced by step > 1 (#15144)
When slicing a Range with step > 1 using a slice range that also has
step > 1, the formula for calculating the step in the slicer function
was incorrect.
2026-03-07 08:56:13 +01:00
José Valim 9a663cf3fa Convert map fields into an orddict and implement meet optimizations (#15142)
Related to #15129.
2026-03-06 21:57:55 +01:00
Daniel Kukula 7d3863fcf6 Reduce memory usage in IEx autocomplete module listing (#15140)
Use :erlang.loaded/0 instead of :code.all_loaded/0 since we only need
module names, not file paths. The latter copies the full path for every
loaded module, which on production systems with many dependencies
caused significant memory spikes (measured 2.8MB -> 172KB on a real pod).

Also replace Enum.sort/1 |> Enum.dedup/1 with :lists.usort/1 and
avoid list allocations.
2026-03-06 19:20:41 +01:00
Raúl R Pearson 29f572643e Upcase "PID" in Task docs (#15141) 2026-03-06 17:36:07 +01:00
Eric Meadows-Jönsson 80df1fe3dc Fix undefined ref_name variable in release S3 upload (#15139) 2026-03-06 12:15:42 +01:00
José Valim 6fd161d67d Optimize previous handling by using nested unions instead of nested differences
Partially addresses #15129.
2026-03-05 12:19:47 +01:00
José Valim 576f123b16 Intersection of maps with not set keys should not be empty, closes #15131 2026-03-05 10:34:03 +01:00
José Valim 77e6687eef Handle Kernel.in/2 in type system instead of using rewrite rules, closes #15130 2026-03-05 08:48:03 +01:00
José Valim de4b1610a9 Update comment on square brackets validity in WHATWG 2026-03-04 21:43:02 +01:00
José Valim 0df45fdba1 Release v1.20.0-rc.2 2026-03-04 16:16:54 +01:00
José Valim 8e5e62653e More docs and warnings around URLs, closes #15125 2026-03-04 09:48:10 +01:00
José Valim f7c4047d30 Keep old functions around so old artifacts still work as expected 2026-03-03 13:37:27 +01:00
José Valim 6e6ed6a3b5 Only return purgeable modules back to pool 2026-03-03 13:35:09 +01:00
José Valim 0ea809e720 Check redundant clauses for definitions (#15124) 2026-03-02 17:19:51 +01:00
Jean Klingler cd38841a18 Raise clearer error when reserved word used at the top level of defmodule (#15123) 2026-03-02 18:18:35 +09:00
Jean Klingler 132197a254 Remove redundant clause triggering warning in tests (#15122) 2026-03-02 17:17:07 +09:00
George Guimarães 8cda8e1bae Print formatted file names in mix format --verbose (#15119) 2026-02-28 12:25:38 +01:00
José Valim 978e4047f4 Update CHANGELOG 2026-02-27 15:46:20 +01:00
José Valim b792fb41c5 Clarify error message for always failing private function, closes #14401 2026-02-27 15:16:02 +01:00
José Valim d7865ecfa7 Improve inference for anonymous functions 2026-02-27 15:02:09 +01:00
José Valim 1f211d121e Clean up diagnostic formatting on patterns 2026-02-27 12:20:08 +01:00
José Valim aed240bf40 Warns on redundant clauses from fn 2026-02-27 11:12:22 +01:00
José Valim bac6ef9d30 Implement redundant clauses for reduce and convert them to warnings 2026-02-27 10:42:13 +01:00
José Valim 7b0ae43231 Address ambiguity on type violation in nested || 2026-02-27 08:56:22 +01:00
Eksperimental 677afeced3 Improve typespecs for t:ExUnit.configure_opts/0 (#15117)
`:exclude`/`:include` can be:
- an atom
- a list of atoms and keyword elements
2026-02-27 08:48:20 +01:00
Eksperimental 2292f3bcb6 Improve docs for Filters in ExUnit (:exclude and :include configuration) (#15118) 2026-02-27 08:47:54 +01:00
José Valim a2a669eb7b Improve error messages in many Kernel operations 2026-02-26 17:58:18 +01:00
José Valim b85d1c84d3 Print negations whenever most of the descr types are either top or negated 2026-02-26 17:58:18 +01:00
Jonatan Männchen 4602240192 Add Software Bill of Materials guide to the Documentation (#15067)
Introduces a new guide explaining SBoMs: what they are, why they matter
(vulnerability analysis, regulatory requirements, license compliance),
and how to generate them for Elixir projects using mix_sbom. Also covers
CI integration via GitHub Action and mentions ORT for deeper file-level
license scanning.
2026-02-26 10:07:12 +01:00
Jonatan Männchen a71a4e8f7a Keep credentials for release branch tagging (#15115)
The checkout in create_draft_release needs credentials to push
the branch-latest tag to origin.
2026-02-25 13:16:23 +01:00
Jonatan Männchen af34af86aa Add zizmor workflow audit to CodeQL workflow
Run zizmor static analysis on GitHub Actions workflows to detect
potential security vulnerabilities in CI/CD configuration.

See: https://docs.zizmor.sh/
2026-02-25 12:59:01 +01:00
Jonatan Männchen 920b7bef2e Add CodeQL workflow for GitHub Actions security analysis
Run CodeQL static analysis on GitHub Actions workflows to detect
potential security vulnerabilities in CI/CD configuration.

Scheduled weekly and on pushes/PRs to main branch.
2026-02-25 12:59:01 +01:00
Jonatan Männchen 01212cefc8 Suppress false-positive github-env warnings from zizmor
The GITHUB_PATH and GITHUB_ENV writes are safe as they use only
workflow-controlled values, not attacker-controlled input.

See: https://docs.zizmor.sh/audits/#github-env
2026-02-25 12:59:01 +01:00
Jonatan Männchen 31d62fddb5 Add cooldown to Dependabot configuration
Delay automatic dependency updates to allow time for discovery of
regressions or supply-chain compromises in newly released versions.

See: https://docs.zizmor.sh/audits/#dependabot-cooldown
2026-02-25 12:59:01 +01:00
Jonatan Männchen 32d20fd77c Prevent template injection in GitHub Actions workflows
Use environment variables instead of direct template expansion
in shell run blocks to prevent potential code injection.

See: https://docs.zizmor.sh/audits/#template-injection
2026-02-25 12:59:01 +01:00
Jonatan Männchen dfe857e7df Prevent credential persistence in checkout actions
Add `persist-credentials: false` to all `actions/checkout` usages
to prevent Git credentials from being persisted in the repository
after checkout completes.

See: https://docs.zizmor.sh/audits/#artipacked
2026-02-25 12:59:01 +01:00
Michał Łępicki 6d5f43318c Allow specifying SEED when running tests (#15111)
this is helpful when reproducing flaky test failures
2026-02-25 09:12:15 +01:00
Michał Łępicki 43dde95ed2 Update ParallelCheker.verify/2 spec (#15113)
The return type changed in https://github.com/elixir-lang/elixir/commit/a0ef7f0c3e0049468799ffcea5dddb6542a01568
2026-02-24 23:07:24 +01:00
Michał Łępicki 565f468d68 Clean up unused in_body? code branch in in macro impl (#15112)
The dialyzer warning was:

lib/kernel.ex:4836:7: Guard test _in_body?@1::'false' =:=
          'true' can never succeed
2026-02-24 21:19:59 +01:00
José Valim c903033097 Compute redundant clauses for try-catch and try-else 2026-02-24 13:11:05 +01:00
José Valim 7caa954690 Compute redundant clauses for receive 2026-02-24 11:19:06 +01:00
José Valim 4c203429d6 Include maybe feature 2026-02-23 14:27:56 +01:00
José Valim a0ef7f0c3e Move struct validation in patterns and updates to type checker
This means changing a struct definition only cases
recompilation when structs are created, no longer on
matches and updates.

Closes #15098.
2026-02-23 14:24:29 +01:00
José Valim 34a50002f9 Keep Kernel.in/2 as :lists.member/2 and optimize in type checker and Erlang pass 2026-02-23 14:24:29 +01:00
Łukasz Jan Niemier 38dcc453b8 Optimise Map.take/2 for empty list of keys (#15110) 2026-02-23 09:10:09 +01:00
José Valim 34fda89acb Recompose multiple orelse as Kernel.in for performance, closes #15108 2026-02-20 18:54:45 +01:00
Steffen Deusch 13846bd25f Fail when misnamed test files are detected and --warnings-as-errors is set (#15106)
Closes https://github.com/elixir-lang/elixir/issues/15105.
2026-02-19 18:56:33 +01:00
José Valim b05fec0c87 Use evaluation when compiling modules (#15087) 2026-02-19 18:11:28 +01:00
José Valim 9ba89f4da6 Properly handle compile attributes in Erlang sources 2026-02-18 13:53:24 +01:00
José Valim 85b1ad0764 Do not emit warnings during IEx parsing
Closes #15090.

Unfortunately, I could not write a test for this as the error
only happens when using actual I/O devices. When using the mock
device we use in tests, the warning appears as expected.
2026-02-18 13:45:56 +01:00
José Valim 23633f7723 Use IEx.Broker.shell to find nodes instead of group leader, closes #15092 2026-02-18 12:15:22 +01:00
José Valim ab7c019d24 Topsort Erlang modules before compilation, closes #15100 2026-02-18 10:59:32 +01:00
José Valim f2e61f6a04 Use system tmp dir for socket copy test, closes #15093 2026-02-18 09:59:23 +01:00
José Valim 5abcc33267 Improve error message and docs for headless clauses, closes #15104 2026-02-18 09:54:04 +01:00
José Valim 52296e00c6 Improve case error message and improve performance 2026-02-18 09:36:52 +01:00
dependabot[bot] d8eed7812b Bump azure/trusted-signing-action from 1.0.0 to 1.1.0 (#15103)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/db7a3a6bd3912025c705162fb7475389f5b69ec6...87c2e83e6868da99d3380aa309851b32ed9a8346)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 23:23:45 +01:00
265 changed files with 14598 additions and 6887 deletions
+12 -7
View File
@@ -3,8 +3,7 @@
---
name: Report an issue
description:
Tell us about something that is not working the way we (probably) intend
description: Tell us about something that is not working the way we (probably) intend
body:
- type: markdown
attributes:
@@ -13,11 +12,17 @@ body:
Please, do not use this form for guidance, questions or support.
Try instead in [Elixir Forum](https://elixirforum.com),
the [IRC Chat](https://web.libera.chat/#elixir),
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
[Slack](https://elixir-slackin.herokuapp.com),
[Discord](https://discord.gg/elixir) or in other online communities.
Try instead in [Elixir Forum](https://elixirforum.com) or any of
our online communities (Slack, Discord, etc).
- type: checkboxes
id: existing-issue
attributes:
label: Existing issue
description: Please search [existing issues](https://github.com/elixir-lang/elixir/issues) before continuing.
options:
- label: I have searched existing issues and could not find a duplicate.
required: true
- type: textarea
id: elixir-and-otp-version
+2
View File
@@ -7,3 +7,5 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
+12 -14
View File
@@ -26,31 +26,27 @@ jobs:
fail-fast: false
matrix:
include:
- otp_version: "28.1"
- otp_version: "29.0"
deterministic: true
- otp_version: "28.1"
erlc_opts: "warnings_as_errors"
- otp_version: "28.4"
docs: true
coverage: true
- otp_version: "28.1"
- otp_version: "27.3"
erlc_opts: "warnings_as_errors"
- otp_version: "27.0"
erlc_opts: "warnings_as_errors"
- otp_version: "26.0"
- otp_version: master
development: true
- otp_version: maint
development: true
# Earlier Erlang/OTP versions ignored compiler directives
# when using warnings_as_errors. So we only set ERLC_OPTS
# from Erlang/OTP 27+.
env:
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
ERLC_OPTS: "warnings_as_errors"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: ${{ matrix.otp_version }}
@@ -96,7 +92,7 @@ jobs:
- name: "Upload Coverage Artifact"
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: TestCoverage
path: cover/*
@@ -119,17 +115,19 @@ jobs:
strategy:
matrix:
otp_version:
- "29.0"
- "28.1"
- "27.3"
- "26.2"
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: ${{ matrix.otp_version }}
+55
View File
@@ -0,0 +1,55 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2026 The Elixir Team
name: "CodeQL Advanced"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "29 8 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: "ubuntu-latest"
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: "/language:${{matrix.language}}"
zizmor:
name: Zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+2
View File
@@ -28,6 +28,8 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run OSS Review Toolkit
id: ort
+3 -1
View File
@@ -34,6 +34,8 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
+1
View File
@@ -46,6 +46,7 @@ runs:
repository: oss-review-toolkit/ort-config
ref: "main"
path: ".ort-config"
persist-credentials: false
- name: Setup ORT Config
id: setup-ort-config
+2
View File
@@ -33,6 +33,8 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install ShellCheck
run: |
+50 -43
View File
@@ -37,30 +37,31 @@ jobs:
if: github.ref_type != 'branch'
run: |
gh release create \
--repo ${{ github.repository }} \
--title ${{ github.ref_name }} \
--repo "$GITHUB_REPOSITORY" \
--title "$GITHUB_REF_NAME" \
--notes '' \
--draft \
${{ github.ref_name }}
"$GITHUB_REF_NAME"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# zizmor: ignore[artipacked]
if: github.ref_type == 'branch'
- name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch'
run: |
ref_name=${{ github.ref_name }}-latest
ref_name="${GITHUB_REF_NAME}-latest"
if ! gh release view $ref_name; then
if ! gh release view "$ref_name"; then
gh release create \
--latest=false \
--title $ref_name \
--notes "Automated release for latest ${{ github.ref_name }}." \
$ref_name
--title "$ref_name" \
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
"$ref_name"
fi
git tag $ref_name --force
git push origin $ref_name --force
git tag "$ref_name" --force
git push origin "$ref_name" --force
build:
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
@@ -70,9 +71,6 @@ jobs:
fail-fast: true
matrix:
include:
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
@@ -80,8 +78,13 @@ jobs:
otp_version: "28.0"
build_docs: build_docs
- otp: 29
otp_version: "29.0"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
@@ -97,19 +100,19 @@ jobs:
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload Windows release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.build_docs
with:
name: Docs
@@ -122,7 +125,7 @@ jobs:
strategy:
fail-fast: true
matrix:
otp: [26, 27, 28]
otp: [27, 28, 29]
flavor: [windows, linux]
env:
@@ -136,20 +139,20 @@ jobs:
steps:
- name: "Download build"
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@db7a3a6bd3912025c705162fb7475389f5b69ec6 # v1.0.0
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
@@ -179,7 +182,7 @@ jobs:
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
@@ -203,10 +206,12 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -221,7 +226,7 @@ jobs:
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
@@ -241,15 +246,19 @@ jobs:
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
done
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
env:
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
- name: "Assemble Release SBoM Artifacts"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "SBoM"
path: |
@@ -259,7 +268,7 @@ jobs:
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "Attestations"
path: "attestations/*.sigstore"
@@ -273,7 +282,7 @@ jobs:
contents: write
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
@@ -283,14 +292,14 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.ref_type }}" == "branch" ]; then
tag=${{ github.ref_name }}-latest
if [ "$GITHUB_REF_TYPE" == "branch" ]; then
tag="${GITHUB_REF_NAME}-latest"
else
tag="${{ github.ref_name }}"
tag="$GITHUB_REF_NAME"
fi
gh release upload \
--repo ${{ github.repository }} \
--repo "$GITHUB_REPOSITORY" \
--clobber \
"$tag" \
elixir-otp-*.zip \
@@ -324,7 +333,7 @@ jobs:
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -335,12 +344,10 @@ jobs:
- name: Upload Precompiled to S3
run: |
ref_name=${{ github.ref_name }}
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
dest=${zip/elixir/${GITHUB_REF_NAME}}
surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
@@ -349,16 +356,16 @@ jobs:
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${ref_name} >> purge_keys.txt
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
fi
done
- name: Upload Docs to S3
run: |
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
unzip Docs.zip
@@ -379,7 +386,7 @@ jobs:
- name: Update builds txt
run: |
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name=${{ github.ref_name }}
ref_name="$GITHUB_REF_NAME"
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
+4 -2
View File
@@ -18,8 +18,10 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: "27.3"
elixir-version: "1.18.3"
@@ -29,4 +31,4 @@ jobs:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
run: |
elixir .github/workflows/notify.exs ${{ github.ref_name }}
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
+13 -7
View File
@@ -25,10 +25,12 @@ runs:
- name: Build Elixir Release
shell: bash
run: |
run: | # zizmor: ignore[github-env]
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
echo "$PWD/bin" >> $GITHUB_PATH
env:
INPUT_OTP: ${{ inputs.otp }}
- name: Install NSIS
shell: bash
@@ -39,15 +41,18 @@ runs:
- name: Build Elixir Windows Installer
shell: bash
run: |
export OTP_VERSION=${{ inputs.otp_version }}
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
export OTP_VERSION="$INPUT_OTP_VERSION"
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
env:
INPUT_OTP: ${{ inputs.otp }}
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
run: | # zizmor: ignore[github-env]
if [ "$GITHUB_REF_NAME" = "main" ]; then
ref=main
else
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
@@ -59,6 +64,7 @@ runs:
repository: elixir-lang/ex_doc
ref: ${{ env.EX_DOC_REF }}
path: ex_doc
persist-credentials: false
- name: Build ex_doc
if: ${{ inputs.build_docs }}
shell: bash
+213 -45
View File
@@ -6,31 +6,13 @@
# Changelog for Elixir v1.20
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
## Type system improvements
This release includes type inference of all constructs.
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
### Type inference of function calls
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
```elixir
def add_foo_and_bar(data) do
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
### Type inference of guards
@@ -68,7 +50,44 @@ def example(x) when tuple_size(x) < 3
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
### Complete typing of maps keys
### Whole-body type inference
Elixir also performs inference based on the function body itself. Take the following code:
```elixir
def add_foo_and_bar(data) do
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
### Typing across clauses
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
```elixir
case System.get_env("SOME_VAR") do
nil -> :not_found
value -> {:ok, String.upcase(value)}
end
```
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
### Typing of atom and domain keys in maps
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
@@ -158,50 +177,195 @@ The code above has a type violation, which is now caught by the type system:
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
## v1.20.0-rc.1 (2026-01-13)
## Compile-time improvements
### 1. Bug fixes
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
#### Elixir
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
* [Kernel] Improve the performance of the type system when working with large unions of open maps
* [Kernel] Do not crash on map types with struct keys when performing type operations
* [Kernel] Mark the outcome of bitstring types as dynamic
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable
* Errors during compilation may have less precise stacktraces
## v1.20.0-rc.0 (2026-01-09)
* Anonymous functions within `defmodule` can have only up to 20 arguments.
If this is an issue, you can use maps or tuples to group the data.
Note the functions themselves inside `defmodule`, such as the ones defined
inside `def` and friends, can still have up to 255 arguments
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
## v1.20.2 (2026-06-23)
### 1. Enhancements
#### Elixir
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Enum] Add `Enum.min_max` sorter
* [Integer] Add `Integer.ceil_div/2`
* [IO] Add `IO.iodata_empty?/1`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Warn on unused requires
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
#### ExUnit
### 2. Bug fixes
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### Elixir
* [Kernel] Fix binary comprehensions with sizes when options such as `:uniq` or `:into` are used
* [Kernel] Improve compiler error messages when `quote` with `unquote` is used inside a pattern or guard
* [Kernel] Restore the compiler optimization of `Kernel.put_elem/3` to emit `:erlang.setelement/3`
* [Module] Fix type checking when applying an empty function type
* [Module] Fix type checking of bitstring patterns that reuse variables
* [Module] Fix type information for `__info__(:struct)` to include the `:required` key
* [Module] Fix type operations on map and optional keys during difference/intersection, including open keys and empty intersections
* [Module] Fix typing of list types involving dynamic or empty lists
* [Module] Include bitstrings as a possible domain key in the type system
* [Module] Preserve file metadata from each clause in type system warnings
* [Module] Fix type warnings for protocol implementations whose protocol module defines additional callbacks
* [Module] Raise clearer type checking errors when an expected struct is removed or redefined during compilation
#### Mix
* [mix deps] Support filtering `mix deps` output
* [mix compile] Avoid unnecessary umbrella recompilation when a path dependency's manifest is newer but unchanged
* [mix deps.compile] Recompile fetched dependencies when their compile-time environment changes
## v1.20.1 (2026-06-09)
### 1. Security
#### Elixir
* [Version] Limit integer components in Version to 14 decimal bytes, to avoid parsing too large integers from untrusted user input. We strongly advise developers parsing versions from user input to limit the data size given to the `Version` module (CVE-2026-49762, GHSA-w2h8-8x3g-278p)
### 2. Bug fixes
#### Elixir
* [Calendar] Cap width in `Calendar.strftime/2` to 1024 characters
* [Code] Ensure `Code.require_file` releases the file if compilation fails
* [Kernel] Fix documentation generation to use the correct version in search
#### Mix
* [mix archive.install] Validate paths and files when extracting archives
* [mix format] Honor `--no-compile` option when loading plugins
## v1.20.0 (2026-06-03)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### EEx
* [EEx] Optimize compiler by flattening expr list only once
#### Elixir
* [Base] Optimize Base validation functions by using SWAR techniques
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Code] Add `:dbg_callback` option to eval functions
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [Enum] Add `Enum.min_max` sorter
* [File] Add support for `[:raw]` opts in `File.read/2`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Float] Optimize `Float.round/2` by avoiding big integers
* [Inspect] Increase inspect limit to help print deeply nested data structures
* [Inspect] Support printing Erlang records (using Erlang notation)
* [Integer] Add `Integer.ceil_div/2`
* [Integer] Add `Integer.popcount/1`
* [IO] Add `IO.iodata_empty?/1`
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
* [Kernel] Detect and warn on redundant clauses
* [Kernel] Perform type inference across applications
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Kernel] Warn on unused requires
* [List] Add `List.first!/1` and `List.last!/1`
* [Module] Purge and delete modules if `after_compile/2` callback fails
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
* [Process] Add `Process.get_label/1`
* [Registry] Switch `keys: {:duplicate, :key}` to `ordered_set` with composite keys
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
* Add Software Bill of Materials guide to the Documentation
#### ExUnit
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### IEx
* [IEx] Optimize autocompleting modules
* [IEx.Helpers] Add `source/1`
#### Mix
* [mix app.tree] Support `--output` option
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
* [mix deps] Support filtering `mix deps` output
* [mix deps.tree] Support `--output` option
* [mix format] Support `--no-compile` option
* [mix help] Support printing docs for types and callbacks
* [mix source] Add `mix source MODULE` to print or open a given module/function location
* [mix test] Add `mix test --dry-run`
### 2. Potential breaking changes
#### Elixir
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note that while Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
* [Kernel] Disallow raw CR line ending in strings, comments, and after `?` for security reasons
* [Kernel] `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
### 3. Hard deprecations
### 3. Bug fixes
#### Elixir
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Allowing preserving directory permissions in `File.cp_r/3`
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
* [File] Fix `File.Stream`'s `Enumerable.count` for files without trailing newline
* [File] Warn when defining `@type record()` for Erlang/OTP 29
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
* [Kernel] Reject negative Duration in `to_timeout/1`
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
* [String] Support empty pattern list in `String.count/2`
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
#### ExUnit
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
#### IEx
* [IEx] Ensure pry works across remote nodes
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
#### Logger
* [Logger] Persist log level to app env in `Logger.configure/1`
#### Mix
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
* [mix compile] Add a build lock around protocol consolidation in umbrellas
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
* [mix test] Respect `--raise` when `mix test --warnings-as-errors` passes with warnings
### 4. Hard deprecations
#### Elixir
@@ -214,6 +378,10 @@ The type system was made possible thanks to a partnership between [CNRS](https:/
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
#### Mix
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
## v1.19
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
+68 -25
View File
@@ -99,15 +99,11 @@ case you are looking for some examples:
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
All pull requests must be reviewed before being merged into the repository.
If changes are necessary, the team will leave appropriate comments requesting
changes to the code. Unfortunately, we cannot guarantee a pull request will
be merged, even when modifications are requested, as the Elixir team will
re-evaluate the contribution as it changes.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
@@ -118,6 +114,29 @@ into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir.
After cloning and compiling Elixir, run:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
# and generate HTML and EPUB documents:
make docs
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
## Licensing and Compliance Requirements
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
@@ -181,25 +200,49 @@ affecting **all external contributors**:
See <https://developercertificate.org/> for a copy of the Developer Certificate
of Origin license.
## Building documentation
## Using AI and coding agents
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir.
While we allow the use of AI on contributions and discussions, please be mindful
when doing so. Generally speaking, Elixir maintainers already have access to AI
(like many other developers). Therefore, if we need the feedback or help of a
coding agent, we can request so ourselves. For this reason, we often find
the point of view of the human behind the agent more valuable.
After cloning and compiling Elixir, run:
That said, here are examples of how one might (or might not) use AI and coding
agents in Elixir spaces:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
* When it comes to discussions, using AI to help express yourself is welcome,
but avoid directly copy and pasting AI generated content. If there is a language
barrier, use AI to translate, review, and improve your text, but do not use AI
to respond on your behalf.
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
* Do not use coding agents to tackle existing issues unless they have the
"Contributions Welcome" label.
# and generate HTML and EPUB documents:
make docs
```
* If you request a feature on the mailing list and it is accepted, you may
use coding agents to implement it, as long as it follows the AI Contributions
guidelines below.
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
* When automating AI usage on the Elixir codebase for performance improvements
or security fixes, pair it with a separate set of agents whose job is to argue
against and try to invalidate any proposed change. And treat their approval as
advisory: a human must still validate it before opening issues or pull requests.
If any code is written by AI, then you must follow the guidelines below.
### AI contributions
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
Certificate of Origin (DCO). The human submitter is responsible for:
* Reviewing all AI-generated code
* Ensuring compliance with licensing requirements
* Adding their own Signed-off-by tag to certify the DCO
* Taking full responsibility for the contribution
* Disclosing use of AI for comments and code contributions
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
AI in the development process. Contributions should include an Assisted-by tag in the
following format:
Assisted-by: AGENT_NAME:MODEL_VERSION
+3 -3
View File
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
ERLC := erlc -I lib/elixir/include
@@ -32,9 +32,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
#==> Functions
define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 26)])' -s erlang halt | grep -q '^true'; \
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 27)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 26.0 is required to build Elixir"; \
echo "At least Erlang/OTP 27.0 is required to build Elixir"; \
exit 1; \
fi
endef
+4 -3
View File
@@ -122,9 +122,10 @@ variable `ERL_COMPILER_OPTIONS=deterministic`.
Contributions to Elixir are always welcome! Before you get started, please check
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
guidelines on how to set up your environment, run the test suite, format your
code, and submit pull requests. We also include information on our review
process, licensing requirements, and helpful tips to ensure a smooth
contribution experience.
code, and submit pull requests.
Note you must disclose the use of coding agents and AI written code in your
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
## Development links
+2 -3
View File
@@ -12,12 +12,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Development
1.19 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.20.0-rc.1
1.20.2
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.0-rc.1
ELIXIR_VERSION=1.20.2
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.20.0-rc.1
set ELIXIR_VERSION=1.20.2
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+25 -12
View File
@@ -303,7 +303,7 @@ defmodule EEx.Compiler do
file: file,
source: source,
line: line,
quoted: [],
quoted: %{},
parser_options: [indentation: indentation] ++ parser_options,
indentation: indentation
}
@@ -347,7 +347,7 @@ defmodule EEx.Compiler do
state.parser_options
expr = Code.string_to_quoted!(chars, options)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
buffer = handle_expr(buffer, mark, expr, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -366,7 +366,7 @@ defmodule EEx.Compiler do
rest,
state.engine.handle_begin(buffer),
[{contents, start_line, start_column} | scope],
%{state | quoted: [], line: line}
%{state | quoted: %{}, line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
@@ -376,7 +376,7 @@ defmodule EEx.Compiler do
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
buffer = handle_expr(buffer, mark, contents, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -410,7 +410,7 @@ defmodule EEx.Compiler do
) do
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
options = [file: state.file, line: line, column: column] ++ state.parser_options
tuples = Code.string_to_quoted!(wrapped, options)
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options)
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
end
@@ -426,7 +426,7 @@ defmodule EEx.Compiler do
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
message = "expected a closing '<% end %>' for block expression in EEx"
expr_meta = non_whitespace_meta(content, line, column, state)
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state)
syntax_error!(message, expr_meta, state)
end
@@ -443,10 +443,10 @@ defmodule EEx.Compiler do
defp wrap_expr(current, line, buffer, chars, state) do
new_lines = List.duplicate(?\n, line - state.line)
key = length(state.quoted)
placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
count = current ++ placeholder ++ new_lines ++ chars
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
key = map_size(state.quoted)
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"]
count = [current, placeholder, new_lines, chars]
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))}
{count, new_state}
end
@@ -479,8 +479,7 @@ defmodule EEx.Compiler do
# Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
{^key, value} = List.keyfind(quoted, key, 0)
value
Map.fetch!(quoted, key)
end
defp insert_quoted({left, line, right}, quoted) do
@@ -513,6 +512,20 @@ defmodule EEx.Compiler do
column: meta.column
end
defp handle_expr(buffer, mark, expr, meta, state) do
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
rescue
e in EEx.SyntaxError ->
reraise %{
e
| file: e.file || state.file,
line: e.line || meta.line,
column: e.column || meta.column,
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
},
__STACKTRACE__
end
defp code_snippet(source, indentation, meta) do
line_start = max(meta.line - 3, 1)
line_end = meta.line
+12
View File
@@ -575,6 +575,18 @@ defmodule EExTest do
EEx.compile_string("foo <%= bar", file: "my_file.eex")
end
end
test "unsupported marker error carries template location metadata" do
error =
assert_raise EEx.SyntaxError, fn ->
EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
end
assert error.file == "sample.eex"
assert error.line == 7
assert error.column == 1
assert Exception.message(error) =~ "sample.eex:7:1:"
end
end
describe "warnings" do
+10 -5
View File
@@ -8,8 +8,13 @@
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("eex")
ExUnit.start(
trace: !!System.get_env("TRACE"),
include: line_include,
exclude: line_exclude
)
maybe_seed_opt = if seed = System.get_env("SEED"), do: [seed: String.to_integer(seed)], else: []
ex_unit_opts =
[
trace: !!System.get_env("TRACE"),
include: line_include,
exclude: line_exclude
] ++ maybe_seed_opt
ExUnit.start(ex_unit_opts)
+5
View File
@@ -1091,6 +1091,11 @@ defmodule Access do
defp normalize_range(range, _list), do: range
defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
when index > last do
{:lists.reverse(gets), :lists.reverse(updates, rest)}
end
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
if index in range do
case next.(head) do
+278 -54
View File
@@ -155,6 +155,255 @@ defmodule Base do
for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>>
end
# SWAR (SIMD Within A Register) fast paths for valid16?/2 and valid32?/2
# (non-hex). Each chunk of 8 bytes is validated in one guard: 7 bytes via
# bitwise arithmetic on a single 56-bit integer, plus a per-byte range
# check for the 8th byte. 56 bits is the largest width that fits in a BEAM
# small int on 64-bit (fixnum range is 59-bit signed); at 64 bits every
# `w + 0x80..` would allocate a bignum on the heap and the optimisation
# would collapse. See https://github.com/erlang/otp/pull/10938 for the
# corresponding pattern in OTP.
@swar_mask80 0x80808080808080
# Per-range SWAR constants, broadcast across 7 lanes. Naming convention:
# @swar_ge_X = 0x80 - X → high bit of `(w + @swar_ge_X)` lane is set
# iff that byte is ≥ X
# @swar_gt_X = 0x7F - X → high bit of `(w + @swar_gt_X)` lane is set
# iff that byte is > X
# A byte is in range [lo, hi] iff
# `bxor(w + @swar_ge_lo, w + @swar_gt_hi)` has its high bit set.
@swar_ge_0 0x50505050505050
@swar_gt_9 0x46464646464646
@swar_ge_2 0x4E4E4E4E4E4E4E
@swar_gt_7 0x48484848484848
@swar_ge_A 0x3F3F3F3F3F3F3F
@swar_gt_F 0x39393939393939
@swar_gt_V 0x29292929292929
@swar_gt_Z 0x25252525252525
@swar_ge_a 0x1F1F1F1F1F1F1F
@swar_gt_f 0x19191919191919
@swar_gt_v 0x09090909090909
@swar_gt_z 0x05050505050505
# For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we
# extend the digit range to [0x2F, 0x39], which absorbs '/' into one range
# check — saves one Mycroft singleton. Trick lifted from
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
@swar_ge_slash 0x51515151515151
# Mycroft zero-byte detection for base64 singletons (+, -, _).
# Per lane: high bit set iff `bxor(w, K*ones) - 0x01..01` has its high bit
# set, i.e. that byte's V value was 0 → original byte was K. Simplified
# (no `bnot V` term) — for ASCII-gated `w`, borrow-propagation false
# positives only occur for adjacent bytes that happen to equal `K xor 0x01`,
# which is outside the base64 alphabet, so it never matters here.
# Pattern follows https://github.com/elixir-lang/elixir/pull/15255.
@swar_mask01 0x01010101010101
@swar_plus_x7 0x2B2B2B2B2B2B2B
@swar_dash_x7 0x2D2D2D2D2D2D2D
@swar_under_x7 0x5F5F5F5F5F5F5F
# Per-byte validity checks (used in both the SWAR clauses for the 8th byte
# of each stride and in the body of the sub-8-byte tail clauses).
@compile {:inline,
valid_char16upper?: 1,
valid_char16lower?: 1,
valid_char16mixed?: 1,
valid_char32upper?: 1,
valid_char32lower?: 1,
valid_char32mixed?: 1,
valid_char32hexupper?: 1,
valid_char32hexlower?: 1,
valid_char32hexmixed?: 1,
valid_char64base?: 1,
valid_char64url?: 1,
valid_word16upper?: 1,
valid_word16lower?: 1,
valid_word16mixed?: 1,
valid_word32upper?: 1,
valid_word32lower?: 1,
valid_word32mixed?: 1,
valid_word32hexupper?: 1,
valid_word32hexlower?: 1,
valid_word32hexmixed?: 1,
valid_word64base?: 1,
valid_word64url?: 1}
defp valid_char16upper?(c), do: c in ?0..?9 or c in ?A..?F
defp valid_char16lower?(c), do: c in ?0..?9 or c in ?a..?f
defp valid_char16mixed?(c), do: c in ?0..?9 or c in ?A..?F or c in ?a..?f
defp valid_char32upper?(c), do: c in ?A..?Z or c in ?2..?7
defp valid_char32lower?(c), do: c in ?a..?z or c in ?2..?7
defp valid_char32mixed?(c), do: c in ?A..?Z or c in ?a..?z or c in ?2..?7
# Most common range first — letters dominate (22/32) over digits (10/32)
# in hex base32, so letters go first in the OR short-circuit.
defp valid_char32hexupper?(c), do: c in ?A..?V or c in ?0..?9
defp valid_char32hexlower?(c), do: c in ?a..?v or c in ?0..?9
defp valid_char32hexmixed?(c), do: c in ?A..?V or c in ?a..?v or c in ?0..?9
defp valid_char64base?(c),
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?+ or c == ?/
defp valid_char64url?(c),
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?- or c == ?_
# SWAR 7-byte word validity. Structure for each function:
# 1. ASCII gate `band(w, MASK80) == 0` — every byte < 0x80 so the
# additions below cannot carry across lanes.
# 2. "Each byte is in range A OR range B (OR range C)" gate — OR per-
# range XOR masks (high bit set in lane iff byte in that range), AND
# with MASK80, demand all 7 high bits set.
defp valid_word16upper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_F)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word16lower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_a, w + @swar_gt_f)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word16mixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_F)
),
bxor(w + @swar_ge_a, w + @swar_gt_f)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32upper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32lower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_a, w + @swar_gt_z),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32mixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexupper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_V)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexlower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_a, w + @swar_gt_v)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexmixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_V)
),
bxor(w + @swar_ge_a, w + @swar_gt_v)
),
@swar_mask80
) == @swar_mask80
end
# base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons.
# For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as
# part of one range (Lemire merge), leaving only '+' as a Mycroft singleton.
# For url, the singletons '-' and '_' are detected via two Mycroft terms.
defp valid_word64base?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_slash, w + @swar_gt_9)
),
bxor(w, @swar_plus_x7) - @swar_mask01
),
@swar_mask80
) == @swar_mask80
end
defp valid_word64url?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_0, w + @swar_gt_9)
),
bor(
bxor(w, @swar_dash_x7) - @swar_mask01,
bxor(w, @swar_under_x7) - @swar_mask01
)
),
@swar_mask80
) == @swar_mask80
end
@doc """
Encodes a binary string into a base 16 encoded string.
@@ -371,25 +620,21 @@ defmodule Base do
decode_name = :"decode16#{base}!"
validate_name = :"validate16#{base}?"
valid_char_name = :"valid_char16#{base}?"
valid_word_name = :"valid_word16#{base}?"
{min, decoded} = to_decode_list.(alphabet)
# SWAR fast path: 7 bytes per stride, validated entirely via
# `valid_word16<base>?` in the body. The `and` short-circuits when SWAR
# fails on any byte. Tail bytes (1-6 leftover) recurse through the
# single-byte clause below.
defp unquote(validate_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_name)(rest)
defp unquote(validate_name)(<<>>), do: true
defp unquote(validate_name)(<<c1, c2, rest::binary>>) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(validate_name)(rest)
end
defp unquote(validate_name)(<<_char, _rest::binary>>), do: false
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(validate_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest)
defp unquote(decode_name)(char) do
index = char - unquote(min)
@@ -761,23 +1006,19 @@ defmodule Base do
validate_name = :"validate64#{base}?"
validate_main_name = :"validate_main64#{validate_name}?"
valid_char_name = :"valid_char64#{base}?"
valid_word_name = :"valid_word64#{base}?"
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?`
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
# 8-byte-multiple `main`) recurses through the single-byte clause.
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_main_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -863,13 +1104,6 @@ defmodule Base do
end
end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do
index = char - unquote(min)
@@ -1425,21 +1659,18 @@ defmodule Base do
valid_char_name = :"valid_char32#{base}?"
{min, decoded} = to_decode_list.(alphabet)
# SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?`
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
# 8-byte-multiple `main`) recurses through the single-byte clause.
valid_word_name = :"valid_word32#{base}?"
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_main_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -1519,13 +1750,6 @@ defmodule Base do
end
end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do
index = char - unquote(min)
+9 -2
View File
@@ -3,6 +3,8 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar do
@strftime_max_width 1024
@moduledoc """
This module defines the responsibilities for working with
calendars, dates, times and datetimes in Elixir.
@@ -529,6 +531,7 @@ defmodule Calendar do
* `%`: indicates the start of a formatted section
* `<padding>`: set the padding (see below)
* `<width>`: a number indicating the minimum size of the formatted section
(maximum #{@strftime_max_width})
* `<format>`: the format itself (see below)
### Accepted padding options
@@ -667,9 +670,13 @@ defmodule Calendar do
end
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
new_width = (width || 0) * 10 + (digit - ?0)
width = (width || 0) * 10 + (digit - ?0)
parse_modifiers(rest, new_width, pad, parser_data)
if width > @strftime_max_width do
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
end
parse_modifiers(rest, width, pad, parser_data)
end
# set default padding if none was specified
+16 -7
View File
@@ -59,11 +59,20 @@ defmodule Date.Range do
end
# TODO: Remove me on v2.0
def member?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range,
date
) do
member? =
quote generated: true do
member?(
%{
__struct__: Date.Range,
first_in_iso_days: var!(first_days),
last_in_iso_days: var!(last_days)
} =
var!(date_range),
var!(date)
)
end
def unquote(member?) do
step = if first_days <= last_days, do: 1, else: -1
member?(Map.put(date_range, :step, step), date)
end
@@ -218,11 +227,11 @@ defmodule Date.Range do
defimpl Inspect do
import Kernel, except: [inspect: 2]
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
end
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
end
+8 -5
View File
@@ -671,7 +671,7 @@ defmodule Calendar.ISO do
{{year, month, day}, time_from_day_fraction(day_fraction)}
{extra_days, day_fraction} ->
base_days = date_to_iso_days(year, month, day)
base_days = valid_date_to_iso_days(year, month, day)
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
end
@@ -892,12 +892,15 @@ defmodule Calendar.ISO do
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_iso_days(0, 1, 1), do: 0
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
def date_to_iso_days(year, month, day) do
ensure_day_in_month!(year, month, day)
valid_date_to_iso_days(year, month, day)
end
defp valid_date_to_iso_days(0, 1, 1), do: 0
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
defp valid_date_to_iso_days(year, month, day) do
y = if month <= 2, do: year - 1, else: year
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
year_of_era = y - era * @years_per_era
@@ -1875,7 +1878,7 @@ defmodule Calendar.ISO do
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
valid_date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
+36 -30
View File
@@ -715,16 +715,18 @@ defmodule NaiveDateTime do
"""
@spec to_date(Calendar.naive_datetime()) :: Date.t()
def to_date(%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
}) do
def to_date(
%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
} = _naive_datetime
) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@@ -741,16 +743,18 @@ defmodule NaiveDateTime do
"""
@spec to_time(Calendar.naive_datetime()) :: Time.t()
def to_time(%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
def to_time(
%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = _naive_datetime
) do
%Time{
hour: hour,
minute: minute,
@@ -1146,16 +1150,18 @@ defmodule NaiveDateTime do
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}) do
def to_gregorian_seconds(
%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
} = _naive_datetime
) do
{days, day_fraction} =
calendar.naive_datetime_to_iso_days(
year,
+122 -71
View File
@@ -286,19 +286,25 @@ defmodule Code do
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (atom() -> term()),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
emit_warnings: boolean()
]
@typedoc """
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
Options for evaluation environment, accepted by `env_for_eval/1`.
"""
@type env_eval_opts :: [
file: binary(),
line: pos_integer(),
module: module(),
prune_binding: boolean()
]
@type env_eval_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:module, module()}
@typedoc """
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
and `eval_quoted_with_env/4`.
"""
@type eval_opt ::
{:prune_binding, boolean()}
| {:dbg_callback, {module(), atom(), list()}}
@boolean_compiler_options [
:docs,
@@ -312,7 +318,12 @@ defmodule Code do
@available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
[
:on_undefined_variable,
:infer_signatures,
:no_warn_undefined,
:module_definition
]
@doc """
Lists all required files.
@@ -568,9 +579,11 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`. Additionally, you may
also pass an environment as second argument, so the evaluation happens
within that environment.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
as third argument, so the evaluation happens within that environment.
## Return
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -600,7 +613,7 @@ defmodule Code do
iex> Enum.sort(binding)
[a: 3, b: 2]
For convenience, you can pass `__ENV__/0` as the `opts` argument and
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
all imports, requires and aliases defined in the current environment
will be automatically carried over:
@@ -612,21 +625,28 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_string(string, binding \\ [], opts_or_env \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do
validated_eval_string(string, binding, env)
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
end
def eval_string(string, binding, opts) when is_list(opts) do
validated_eval_string(string, binding, opts)
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
end
defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
defp validate_binding(binding) when is_list(binding), do: binding
defp validate_binding(binding) do
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
end
defp validated_eval_string(string, binding, env, opts) do
%{line: line, file: file} = env
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
{value, binding}
end
@@ -1129,7 +1149,8 @@ defmodule Code do
returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of arguments and return types.
The options are described under `env_for_eval/1`.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`.
## Examples
@@ -1151,11 +1172,20 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
end
def eval_quoted(quoted, binding, opts) when is_list(opts) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
end
defp eval_quoted(quoted, binding, env, opts) do
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
{value, binding}
end
@@ -1183,14 +1213,9 @@ defmodule Code do
* `:module` - the module to run the environment on
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
"""
@doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """
@@ -1204,11 +1229,19 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`.
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
used in the evaluated code. It must be a `{module, function, args}`
tuple, see `dbg/2` for more details.
"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
@@ -1286,7 +1319,7 @@ defmodule Code do
and keyword lists.
The encoder function will receive the atom name (as a binary) and a
keyword list with the current file, line and column. It must return
keyword list with the current line and column. It must return
`{:ok, token :: term} | {:error, reason :: binary}`.
The encoder function is supposed to create an atom from the given
@@ -1325,14 +1358,7 @@ defmodule Code do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
{:ok, tokens} ->
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, _error_msg} = error ->
error
end
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
end
@doc """
@@ -1408,8 +1434,7 @@ defmodule Code do
Process.put(:code_formatter_comments, [])
opts = [preserve_comments: &preserve_comments/5] ++ opts
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, opts) do
with {:ok, forms} <- :elixir.string_to_quoted(charlist, line, column, file, opts) do
comments = Enum.reverse(Process.get(:code_formatter_comments))
{:ok, forms, comments}
end
@@ -1609,13 +1634,19 @@ defmodule Code do
nil
:proceed ->
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
try do
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
:elixir_code_server.cast({:required, file})
loaded
:elixir_code_server.cast({:required, file})
loaded
catch
kind, reason ->
:elixir_code_server.call({:release, file})
:erlang.raise(kind, reason, __STACKTRACE__)
end
end
end
@@ -1710,9 +1741,6 @@ defmodule Code do
Available options are:
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled
module. This option can also be overridden per module using the `@compile`
directive. Defaults to `true`.
@@ -1728,6 +1756,9 @@ defmodule Code do
via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files.
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added.
Defaults to `false`.
@@ -1739,24 +1770,37 @@ defmodule Code do
should be using during type inference. When `false`, it disables module-local
signature inference used when type checking remote calls to the compiled
module. Type checking will be executed regardless of the value of this option.
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
Mix projects will set this option to your dependencies list in dev/prod, and
it will disable this option during test (as there is typically no need to infer
signature for test files). Outside of Mix projects, it defaults to `[:elixir]`.
When setting this option, we recommend running `mix clean` so the modules can be
recompiled with the new behaviour. `mix test` automatically disables this option
via the `:test_elixirc_options` project configuration, as there is typically no
need to infer signatures for test files.
* `:module_definition` (since v1.20.0) - stores if the module definition should
be `:compiled` (the default) or `:interpreted`. Note this does not affect the
`.beam` file written to disk, only how the contents inside `defmodule` are
executed. Using the `:interpreted` mode may offer better compilation times for
large projects, especially on machines with high core count, however, it comes
with some downsides:
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* Errors during compilation may have less precise stacktraces
* Anonymous functions within `defmodule` can have only up to 20 arguments.
If this is an issue, you can use maps or tuples to group the data.
Note the functions themselves inside `defmodule`, such as the ones defined
inside `def` and friends, can still have up to 255 arguments
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
tuples that will not emit warnings that the module or function does not exist
at compilation time. Pass atom `:all` to skip warning for all undefined
functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
compilation. See the module docs for more information. Defaults to `[]`.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
When `:raise` (the default), undefined variables will trigger a compilation
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
* `:parser_options` (since v1.10.0) - a keyword list of options to be given
to the parser when compiling files. It accepts the same options as
@@ -1767,14 +1811,12 @@ defmodule Code do
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
latter is used for other purposes beyond compilation.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
When `:raise` (the default), undefined variables will trigger a compilation
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
compilation. See the module docs for more information. Defaults to `[]`.
It always returns `:ok`. Raises an error for invalid options.
@@ -1814,6 +1856,15 @@ defmodule Code do
:ok
end
def put_compiler_option(:module_definition, value) do
if value not in [:interpreted, :compiled] do
raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}"
end
:elixir_config.put(:module_definition, value)
:ok
end
def put_compiler_option(:infer_signatures, value) do
value =
cond do
+40 -5
View File
@@ -204,6 +204,7 @@ defmodule Code.Fragment do
| {:local_arity, charlist}
| {:local_call, charlist}
| {:anonymous_call, inside_caller}
| {:capture_arg, charlist}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:operator_arity, charlist}
@@ -1234,14 +1235,43 @@ defmodule Code.Fragment do
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
(see "Tracking sigils" section below)
## Tracking sigils
The `:preserve_sigils` option can be used to track cursor positions inside
a sigil.
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
as the second argument:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
In case the sigil is completed and has zero or more modifiers, the cursor will
be nested in the list, with all previous delimiters specified:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
If the cursor is after the sigil, then it is discarded as everything else:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
{:ok, {:__cursor__, [line: 1], []}}
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1261,7 +1291,10 @@ defmodule Code.Fragment do
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
@@ -1269,7 +1302,7 @@ defmodule Code.Fragment do
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
@@ -1288,10 +1321,12 @@ defmodule Code.Fragment do
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
:elixir.tokens_to_quoted(tokens, file, opts)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
{:error, :elixir_tokenizer.format_error(info)}
end
end
+16 -9
View File
@@ -68,7 +68,7 @@ defmodule Code.Normalizer do
# Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state)
normalize_bitstring(quoted, state, false)
end
# Atoms with interpolations
@@ -89,13 +89,7 @@ defmodule Code.Normalizer do
normalize_literal(:utf8, [], state)
end
string =
if state.escape do
normalize_bitstring(string, state, true)
else
normalize_bitstring(string, state)
end
string = normalize_bitstring(string, state, state.escape)
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end
@@ -118,6 +112,7 @@ defmodule Code.Normalizer do
end
end)
parts = maybe_add_trailing_newline(call_meta, parts, state)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else
normalize_call(quoted, state)
@@ -405,7 +400,8 @@ defmodule Code.Normalizer do
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
parts = maybe_add_trailing_newline(meta, parts, state)
meta = patch_meta_line(meta, state.parent_meta)
parts =
@@ -427,6 +423,17 @@ defmodule Code.Normalizer do
{:<<>>, meta, parts}
end
defp maybe_add_trailing_newline(meta, parts, state) do
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
last = List.last(parts),
true <- is_binary(last) and not String.ends_with?(last, "\n") do
[_last | rest] = Enum.reverse(parts)
Enum.reverse([last <> "\n" | rest])
else
_ -> parts
end
end
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
Enum.map(parts, fn
{:"::", interpolation_meta,
+16
View File
@@ -69,6 +69,22 @@ defprotocol Collectable do
iex> Enum.into([1, 2, 3], MapSet.new())
MapSet.new([1, 2, 3])
## Halting
The `:halt` flag will be given whenever the collection won't
terminate correctly and must be used to clean up existing resources
(such as sockets, file handles, etc).
Note it is not guaranteed that the accumulator given to halt will
be the latest version of the accumulator returned by a previous call
with `{:cont, elem}`. Therefore, you must track the collected results
within the resource you intend to halt.
This is by design: ensuring halt is always called with the latest
accumulator would make pure collectables (the ones that do not implement
halt) expensive. However, given the collectables that must implement halt
already need to track state, the burden of tracking the accumulator
across invocations is put on them.
"""
@type command :: {:cont, term} | :done | :halt
+13 -7
View File
@@ -377,21 +377,27 @@ defmodule Config do
end
end
defp validate!(config, file) do
Enum.all?(config, fn
defp validate!(config, file) when is_list(config) do
Enum.each(config, fn
{app, value} when is_atom(app) ->
if Keyword.keyword?(value) do
true
else
if not Keyword.keyword?(value) do
raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}"
end
_ ->
false
other ->
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
end)
config
end
defp validate!(config, file) do
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got: #{inspect(config)}"
end
end
+35 -25
View File
@@ -829,7 +829,7 @@ defmodule Enum do
"""
@spec dedup(t) :: list
def dedup(enumerable) when is_list(enumerable) do
dedup_list(enumerable, []) |> :lists.reverse()
dedup_list(enumerable)
end
def dedup(enumerable) do
@@ -2023,11 +2023,16 @@ defmodule Enum do
operators work by using this function.
"""
@spec member?(t, element) :: boolean
def member?(enumerable, element) when is_list(enumerable) do
def member?(enumerable, element) do
__in__(element, enumerable)
end
@doc false
def __in__(element, enumerable) when is_list(enumerable) do
:lists.member(element, enumerable)
end
def member?(enumerable, element) do
def __in__(element, enumerable) do
case Enumerable.member?(enumerable, element) do
{:ok, element} when is_boolean(element) ->
element
@@ -4507,19 +4512,9 @@ defmodule Enum do
# dedup
defp dedup_list([value | tail], acc) do
acc =
case acc do
[^value | _] -> acc
_ -> [value | acc]
end
dedup_list(tail, acc)
end
defp dedup_list([], acc) do
acc
end
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
defp dedup_list([]), do: []
## drop
@@ -5168,7 +5163,16 @@ defimpl Enumerable, for: Range do
end
# TODO: Remove me on v2.0
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
reduce =
quote generated: true do
reduce(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(acc),
var!(fun)
)
end
def unquote(reduce) do
step = if first <= last, do: 1, else: -1
reduce(Map.put(range, :step, step), acc, fun)
end
@@ -5191,12 +5195,12 @@ defimpl Enumerable, for: Range do
{:done, acc}
end
def member?(first..last//step, value) when is_integer(value) do
if step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
else
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
end
def member?(first..last//step, value) when is_integer(value) and step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
end
def member?(first..last//step, value) when is_integer(value) and step < 0 do
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
end
# TODO: Remove me on v2.0
@@ -5215,11 +5219,17 @@ defimpl Enumerable, for: Range do
end
def slice(first.._//step = range) do
{:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
{:ok, Range.size(range), &slice(first + &1 * step, step * &3, &2)}
end
# TODO: Remove me on v2.0
def slice(%{__struct__: Range, first: first, last: last} = range) do
slice =
quote generated: true do
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
end
def unquote(slice) do
step = if first <= last, do: 1, else: -1
slice(Map.put(range, :step, step))
end
+4 -4
View File
@@ -26,7 +26,7 @@ defmodule Exception do
@typedoc "The exception type"
@type t :: %{
required(:__struct__) => module,
required(:__exception__) => true,
required(:__exception__) => term,
optional(atom) => any
}
@@ -77,7 +77,7 @@ defmodule Exception do
@doc false
@deprecated "Use Kernel.is_exception/1 instead"
def exception?(term)
def exception?(%_{__exception__: true}), do: true
def exception?(%_{__exception__: _}), do: true
def exception?(_), do: false
@doc """
@@ -89,7 +89,7 @@ defmodule Exception do
return a descriptive error message instead.
"""
@spec message(t) :: String.t()
def message(%module{__exception__: true} = exception) do
def message(%module{__exception__: _} = exception) do
try do
module.message(exception)
rescue
@@ -123,7 +123,7 @@ defmodule Exception do
@spec normalize(:error, any, stacktrace) :: t
@spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var
def normalize(kind, payload, stacktrace \\ [])
def normalize(:error, %_{__exception__: true} = payload, _stacktrace), do: payload
def normalize(:error, %_{__exception__: _} = payload, _stacktrace), do: payload
def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace)
def normalize(_kind, payload, _stacktrace), do: payload
+105 -20
View File
@@ -400,6 +400,13 @@ defmodule File do
You can use `:file.format_error/1` to get a descriptive string of the error.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.read("hello.txt")
@@ -408,15 +415,24 @@ defmodule File do
File.read("non_existing.txt")
#=> {:error, :enoent}
"""
@spec read(Path.t()) :: {:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
def read(path) do
:file.read_file(IO.chardata_to_string(path))
@spec read(Path.t(), [exists_option]) ::
{:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
when exists_option: :raw
def read(path, opts \\ []) do
:file.read_file(IO.chardata_to_string(path), opts)
end
@doc """
Returns a binary with the contents of the given filename,
or raises a `File.Error` exception if an error occurs.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.read!("hello.txt")
@@ -425,9 +441,9 @@ defmodule File do
File.read!("non_existing.txt")
** (File.Error) could not read file "non_existing.txt": no such file or directory
"""
@spec read!(Path.t()) :: binary
def read!(path) do
case read(path) do
@spec read!(Path.t(), [exists_option]) :: binary when exists_option: :raw
def read!(path, opts \\ []) do
case read(path, opts) do
{:ok, binary} ->
binary
@@ -1087,7 +1103,7 @@ defmodule File do
@doc ~S"""
Copies the contents in `source` to `destination` recursively, maintaining the
source directory structure and modes.
source directory structure and regular file modes.
If `source` is a file or a symbolic link to it, `destination` must be a path
to an existent file, a symbolic link to one, or a path to a non-existent file.
@@ -1098,7 +1114,9 @@ defmodule File do
If the source is a file, it copies `source` to `destination`. If the `source`
is a directory, it copies the contents inside source into the `destination` directory.
If a file already exists in the destination, it invokes the optional `on_conflict`
For regular files, their respective file modes are preserved in the destination.
Directory modes are preserved only when `:preserve_directory_permissions` is `true`.
If a file already exists in the destination, it invokes the optional `:on_conflict`
callback given as an option. See "Options" for more information.
This function may fail while copying files, in such cases, it will leave the
@@ -1116,6 +1134,12 @@ defmodule File do
Special files such as device files, sockets, and named pipes are not copied.
Typical error reasons are:
* `:enoent` - `source` does not exist
* `:eisdir` - `source` is a file and `destination` is a directory
* `:einval` - `destination` is the same as or a subdirectory of `source`
## Options
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
@@ -1129,6 +1153,11 @@ defmodule File do
dereferenced and have their contents copied instead when set to `true`. If the dereferenced
files do not exist, than the operation fails. The default is `false`.
* `:preserve_directory_permissions` - (since v1.20.0) when `true`, the permissions of
source directories are copied to the destination directories after their contents are
written. This is useful when source directories are read-only or have restricted
permissions that must be preserved. The default is `false`.
## Examples
# Copies file "a.txt" to "b.txt"
@@ -1146,11 +1175,16 @@ defmodule File do
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
File.cp_r("non_existing.txt", "copy.txt")
#=> {:error, :enoent}
#=> {:error, :enoent, "non_existing.txt"}
# Copying into a subdirectory of source is not allowed
File.cp_r("src", "src/dest")
#=> {:error, :einval, "src/dest"}
"""
@spec cp_r(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean()
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) ::
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
@@ -1172,6 +1206,7 @@ defmodule File do
def cp_r(source, destination, options) when is_list(options) do
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
dereference? = Keyword.get(options, :dereference_symlinks, false)
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
source =
source
@@ -1183,9 +1218,25 @@ defmodule File do
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
case do_cp_r(source, destination, on_conflict, dereference?, []) do
{:error, _, _} = error -> error
res -> {:ok, res}
source_parts = source |> Path.expand() |> Path.split()
dest_parts = destination |> Path.expand() |> Path.split()
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
{:error, :einval, destination}
else
dereference = if dereference?, do: MapSet.new(), else: nil
case do_cp_r(
source,
destination,
on_conflict,
dereference,
preserve_directory_permissions?,
[]
) do
{:error, _, _} = error -> error
res -> {:ok, res}
end
end
end
@@ -1206,7 +1257,8 @@ defmodule File do
"""
@spec cp_r!(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean()
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) :: [binary]
def cp_r!(source, destination, options \\ []) do
case cp_r(source, destination, options) do
@@ -1223,7 +1275,7 @@ defmodule File do
end
end
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do
{:ok, :regular} ->
case do_cp_file(src, dest, on_conflict, acc) do
@@ -1236,8 +1288,15 @@ defmodule File do
{:ok, :symlink} ->
case :file.read_link(src) do
{:ok, link} when dereference? ->
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
{:ok, link} when dereference != nil ->
resolved = Path.expand(link, Path.dirname(src))
if MapSet.member?(dereference, resolved) do
{:error, :eloop, src}
else
dereference = MapSet.put(dereference, resolved)
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
end
{:ok, link} ->
do_cp_link(link, src, dest, on_conflict, acc)
@@ -1251,9 +1310,35 @@ defmodule File do
{:ok, files} ->
case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] ->
Enum.reduce(files, [dest | acc], fn x, acc ->
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
files
|> Enum.reduce_while([dest | acc], fn x, acc ->
case do_cp_r(
Path.join(src, x),
Path.join(dest, x),
on_conflict,
dereference,
preserve_dir_perms?,
acc
) do
{:error, _, _} = error -> {:halt, error}
acc -> {:cont, acc}
end
end)
|> case do
{:error, _, _} = error ->
error
files when preserve_dir_perms? ->
# Change the directory after writing files in case
# it was originally read only
case copy_file_mode(src, dest) do
:ok -> files
{:error, reason} -> {:error, reason, src}
end
files ->
files
end
{:error, reason} ->
{:error, reason, dest}
@@ -1272,7 +1357,7 @@ defmodule File do
end
# If we reach this clause, there was an error while processing a file.
defp do_cp_r(_, _, _, _, acc) do
defp do_cp_r(_, _, _, _, _, acc) do
acc
end
+13 -6
View File
@@ -119,7 +119,7 @@ defmodule File.Stream do
counter = fn device ->
device = skip_bom_and_offset(device, raw, modes)
count_lines(device, path, pattern, read_function(stream), 0)
count_lines(device, path, pattern, read_function(stream), 0, :empty)
end
{:ok, open!(stream, modes, counter)}
@@ -229,21 +229,28 @@ defmodule File.Stream do
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
end
defp count_lines(device, path, pattern, read, count) do
defp count_lines(device, path, pattern, read, count, last_byte) do
case read.(device) do
data when is_binary(data) and byte_size(data) > 0 ->
newlines = length(:binary.matches(data, pattern))
last = :binary.last(data)
count_lines(device, path, pattern, read, count + newlines, last)
data when is_binary(data) ->
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
count_lines(device, path, pattern, read, count, last_byte)
:eof ->
count
case last_byte do
:empty -> 0
?\n -> count
_ -> count + 1
end
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
end
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
end
+136 -120
View File
@@ -186,10 +186,9 @@ defmodule Float do
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
# rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, true = _e?, acc) do
# :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example,
# "1.0e400". Because of this, we rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, _, acc) do
acc
|> add_dot(dot?)
|> :lists.reverse()
@@ -200,16 +199,6 @@ defmodule Float do
float -> {float, rest}
end
defp parse_unsigned(rest, dot?, false = _e?, acc) do
float =
acc
|> add_dot(dot?)
|> :lists.reverse()
|> :erlang.list_to_float()
{float, rest}
end
defp add_dot(acc, true), do: acc
defp add_dot(acc, false), do: [?0, ?. | acc]
@@ -355,15 +344,12 @@ defmodule Float do
"""
@spec round(float, precision_range) :: float
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
def round(float, precision \\ 0)
def round(float, 0) when float == 0.0, do: float
def round(float, 0) when is_float(float) do
case float |> :erlang.round() |> :erlang.float() do
case :erlang.round(float) * 1.0 do
zero when zero == 0.0 and float < 0.0 -> -0.0
rounded -> rounded
end
@@ -377,140 +363,170 @@ defmodule Float do
raise ArgumentError, invalid_precision_message(precision)
end
# Decimal-place rounding via exact rational scaling. This is the bignum
# core used by reference implementations like David M. Gay's "Correctly
# Rounded Binary-Decimal and Decimal-Binary Conversions" (cited in the
# @doc above), Python's round(), and Java's BigDecimal.setScale.
#
# 1. Decompose float exactly: |float| = mantissa / 2^shift.
# 2. Scale exactly: |float| * 10^precision = mantissa * 10^precision / 2^shift.
# Because precision is bounded to 0..15, the product fits in ~103 bits
# (53-bit mantissa + ~50-bit power of ten) and BEAM bignums handle it
# directly without approximation.
# 3. Round the exact rational to an integer per the requested mode
# (half_up / floor / ceil) using quotient and remainder.
# 4. Emit the float closest to rounded_int / 10^precision:
# - fast path: when rounded_int < 2^53, both operands are exactly
# representable as floats and IEEE division is correctly rounded.
# - slow path: bignum alignment + manual mantissa extraction with
# round-to-nearest-even for the trailing bit.
#
# The integer-rounding decision (step 3) and the binary-emission decision
# (step 4) are deliberately independent: step 3 picks the exact rational
# the user asked for, step 4 picks the closest float to that rational.
# Conflating them is the classic source of double-rounding bugs.
#
# Faster algorithms exist (Cox 2026's table-based uscale; Ryū / Schubfach
# for round-trip printing) but target different problems or assume
# fixed-width machine arithmetic that BEAM doesn't expose efficiently.
# At precision <= 15, the exact path is small, easy to audit, and fast
# enough that a more complex algorithm has not been justified by benchmarks.
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
defp round(float, precision, rounding) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count} = decompose(significant, 1)
count = count - exp + 1023
defp round(float, precision, mode) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
cond do
# Precision beyond 15 digits
count >= 104 ->
case rounding do
:ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
:ceil when sign === 1 -> minus_zero()
:half_up when sign === 1 -> minus_zero()
_ -> 0.0
end
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump
# to 10^-precision; everything else rounds to signed zero).
exp == 0 ->
tiny_round(sign, precision, mode)
# We are asking more precision than we have
count <= precision ->
# |float| >= 2^52 — has no fractional bits, return unchanged.
exp - 1075 >= 0 ->
float
true ->
# Difference in precision between float and asked precision
# We subtract 1 because we need to calculate the remainder too
diff = count - precision - 1
# Get up to latest so we calculate the remainder
power_of_10 = power_of_10(diff)
# Convert the numerand to decimal base
num = num * power_of_5(count)
# Move to the given precision - 1
num = div(num, power_of_10)
div = div(num, 10)
num = rounding(rounding, sign, num, div)
# Convert back to float without loss
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
den = power_of_10(precision)
boundary = den <<< 52
cond do
num == 0 and sign == 1 ->
minus_zero()
num == 0 ->
0.0
num >= boundary ->
{den, exp} = scale_down(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
true ->
{num, exp} = scale_up(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
end
mantissa = @power_of_2_to_52 ||| mantissa
shift = 1075 - exp
do_round(sign, mantissa, shift, precision, mode)
end
end
# TODO remove once we require Erlang/OTP 27+
# This function tricks the compiler to avoid this bug in previous versions:
# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
defp minus_zero, do: -0.0
defp decompose(significant, initial) do
decompose(significant, 1, 0, initial)
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign.
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do
tiny_round(sign, precision, mode)
end
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
end
defp do_round(sign, mantissa, shift, precision, mode) do
power = power_of_10(precision)
product = mantissa * power
half = 1 <<< (shift - 1)
quotient = product >>> shift
remainder = product - (quotient <<< shift)
rounded_int = round_step(mode, sign, quotient, remainder, half)
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, last_count, acc)
end
cond do
rounded_int == 0 ->
signed_zero(sign)
defp decompose(<<>>, _count, last_count, acc) do
{acc, last_count}
end
rounded_int < @power_of_2_to_52 <<< 1 ->
# Both rounded_int and power fit in 53 bits, so IEEE float division
# is correctly rounded.
result = rounded_int / power
if sign == 1, do: -result, else: result
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
defp scale_down(num, den, exp) do
new_den = den <<< 1
if num < new_den do
{den >>> 52, exp}
else
scale_down(num, new_den, exp + 1)
true ->
bignum_to_float(sign, rounded_int, power)
end
end
defp decimal_to_float(sign, num, den, exp) do
quo = div(num, den)
rem = num - quo * den
defp round_step(:half_up, _sign, quotient, remainder, half) do
if remainder >= half, do: quotient + 1, else: quotient
end
tmp =
case den >>> 1 do
den when rem > den -> quo + 1
den when rem < den -> quo
_ when (quo &&& 1) === 1 -> quo + 1
_ -> quo
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient
defp round_step(:ceil, 1, quotient, _remainder, _half), do: quotient
defp signed_zero(0), do: 0.0
defp signed_zero(1), do: -0.0
# Result of rounding a non-zero float whose |float * 10^precision| < 0.5.
# ceil(+) → +10^-precision, floor(-) → -10^-precision, others → signed 0.
defp tiny_round(0, precision, :ceil), do: 1.0 / power_of_10(precision)
defp tiny_round(1, precision, :floor), do: -1.0 / power_of_10(precision)
defp tiny_round(sign, _precision, _mode), do: signed_zero(sign)
# Slow path: emit float closest to `sign * rounded_int / power` when
# rounded_int >= 2^53. The binary emission step is always IEEE
# round-to-nearest-even, regardless of the integer-rounding mode.
defp bignum_to_float(sign, rounded_int, power) do
shift_adjust = bit_length(rounded_int) - bit_length(power) - 53
{numerator, denominator, exp} = align(rounded_int, power, shift_adjust)
quotient = div(numerator, denominator)
remainder = numerator - quotient * denominator
half = denominator >>> 1
mantissa =
cond do
remainder > half -> quotient + 1
remainder < half -> quotient
(quotient &&& 1) === 1 -> quotient + 1
true -> quotient
end
tmp = tmp - @power_of_2_to_52
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
tmp
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient
# or when rounding carries. Rebalance into the canonical [2^52, 2^53)
# range so the 52-bit packing below doesn't silently truncate.
{mantissa, exp} =
if mantissa == @power_of_2_to_52 <<< 1,
do: {@power_of_2_to_52, exp + 1},
else: {mantissa, exp}
<<result::float>> = <<sign::1, exp + 1023::11, mantissa - @power_of_2_to_52::52>>
result
end
defp rounding(:floor, 1, _num, div), do: div + 1
defp rounding(:ceil, 0, _num, div), do: div + 1
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53)
# and the resulting float = numerator/denominator * 2^(exp-52).
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
new_power = power <<< shift_adjust
defp rounding(:half_up, _sign, num, div) do
case rem(num, 10) do
rem when rem < 5 -> div
rem when rem >= 5 -> div + 1
if rounded_int < new_power <<< 53,
do: {rounded_int, new_power, 52 + shift_adjust},
else: {rounded_int, new_power <<< 1, 53 + shift_adjust}
end
defp align(rounded_int, power, shift_adjust) do
shifted = rounded_int <<< -shift_adjust
cond do
shifted >= power <<< 53 -> {shifted, power <<< 1, 53 + shift_adjust}
shifted >= power <<< 52 -> {shifted, power, 52 + shift_adjust}
true -> {shifted <<< 1, power, 51 + shift_adjust}
end
end
defp rounding(_, _, _, div), do: div
defp bit_length(0), do: 0
defp bit_length(integer) when integer > 0, do: bit_length(integer, 0)
defp bit_length(integer, acc) when integer >= 1 <<< 64, do: bit_length(integer >>> 64, acc + 64)
defp bit_length(integer, acc) when integer >= 1 <<< 16, do: bit_length(integer >>> 16, acc + 16)
defp bit_length(integer, acc) when integer >= 1 <<< 4, do: bit_length(integer >>> 4, acc + 4)
defp bit_length(integer, acc) when integer >= 1, do: bit_length(integer >>> 1, acc + 1)
defp bit_length(_integer, acc), do: acc
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_10(unquote(x)), do: unquote(acc)
Enum.reduce(0..15, 1, fn exponent, acc ->
defp power_of_10(unquote(exponent)), do: unquote(acc)
acc * 10
end)
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_5(unquote(x)), do: unquote(acc)
acc * 5
end)
@doc """
Returns a pair of integers whose ratio is exactly equal
to the original float and with a positive denominator.
+9 -4
View File
@@ -232,6 +232,8 @@ defmodule GenServer do
a name on start via the `:name` option. Registered names are also
automatically cleaned up on termination. The supported values are:
* `nil` (default) - the GenServer is not registered with a name.
* an atom - the GenServer is registered locally (to the current node)
with the given name using `Process.register/2`.
@@ -565,9 +567,12 @@ defmodule GenServer do
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
after a delay to attempt a restart.
Returning `{:stop, reason}` will cause `start_link/3` to return
`{:error, reason}` and the process to exit with reason `reason` without
entering the loop or calling `c:terminate/2`.
Returning `{:error, reason}` will cause `start_link/3` to return
`{:error, reason}`.
Returning `{:stop, reason}` will the process to exit with reason `reason`,
without entering the loop or calling `c:terminate/2`. `start_link/3` will
return `{:error, reason}`, but only if the caller is trapping exits.
"""
@callback init(init_arg :: term) ::
{:ok, state}
@@ -858,7 +863,7 @@ defmodule GenServer do
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
@typedoc "The GenServer name"
@type name :: atom | {:global, term} | {:via, module, term}
@type name :: nil | atom | {:global, term} | {:via, module, term}
@typedoc "Options used by the `start*` functions"
@type options :: [option]
+14 -1
View File
@@ -669,6 +669,11 @@ defimpl Inspect, for: Any do
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
end
# A temporary clause to deal with native records until they are officially supported
def inspect(native_record, _opts) do
:io_lib.format("~p", [native_record]) |> IO.iodata_to_binary()
end
def inspect_as_struct(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
@@ -696,7 +701,15 @@ defimpl Inspect, for: Range do
end
# TODO: Remove me on v2.0
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
inspect =
quote generated: true do
inspect(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(opts)
)
end
def unquote(inspect) do
step = if first <= last, do: 1, else: -1
inspect(Map.put(range, :step, step), opts)
end
+3 -3
View File
@@ -46,8 +46,8 @@ defmodule Inspect.Opts do
* `:limit` - limits the number of items that are inspected for tuples,
bitstrings, maps, lists and any other collection of items, with the exception of
printable strings and printable charlists which use the `:printable_limit` option.
It accepts a positive integer or `:infinity`. It defaults to `100` since
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
It accepts a positive integer or `:infinity`. It defaults to `200` since
`Elixir v1.20.0`, as it has better defaults to deal with nested collections.
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
charlists: :infer,
custom_options: [],
inspect_fun: &Inspect.inspect/2,
limit: 100,
limit: 200,
pretty: false,
printable_limit: 4096,
safe: true,
+50 -4
View File
@@ -18,6 +18,45 @@ defmodule Integer do
import Bitwise
@doc """
Counts the number of set bits (1) in the binary representation of a non-negative `integer`.
This operation is known as the Hamming weight or population count.
Raises an `ArithmeticError` if `integer` is negative.
## Examples
iex> Integer.popcount(0)
0
iex> Integer.popcount(1)
1
iex> Integer.popcount(0b10110101)
5
iex> Integer.popcount(255)
8
iex> Integer.popcount(0b1111111111111111)
16
iex> Integer.popcount(-1)
** (ArithmeticError) bad argument in arithmetic expression
"""
@doc since: "1.20.0"
@spec popcount(non_neg_integer) :: non_neg_integer
def popcount(integer) when is_integer(integer) and integer < 0,
do: :erlang.error(:badarith, [integer])
def popcount(integer) when is_integer(integer),
do: popcount(integer, 0)
defp popcount(0, acc), do: acc
defp popcount(n, acc), do: popcount(n &&& n - 1, acc + 1)
@doc """
Determines if `integer` is odd.
@@ -258,8 +297,9 @@ defmodule Integer do
defp undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | _], base, _)
when is_integer(digit) and (digit >= base or digit <= -base),
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit)
@@ -492,8 +532,12 @@ defmodule Integer do
iex> Integer.extended_gcd(10, 0)
{10, 1, 0}
iex> Integer.extended_gcd(-10, 0)
{10, -1, 0}
iex> Integer.extended_gcd(0, 10)
{10, 0, 1}
iex> Integer.extended_gcd(0, -10)
{10, 0, -1}
iex> Integer.extended_gcd(0, 0)
{0, 0, 0}
@@ -501,8 +545,10 @@ defmodule Integer do
@doc since: "1.12.0"
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
def extended_gcd(0, 0), do: {0, 0, 0}
def extended_gcd(0, b), do: {b, 0, 1}
def extended_gcd(a, 0), do: {a, 1, 0}
def extended_gcd(0, b) when b > 0, do: {b, 0, 1}
def extended_gcd(0, b) when b < 0, do: {-b, 0, -1}
def extended_gcd(a, 0) when a > 0, do: {a, 1, 0}
def extended_gcd(a, 0) when a < 0, do: {-a, -1, 0}
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
extended_gcd(integer2, integer1, 0, 1, 1, 0)
+10 -10
View File
@@ -67,7 +67,7 @@ defprotocol JSON.Encoder do
{io, _prefix} =
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
key = IO.iodata_to_binary([prefix, :json.encode_binary(Atom.to_string(field)), ?:])
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
end)
@@ -130,25 +130,25 @@ end
defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do
:elixir_json.encode_binary(value)
:json.encode_binary(value)
end
end
defimpl JSON.Encoder, for: List do
def encode(value, encoder) do
:elixir_json.encode_list(value, encoder)
:json.encode_list(value, encoder)
end
end
defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do
:elixir_json.encode_integer(value)
:json.encode_integer(value)
end
end
defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do
:elixir_json.encode_float(value)
:json.encode_float(value)
end
end
@@ -408,7 +408,7 @@ defmodule JSON do
decoders = Keyword.put_new(decoders, :null, nil)
try do
:elixir_json.decode(binary, acc, Map.new(decoders))
:json.decode(binary, acc, Map.new(decoders))
catch
:error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}}
@@ -528,16 +528,16 @@ defmodule JSON do
end
def protocol_encode(value, _encoder) when is_binary(value),
do: :elixir_json.encode_binary(value)
do: :json.encode_binary(value)
def protocol_encode(value, _encoder) when is_integer(value),
do: :elixir_json.encode_integer(value)
do: :json.encode_integer(value)
def protocol_encode(value, _encoder) when is_float(value),
do: :elixir_json.encode_float(value)
do: :json.encode_float(value)
def protocol_encode(value, encoder) when is_list(value),
do: :elixir_json.encode_list(value, encoder)
do: :json.encode_list(value, encoder)
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
do: JSON.Encoder.Map.encode(value, encoder)
+81 -71
View File
@@ -2081,7 +2081,7 @@ defmodule Kernel do
end
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, operator}],
{:case, [], [check, [do: bools ++ error]]}
)
end
@@ -2109,7 +2109,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :"!!"}],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> false
@@ -2123,7 +2123,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :!}],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> true
@@ -2752,7 +2752,7 @@ defmodule Kernel do
nil ->
quote do
case unquote(term) do
%_{__exception__: true} -> true
%_{__exception__: _} -> true
_ -> false
end
end
@@ -2764,8 +2764,7 @@ defmodule Kernel do
quote do
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -2792,7 +2791,7 @@ defmodule Kernel do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name, __exception__: true} -> true
%{__struct__: ^name, __exception__: _} -> true
_ -> false
end
@@ -2810,8 +2809,7 @@ defmodule Kernel do
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -4054,7 +4052,7 @@ defmodule Kernel do
defp build_if(condition, do: do_clause, else: else_clause) do
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :if}],
quote do
case unquote(condition) do
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
@@ -4105,9 +4103,15 @@ defmodule Kernel do
end
defp build_unless(condition, do: do_clause, else: else_clause) do
quote do
if(unquote(condition), do: unquote(else_clause), else: unquote(do_clause))
end
annotate_case(
[optimize_boolean: true, type_check: {:case, :unless}],
quote do
case unquote(condition) do
x when unquote(x_is_false_or_nil()) -> unquote(do_clause)
_ -> unquote(else_clause)
end
end
)
end
defp build_unless(_condition, _arguments) do
@@ -4375,7 +4379,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
annotate_case(
[type_check: :expr],
[type_check: {:case, :&&}],
quote do
case unquote(left) do
x when unquote(x_is_false_or_nil()) ->
@@ -4418,7 +4422,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "||")
annotate_case(
[type_check: :expr],
[type_check: {:case, :||}],
quote do
case unquote(left) do
x when unquote(x_is_false_or_nil()) ->
@@ -4699,17 +4703,13 @@ defmodule Kernel do
false
[] ->
quote do
_ = unquote(left)
false
end
# inlined as false in erlang pass
quote(do: :lists.member(unquote(left), []))
[head | tail] = list ->
# We only expand lists in the body if they are relatively
# short and it is made only of literal expressions.
case not in_body? or small_literal_list?(right) do
true -> in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
false -> quote(do: :lists.member(unquote(left), unquote(right)))
case in_body? do
false -> in_list(left, head, tail, expand, list)
true -> quote(do: :lists.member(unquote(left), unquote(right)))
end
%{} = right ->
@@ -4721,7 +4721,7 @@ defmodule Kernel do
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step)))
_ when in_body? ->
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
quote(do: Elixir.Enum.__in__(unquote(left), unquote(right)))
_ ->
raise_on_invalid_args_in_2(right)
@@ -4756,12 +4756,6 @@ defmodule Kernel do
end
end
defp small_literal_list?(list) when is_list(list) and length(list) <= 32 do
:lists.all(fn x -> is_binary(x) or is_atom(x) or is_number(x) end, list)
end
defp small_literal_list?(_list), do: false
defp in_range(left, first, last, step) when is_integer(step) do
in_range_literal(left, first, last, step)
end
@@ -4821,12 +4815,12 @@ defmodule Kernel do
end
end
defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
defp in_list(left, head, tail, expand, right) do
[head | tail] = :lists.map(&comp(left, &1, expand, right), [head | tail])
:lists.foldl(&quote(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
end
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
defp comp(left, {:|, _, [head, tail]}, expand, right) do
case expand.(tail) do
[] ->
quote(do: :erlang."=:="(unquote(left), unquote(head)))
@@ -4835,15 +4829,7 @@ defmodule Kernel do
quote do
Kernel.or(
:erlang."=:="(unquote(left), unquote(head)),
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
)
end
tail when in_body? ->
quote do
Kernel.or(
:erlang."=:="(unquote(left), unquote(head)),
:lists.member(unquote(left), unquote(tail))
unquote(in_list(left, tail_head, tail, expand, right))
)
end
@@ -4852,7 +4838,7 @@ defmodule Kernel do
end
end
defp comp(left, right, _expand, _right, _in_body?) do
defp comp(left, right, _expand, _right) do
quote(do: :erlang."=:="(unquote(left), unquote(right)))
end
@@ -5228,6 +5214,11 @@ defmodule Kernel do
end
end
defmacro defmodule(alias, [{:do, _block}, {atom, _} | _]) when is_atom(atom) do
raise ArgumentError,
"unexpected reserved word at the top-level of the \"defmodule #{Macro.to_string(alias)}\" do-block: #{atom}"
end
defp module_meta({_, meta, _}), do: meta
defp module_meta(_), do: []
@@ -5880,11 +5871,15 @@ defmodule Kernel do
end
@doc """
Defines a macro suitable for use in guard expressions.
Defines a custom guard with the given name.
It raises at compile time if the `guard` uses expressions that aren't
allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a macro that can be used both inside or outside guards.
Once defined, custom guards can be invoked within regular code or in
guards. The module that contains the custom guard must be required before usage.
Custom guards are defined by providing a valid guard expression to
the right-hand side of `when`. `defguard` will then expand and validate
the expressions as guards. `defguard` will raise at compile time if the
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
@@ -5892,31 +5887,30 @@ defmodule Kernel do
## Example
For example, to define a guard similar to `Integer.is_even/1`, you can write:
defmodule Integer.Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end
defmodule Collatz do
@moduledoc "Tools for working with the Collatz sequence."
import Integer.Guards
which can then be used as:
@doc "Determines the number of steps `n` takes to reach `1`."
# If this function never converges, please let me know what `n` you used.
def converge(n) when n > 0, do: step(n, 0)
require Integer.Guards
Integer.Guards.is_even(3)
#=> false
defp step(1, step_count) do
step_count
end
## Implementation details
defp step(n, step_count) when is_even(n) do
step(div(n, 2), step_count + 1)
end
Behind the scenes, `defguard` will generate a macro which can be used
inside and outside of guards, preserving their respective semantics.
defp step(n, step_count) do
step(3 * n + 1, step_count + 1)
end
end
When invoked inside a guard, it behaves as if the right-hand side of
`when` is injected as part of the guard, replacing the custom guard
arguments by the expressions given as inputs.
When invoked outside of a guard, it preserves regular function calling
semantics with one caveat: all arguments are evaluated before invocation,
except arguments which are unused, which are then never evaluated.
"""
@doc since: "1.6.0"
@spec defguard(Macro.t()) :: Macro.t()
@@ -6331,7 +6325,15 @@ defmodule Kernel do
"""
@doc since: "1.14.0"
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
# The compiling process may override the callback by putting it in
# the process dictionary.
dbg_callback =
case :erlang.get({:elixir, :dbg_callback}) do
:undefined -> Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
value -> value
end
{mod, fun, args} = dbg_callback
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
end
@@ -6420,12 +6422,20 @@ defmodule Kernel do
{microsecond, _precision} = duration.microsecond
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
total =
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
if total < 0 do
raise ArgumentError,
"duration must be positive, got: #{inspect(duration)}"
end
total
end
end
+17 -10
View File
@@ -25,9 +25,8 @@ defmodule Kernel.ParallelCompiler do
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
{:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(),
verification: boolean(),
@@ -198,8 +197,8 @@ defmodule Kernel.ParallelCompiler do
deprecation warnings, and type checking should run. Defaults to `true`.
We recommend disabling it only for debugging purposes.
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
and group pass checker
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
each module type check, and group pass checker
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
after compilation (see `Code.purge_compiler_modules/0`)
@@ -340,7 +339,7 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(schedulers, checker, files, output, options) do
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = Process.send_after(self(), :threshold_check, threshold)
timer_ref = :erlang.send_after(threshold, self(), :threshold_check)
purge_compiler_modules =
if Keyword.get(options, :purge_compiler_modules, false) do
@@ -460,7 +459,7 @@ defmodule Kernel.ParallelCompiler do
modules = write_module_binaries(result, state.output, state)
profile(state, "after compile callback", state.after_compile)
runtime_warnings =
{runtime_warnings, errors} =
if state.verification? do
profile(
state,
@@ -471,11 +470,19 @@ defmodule Kernel.ParallelCompiler do
fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end
)
else
[]
{[], []}
end
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
{{:ok, modules, info}, state}
case errors do
[] ->
{{:ok, modules, info}, state}
_ ->
IO.puts(:stderr, "== Type checking failed with errors ==")
{{:error, errors, info}, state}
end
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
@@ -833,7 +840,7 @@ defmodule Kernel.ParallelCompiler do
end
end
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
timer_ref = :erlang.send_after(state.long_compilation_threshold, self(), :threshold_check)
state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
+15 -5
View File
@@ -255,9 +255,20 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do
{name, arity} = type_pair ->
if built_in_type?(name, arity) do
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
compile_error(env, message)
cond do
# This is a built-in type since OTP 29 but it just generates a warning for now
{name, arity} == {:record, 0} ->
IO.warn("type #{name}/#{arity} is overriding a built-in type",
file: file,
line: line
)
built_in_type?(name, arity) ->
message = "type #{name}/#{arity} is a built-in type and it cannot be redefined"
compile_error(env, message)
true ->
:ok
end
if Map.has_key?(type_pairs, type_pair) do
@@ -599,8 +610,7 @@ defmodule Kernel.Typespec do
types =
:lists.map(
fn %{field: field} ->
default_type = if field == :__exception__, do: true, else: quote(do: term())
{field, Keyword.get(fields, field, default_type)}
{field, Keyword.get(fields, field, quote(do: term()))}
end,
struct_info
)
+3 -3
View File
@@ -266,7 +266,7 @@ defmodule Kernel.Utils do
module.exception([])
end
def raise(%_{__exception__: true} = exception) do
def raise(%_{__exception__: _} = exception) do
exception
end
@@ -339,7 +339,7 @@ defmodule Kernel.Utils do
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
end
end
end
@@ -369,7 +369,7 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, module) do
defp unquote_refs_once(guard, refs, %{module: module}) do
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
+20 -11
View File
@@ -119,14 +119,23 @@ defmodule Keyword do
iex> Keyword.from_keys([:foo, :bar, :baz], :atom)
[foo: :atom, bar: :atom, baz: :atom]
iex> Keyword.from_keys([], :atom)
[]
iex> Keyword.from_keys(["foo"], :bar)
** (ArgumentError) expected a list of atoms as keys, got: "foo"
"""
@doc since: "1.14.0"
@spec from_keys([key], value) :: t(value)
def from_keys(keys, value) when is_list(keys) do
:lists.map(&{&1, value}, keys)
:lists.map(
fn
key when is_atom(key) -> {key, value}
other -> raise ArgumentError, "expected a list of atoms as keys, got: #{inspect(other)}"
end,
keys
)
end
@doc """
@@ -261,34 +270,34 @@ defmodule Keyword do
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
{:ok, keyword()} | {:error, [atom]}
def validate(keyword, values) when is_list(keyword) and is_list(values) do
validate(keyword, values, [], [], [])
validate(keyword, values, [], keyword, [])
end
defp validate([{key, _} = pair | keyword], values1, values2, acc, bad_keys) when is_atom(key) do
defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
case find_key!(key, values1, values2) do
{values1, values2} ->
validate(keyword, values1, values2, [pair | acc], bad_keys)
validate(keyword, values1, values2, original, bad_keys)
:error ->
case find_key!(key, values2, values1) do
{values1, values2} ->
validate(keyword, values1, values2, [pair | acc], bad_keys)
validate(keyword, values1, values2, original, bad_keys)
:error ->
validate(keyword, values1, values2, acc, [key | bad_keys])
validate(keyword, values1, values2, original, [key | bad_keys])
end
end
end
defp validate([], values1, values2, acc, []) do
{:ok, move_pairs!(values1, move_pairs!(values2, acc))}
defp validate([], values1, values2, original, []) do
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
end
defp validate([], _values1, _values2, _acc, bad_keys) do
defp validate([], _values1, _values2, _original, bad_keys) do
{:error, bad_keys}
end
defp validate([pair | _], _values1, _values2, _acc, []) do
defp validate([pair | _], _values1, _values2, _original, []) do
raise ArgumentError,
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
end
@@ -956,7 +965,7 @@ defmodule Keyword do
iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
true
Comparison between values is done with `===/3`,
Comparison between values is done with `===/2`,
which means integers are not equivalent to floats:
iex> Keyword.equal?([a: 1.0], [a: 1])
+10 -4
View File
@@ -971,9 +971,15 @@ defmodule Macro do
}
]
def struct_info!(module, env) when is_atom(module) do
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
meta = [line: env.line]
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do
{:ok, info} ->
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
info
{:error, desc} ->
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
end
@@ -2190,7 +2196,7 @@ defmodule Macro do
Please check `expand_literals/2` for use cases and pitfalls.
"""
@doc since: "1.14.1"
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: {t(), acc} when acc: term()
def expand_literals(ast, acc, fun)
def expand_literals({:__aliases__, meta, args}, acc, fun) do
+2 -1
View File
@@ -367,7 +367,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
{:ok, t} | {:error, String.t()}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
+13 -4
View File
@@ -547,6 +547,8 @@ defmodule Map do
:erlang.error({:badmap, non_map})
end
defp take([], _map, []), do: %{}
defp take([], _map, acc) do
:maps.from_list(acc)
end
@@ -1107,7 +1109,7 @@ defmodule Map do
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
false
Comparison between keys and values is done with `===/3`,
Comparison between keys and values is done with `===/2`,
which means integers are not equivalent to floats:
iex> Map.equal?(%{a: 1.0}, %{a: 1})
@@ -1136,9 +1138,9 @@ defmodule Map do
> #### Performance considerations {: .tip}
>
> If you find yourself doing multiple calls to `Map.filter/2`
> and `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
> a map at the end using `Map.new/1`.
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
> a map at the end using `Map.new/1` or `Map.new/2`.
## Examples
@@ -1170,6 +1172,13 @@ defmodule Map do
See also `filter/2`.
> #### Performance considerations {: .tip}
>
> If you find yourself doing multiple calls to `Map.filter/2`
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
> a map at the end using `Map.new/1` or `Map.new/2`.
## Examples
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
+2 -1
View File
@@ -57,7 +57,8 @@ defmodule MapSet do
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
# either via module attributes or by the compiler.
@typep internal(value) :: %{optional(value) => term()}
# Defaulting to a broad `term()` type to prevent opaqueness violations.
@typep internal(_value) :: term()
@type t(value) :: %__MODULE__{map: internal(value)}
@type t :: t(term)
+45 -35
View File
@@ -656,7 +656,8 @@ defmodule Module do
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: module()
@spec module_info(:nifs) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info]
@@ -679,7 +680,7 @@ defmodule Module do
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@type definition :: {function_name :: atom, arity}
@type def_kind :: :def | :defp | :defmacro | :defmacrop
@type create_opts :: [
@@ -958,7 +959,7 @@ defmodule Module do
It handles binaries and atoms.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -984,7 +985,7 @@ defmodule Module do
It handles binaries and atoms. If one of the aliases
is nil, it is discarded.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1015,7 +1016,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1039,7 +1040,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1249,17 +1250,18 @@ defmodule Module do
"""
@spec defines?(module, definition) :: boolean
def defines?(module, {name, arity} = tuple)
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 do
def defines?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
:ets.member(set, {:def, tuple})
:ets.member(set, {:def, definition})
end
@doc """
Checks if the module defines a function or macro of the
given `kind`.
given kind.
`kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
`def_kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
This function can only be used on modules that have not yet been compiled.
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
@@ -1275,12 +1277,13 @@ defmodule Module do
"""
@spec defines?(module, definition, def_kind) :: boolean
def defines?(module, {name, arity} = tuple, def_kind)
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 and
def defines?(module, {function_name, arity} = definition, def_kind)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 and
def_kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
case :ets.lookup(set, {:def, tuple}) do
case :ets.lookup(set, {:def, definition}) do
[{_, ^def_kind, _, _, _, _}] -> true
_ -> false
end
@@ -1293,7 +1296,9 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec defines_type?(module, definition) :: boolean
def defines_type?(module, definition) when is_atom(module) do
def defines_type?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
Kernel.Typespec.defines_type?(module, definition)
end
@@ -1306,7 +1311,9 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec spec_to_callback(module, definition) :: boolean
def spec_to_callback(module, definition) do
def spec_to_callback(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
Kernel.Typespec.spec_to_callback(module, definition)
end
@@ -1358,7 +1365,7 @@ defmodule Module do
"""
@doc since: "1.13.0"
@spec overridables_in(module) :: [atom]
@spec overridables_in(module) :: [definition]
def overridables_in(module) when is_atom(module) do
assert_not_compiled!(__ENV__.function, module, :all)
:elixir_overridable.overridables_for(module)
@@ -1407,10 +1414,10 @@ defmodule Module do
"""
@spec definitions_in(module, def_kind) :: [definition]
def definitions_in(module, kind)
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
def definitions_in(module, def_kind)
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in)
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
:ets.select(set, [{{{:def, :"$1"}, def_kind, :_, :_, :_, :_}, [], [:"$1"]}])
end
@doc """
@@ -1441,16 +1448,17 @@ defmodule Module do
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
| nil
@doc since: "1.12.0"
def get_definition(module, {name, arity}, options \\ [])
when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
def get_definition(module, {function_name, arity} = _definition, options \\ [])
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 and is_list(options) do
{set, bag} = data_tables_for!(module, __ENV__.function, "")
case :ets.lookup(set, {:def, {name, arity}}) do
case :ets.lookup(set, {:def, {function_name, arity}}) do
[{_key, kind, meta, _, _, _}] ->
clauses =
if options[:skip_clauses],
do: [],
else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2)
{:v1, kind, meta, clauses}
@@ -1467,10 +1475,11 @@ defmodule Module do
"""
@doc since: "1.12.0"
@spec delete_definition(module, definition) :: boolean()
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
def delete_definition(module, {function_name, arity} = _definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
assert_not_compiled!(__ENV__.function, module, :writeable)
:elixir_def.take_definition(module, {name, arity}) != false
:elixir_def.take_definition(module, {function_name, arity}) != false
end
@doc """
@@ -1486,20 +1495,20 @@ defmodule Module do
given.
"""
@spec make_overridable(module, [definition]) :: :ok
def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
def make_overridable(module, definitions) when is_atom(module) and is_list(definitions) do
assert_not_compiled!(__ENV__.function, module, :writeable)
func = fn
{function_name, arity} = tuple
{function_name, arity} = definition
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 ->
case :elixir_def.take_definition(module, tuple) do
case :elixir_def.take_definition(module, definition) do
false ->
raise ArgumentError,
"cannot make function #{function_name}/#{arity} " <>
"overridable because it was not defined"
clause ->
:elixir_overridable.record_overridable(module, tuple, clause)
:elixir_overridable.record_overridable(module, definition, clause)
end
other ->
@@ -1508,7 +1517,7 @@ defmodule Module do
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
end
:lists.foreach(func, tuples)
:lists.foreach(func, definitions)
end
@spec make_overridable(module, module) :: :ok
@@ -1566,9 +1575,10 @@ defmodule Module do
exists or one is pending.
"""
@spec overridable?(module, definition) :: boolean
def overridable?(module, {function_name, arity} = tuple)
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 do
:elixir_overridable.overridable_for(module, tuple) != :not_overridable
def overridable?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
:elixir_overridable.overridable_for(module, definition) != :not_overridable
end
@doc """
+58 -30
View File
@@ -10,6 +10,7 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type error() :: term()
@type mode() :: :erlang | :elixir | :protocol
@typedoc """
@@ -19,6 +20,7 @@ defmodule Module.ParallelChecker do
{:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {:profile, :time}
| {atom(), term()}
]
@@ -104,7 +106,7 @@ defmodule Module.ParallelChecker do
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
else
_ -> {:not_found, nil}
_ -> {:uncached, nil}
end
is_tuple(info) ->
@@ -116,18 +118,18 @@ defmodule Module.ParallelChecker do
send(checker, {ref, :cached})
receive do
{^ref, :check} ->
{^ref, :check, profile} ->
# Set the compiler info so we can collect warnings
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
{warnings, errors} =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
check_module(module_tuple, {checker, table}, log?, profile)
else
[]
{[], []}
end
send(pid, {__MODULE__, module, warnings})
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :done, module})
end
@@ -184,7 +186,7 @@ defmodule Module.ParallelChecker do
Returns the updated list of warnings from the verification.
"""
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
@spec verify(cache(), [{module(), Path.t()}]) :: {[warning()], [error()]}
def verify({checker, table}, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
@@ -194,29 +196,34 @@ defmodule Module.ParallelChecker do
end
count = :gen_server.call(checker, :start, :infinity)
diagnostics = collect_results(count, [])
{warnings, errors} = collect_results(count, [], [])
case :erlang.get(:elixir_code_diagnostics) do
:undefined -> :ok
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
end
diagnostics
{warnings, errors}
end
defp collect_results(0, diagnostics) do
diagnostics
defp collect_results(0, warnings, errors) do
{warnings, errors}
end
defp collect_results(count, diagnostics) do
defp collect_results(count, warnings, errors) do
receive do
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
diagnostic = %{diagnostic | file: file && Path.absname(file)}
collect_results(count, [diagnostic | diagnostics])
{__MODULE__, _module, new_diagnostics} ->
collect_results(count - 1, new_diagnostics ++ diagnostics)
if Map.get(diagnostic, :severity, :warning) == :error do
collect_results(count, warnings, [diagnostic | errors])
else
collect_results(count, [diagnostic | warnings], errors)
end
{__MODULE__, _module, new_warnings, new_errors} ->
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
end
end
@@ -256,7 +263,7 @@ defmodule Module.ParallelChecker do
## Module checking
defp check_module(module_tuple, cache, log?) do
defp check_module(module_tuple, cache, log?, profile) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
module_tuple
@@ -270,18 +277,23 @@ defmodule Module.ParallelChecker do
definitions
)
diagnostics =
module
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
type_warnings =
profile(module, profile, fn ->
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
end)
{warnings, errors} =
type_warnings
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(file, log?)
|> group_diagnostics()
|> emit_diagnostics(file, log?)
|> Enum.split_with(&(&1.severity == :warning))
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
diagnostics
{warnings, errors}
end
defp module_map_to_module_tuple(module_map) do
@@ -332,16 +344,16 @@ defmodule Module.ParallelChecker do
## Warning helpers
defp group_warnings(warnings) do
defp group_diagnostics(triplets) do
{ungrouped, grouped} =
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(warning)
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(term)
if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location])
grouped =
Map.update(grouped, warning, {locations, diagnostic}, fn
Map.update(grouped, term, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end)
@@ -359,7 +371,7 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped)
end
defp emit_warnings(warnings, file, log?) do
defp emit_diagnostics(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
log? and print_diagnostics(diagnostics)
@@ -570,6 +582,7 @@ defmodule Module.ParallelChecker do
schedulers: schedulers,
threshold: threshold,
callback: callback,
profile: Keyword.get(options, :profile),
protocols: [],
table: table
}
@@ -646,9 +659,24 @@ defmodule Module.ParallelChecker do
end
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
send(pid, {ref, :check})
timer = Process.send_after(self(), {__MODULE__, :timeout, module, pid}, state.threshold)
send(pid, {ref, :check, state.profile})
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
spawned = Map.put(state.spawned, module, timer)
run_checkers(%{state | modules: modules, spawned: spawned})
end
defp profile(module, :time, fun) do
{time, result} = :timer.tc(fun)
IO.puts(
:stderr,
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
)
result
end
defp profile(_module, _profile, fun) do
fun.()
end
end
+248 -44
View File
@@ -4,7 +4,7 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern}
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
@@ -86,7 +86,7 @@ defmodule Module.Types do
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], private, context}
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
else
{types, private, context}
end
@@ -136,8 +136,8 @@ defmodule Module.Types do
defp impl_for(attrs) do
case List.keyfind(attrs, :__impl__, 0) do
{:__impl__, [protocol: protocol, for: for]} ->
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
{for, protocol.behaviour_info(:callbacks)}
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
{for, protocol.__protocol__(:functions)}
else
nil
end
@@ -251,17 +251,27 @@ defmodule Module.Types do
context ->
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
clauses_indexes =
for type_index <- pending,
not skip_unused_clause?(info, type_index),
{clause_index, ^type_index} <- mapping,
do: clause_index
if pending != [] do
{used_indexes, unused_indexes} =
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
{used_indexes, unused_indexes} ->
if type_index in pending and not skip_unused_clause?(info, type_index) do
{used_indexes, [clause_index | unused_indexes]}
else
{[clause_index | used_indexes], unused_indexes}
end
end)
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
else
context
end
end
end
@@ -289,11 +299,11 @@ defmodule Module.Types do
local_sigs ->
case finder.(fun_arity) do
{mode, {fun_arity, kind, meta, clauses}, expected} ->
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -306,9 +316,10 @@ defmodule Module.Types do
end
end
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
context =
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
stack = with_file_meta(stack, meta)
Module.Types.Traverse.of_expr(body, stack, context)
end)
@@ -316,31 +327,113 @@ defmodule Module.Types do
{inferred, [{0, 0}], context}
end
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
{fun, _arity} = fun_arity
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
stack = fresh_stack(stack, mode, fun_arity)
base_info = {:def, kind, fun, expected}
{_, _, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, [], [], context}, fn
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
context = fresh_context(context)
case clauses do
[{meta, args, [], {:super, _, [_ | _]} = body}] ->
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context)
_ ->
infer_local_handler(clauses, base_info, kind, fun, expected, stack, context)
end
end
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
stack = with_file_meta(stack, meta)
guards = []
previous = Pattern.init_previous()
fresh_context = fresh_context(context)
info = {base_info, args, guards}
try do
{trees, _, _, _, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
# Compute the intersected arrows from the function call
{:super, meta, call_args} = body
{_kind, call_fun} = Keyword.fetch!(meta, :super)
term = Descr.term()
of_fun = &Expr.of_expr/5
{arrows, body_context} =
Apply.local_arrows(call_fun, call_args, term, body, stack, head_context, of_fun)
# For each arrow, compute the default arrow
{_, _, mapping, inferred} =
Enum.reduce(arrows, {0, 0, [], []}, fn
{clause_domain, return_type}, {index, total, mapping, inferred} ->
of_fun = &Expr.of_expr(&1, &2, body, stack, &3)
{_clause_args, clause_context} =
Helpers.zip_map_reduce(call_args, clause_domain, head_context, of_fun)
clause_types = Pattern.of_domain(trees, stack, clause_context)
{type_index, inferred} =
add_inferred(inferred, clause_types, return_type, total - 1, [])
total = if type_index == -1, do: total + 1, else: total
{index + 1, total, [{0, index} | mapping], inferred}
end)
domain =
case inferred do
[_] ->
nil
_ ->
inferred
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
end
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end
defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do
{_, _, _, domain, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
{meta, args, guards, body},
{index, total, previous, domain, mapping, inferred, acc_context} ->
stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
try do
{trees, _precise?, context} =
Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context)
{trees, _precise?, head_no_previous_args_types, previous, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
{return_type, context} =
Expr.of_expr(body, Descr.term(), body, stack, context)
Expr.of_expr(body, Descr.term(), body, stack, head_context)
args_types = Pattern.of_domain(trees, stack, context)
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
domain =
case domain do
[] ->
args_types
_ ->
head_args_types = Pattern.of_domain(trees, stack, head_context)
compute_domain(args_types, head_args_types, head_no_previous_args_types, domain)
end
if type_index == -1 do
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
mapping = [{index, total} | mapping]
{index + 1, total + 1, previous, domain, mapping, inferred, context}
else
{index + 1, total, [{index, type_index} | mapping], inferred, context}
mapping = [{index, type_index} | mapping]
{index + 1, total, previous, domain, mapping, inferred, context}
end
rescue
e ->
@@ -350,19 +443,69 @@ defmodule Module.Types do
domain =
case clauses_types do
[_] ->
nil
_ ->
clauses_types
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
[_] -> nil
_ -> domain
end
inferred = {:infer, domain, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(clauses_context, context)}
end
defp compute_domain(
[arg | args_types],
[head_arg | head_args_types],
[no_prev_arg | no_prev_args_types],
[d | domain]
) do
[
# This is an optimization that broadens the domain, but it is acceptable
# because the domain is used for reverse arrows and not type checking.
#
# The overall idea is that, if we have a function with three clauses,
# the domain is computed by unioning their inferred types. However, their
# inferred types often have the different of the previous clauses:
#
# union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
#
# Where `rN` represents the refinement in every function body.
#
# What this function does is, if the type of a given arg in a clause
# before and after the body is the same (meaning r3 is term), then
# we replace all of `(c3 - c2 - c1)` by just `c3`, which removes
# many of the differences in the node. However, keep in mind that,
# because `r2` may have refine `c2` in the previous clause, the domain
# may end-up being broader. Take this example:
#
# % %{..., foo: integer()} -> binary()
# def example(%{foo: var}), do: Integer.to_string(var)
#
# % %{...} and not %{..., foo: term()} -> :error
# def example(%{}), do: :error
#
# The actual domain is:
#
# %{..., foo: not_set()} or %{..., foo: integer()}
# #=> %{..., foo: if_set(integer())}
#
# But we will infer:
#
# %{...} or %{..., foo: integer()}
# #=> %{...}
#
# We lose precision but this is exactly what we want: to have simpler types.
# Furthermore, the signature used in type checking is not refined in any way,
# so type checking is still sound.
if arg == head_arg do
Descr.union(Descr.upper_bound(no_prev_arg), d)
else
Descr.union(arg, d)
end
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
]
end
defp compute_domain([], [], [], []), do: []
# We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
@@ -374,6 +517,58 @@ defmodule Module.Types do
defp add_inferred([], args, return, -1, acc),
do: {-1, [{args, return} | Enum.reverse(acc)]}
# Compact clauses that have the same return and differ in exactly one
# argument by unioning that argument. For example:
#
# (integer(), atom() -> boolean()) and (float(), atom() -> boolean())
#
# becomes:
#
# (number(), atom() -> boolean())
#
# Arity-zero clauses have no argument position to widen.
defp group_clauses_by_return({:infer, domain, [{[_ | _], _} | _] = clauses}) do
clauses =
Enum.reduce(clauses, [], fn {args, return}, acc ->
group_clause_by_return(acc, args, return)
end)
{:infer, domain, clauses}
end
defp group_clauses_by_return(info), do: info
defp group_clause_by_return([{existing_args, return} | tail], args, return) do
case union_args(existing_args, args, [], false) do
nil ->
[{existing_args, return} | group_clause_by_return(tail, args, return)]
new_args ->
[{new_args, return} | tail]
end
end
defp group_clause_by_return([head | tail], args, return) do
[head | group_clause_by_return(tail, args, return)]
end
defp group_clause_by_return([], args, return), do: [{args, return}]
defp union_args([arg | existing], [arg | args], acc, changed?) do
union_args(existing, args, [arg | acc], changed?)
end
# Allow exactly one differing argument. That one position is widened
# with union/2. A second difference means the clauses must stay separate.
defp union_args([existing_arg | existing], [arg | args], acc, false) do
union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true)
end
defp union_args([_ | _], [_ | _], _acc, true), do: nil
# In theory fully equal args are merged on add_inferred
defp union_args([], [], acc, _changed?), do: Enum.reverse(acc)
defp with_file_meta(stack, meta) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
@@ -435,7 +630,9 @@ defmodule Module.Types do
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
local_handler: handler,
# Reverse arrow handling (nil | :cache | :use)
reverse_arrow: nil
}
end
@@ -450,34 +647,41 @@ defmodule Module.Types do
subpatterns: %{},
# Variables that are specific to the current environment/conditional
conditional_vars: nil,
# Track metadata specific to matches and guards
# Track metadata specific to patterns and guards
pattern_info: nil,
# If type checking has found an error/failure
failed: false,
# Local signatures used by local handler
local_sigs: %{},
# Track which clauses have been used across private local calls
local_used: %{}
local_used: %{},
# Cached reverse arrows
reverse_arrows: %{}
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
%{stack | mode: mode, function: function, reverse_arrow: nil}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
end
defp restore_context(later_context, %{vars: vars, failed: failed}) do
%{later_context | vars: vars, failed: failed}
defp restore_context(later_context, %{
vars: vars,
failed: failed,
reverse_arrows: reverse_arrows
}) do
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
end
## Diagnostics
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message: "this clause of #{kind} #{fun}/#{arity} is never used"
message:
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
}
end
+495 -121
View File
@@ -38,7 +38,9 @@ defmodule Module.Types.Apply do
end
fas = list(tuple([atom(), integer()]))
struct_info = list(closed_map(default: if_set(term()), field: atom()))
struct_info =
list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
shared_info = [
attributes: list(tuple([atom(), list(term())])),
@@ -236,21 +238,43 @@ defmodule Module.Types.Apply do
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
## Kernel
{Kernel, :elem, [{[open_tuple([]), integer()], dynamic()}]},
{Kernel, :is_map_key, [{[open_map(), term()], boolean()}]},
{Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
## Lists
{:lists, :member,
[
{[term(), empty_list()], atom([false])},
{[term(), non_empty_list(term())], boolean()}
]},
## Map
{Map, :delete, [{[open_map(), term()], open_map()}]},
{Map, :fetch,
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
{Map, :fetch!, [{[open_map(), term()], term()}]},
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
{Map, :get, [{[open_map(), term()], term()}]},
{Map, :get, [{[open_map(), term(), term()], term()}]},
{Map, :get_lazy, [{[open_map(), term(), fun(0)], term()}]},
{Map, :has_key?, [{[open_map(), term()], boolean()}]},
{Map, :pop, [{[open_map(), term()], tuple([term(), open_map()])}]},
{Map, :pop, [{[open_map(), term(), term()], tuple([term(), open_map()])}]},
{Map, :pop!, [{[open_map(), term()], tuple([term(), open_map()])}]},
{Map, :pop_lazy, [{[open_map(), term(), fun(0)], tuple([term(), open_map()])}]},
{Map, :put, [{[open_map(), term(), term()], open_map()}]},
{Map, :put_new, [{[open_map(), term(), term()], open_map()}]},
{Map, :put_new_lazy, [{[open_map(), term(), fun(0)], open_map()}]},
{Map, :replace, [{[open_map(), term(), term()], open_map()}]},
{Map, :replace!, [{[open_map(), term(), term()], open_map()}]},
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
{Map, :update, [{[open_map(), term(), term(), fun(1)], open_map()}]},
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
{Tuple, :delete_at, [{[open_tuple([]), integer()], dynamic(open_tuple([]))}]},
{Tuple, :duplicate, [{[term(), integer()], tuple()}]},
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
{:maps, :find,
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
@@ -267,8 +291,6 @@ defmodule Module.Types.Apply do
] do
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
true = Code.ensure_loaded?(mod)
domain_clauses =
case clauses do
[_] ->
@@ -316,6 +338,18 @@ defmodule Module.Types.Apply do
do: unquote(Macro.escape(domain_clauses))
end
def signature(:erlang, :is_integer, 3),
do:
unquote(
Macro.escape(
{:strong, [term(), integer(), integer()],
[
{[integer(), integer(), integer()], boolean()},
{[negation(integer()), integer(), integer()], atom([false])}
]}
)
)
def signature(_mod, _fun, _arity), do: :none
@doc """
@@ -374,63 +408,70 @@ defmodule Module.Types.Apply do
end
defp do_remote(:erlang, name, [left, right], expected, expr, stack, context, of_fun)
when name in [:>=, :"=<", :>, :<, :min, :max] do
when name in [:>=, :"=<", :>, :<] do
case sized_order(name, left, right, expected) do
{arg, expected, precise?, return} ->
{actual, context} = of_fun.(arg, expected, expr, stack, context)
# expected is a static type, so we can use subtype? rather than compatible?
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
{result, context}
:none ->
{left_type, context} = of_fun.(left, term(), expr, stack, context)
{right_type, context} = of_fun.(right, term(), expr, stack, context)
result = return(boolean(), [left_type, right_type], stack)
result =
if name in [:min, :max] do
union(left_type, right_type)
else
return(boolean(), [left_type, right_type], stack)
end
if is_warning(stack) do
common = intersection(left_type, right_type)
cond do
# This check is incomplete. After all, we could have the number type nested
# inside a tuple or a list and the comparison would still be valid.
# However, nested comparison between distinct numbers is very uncommon,
# so we only check the direct value here.
empty?(common) and not (number_type?(left_type) and number_type?(right_type)) ->
error = {:mismatched_comparison, left_type, right_type}
remote_error(error, :erlang, name, 2, expr, stack, context)
match?({false, _}, map_fetch_key(dynamic(common), :__struct__)) ->
error = {:struct_comparison, left_type, right_type}
remote_error(error, :erlang, name, 2, expr, stack, context)
true ->
{result, context}
end
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
remote_error(error, :erlang, name, 2, expr, stack, context)
else
{result, context}
end
end
end
defp do_remote(:erlang, name, [left, right], expected, expr, stack, context, of_fun)
when name in [:min, :max] do
# While comparison between distinct types are allowed,
# we check for disjointedness, so we effectively require
# left and right to have at least one type in common.
# Overall, it behaves as if we had this signature:
#
# integer(), integer() -> integer()
# float(), float() -> float()
# float(), integer() -> number()
# integer(), float() -> number()
# a and not number(), b and not number() -> a and b
#
# However, during inference, we type it as `a, b -> a and b` only.
{left_type, context} = of_fun.(left, expected, expr, stack, context)
{right_type, context} = of_fun.(right, expected, expr, stack, context)
result = union(left_type, right_type)
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
remote_error(error, :erlang, name, 2, expr, stack, context)
else
{result, context}
end
end
defp do_remote(:erlang, :element, [index, tuple], expected, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index - 1, 0)) ++ [expected])
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
case tuple_fetch(tuple_type, index - 1) do
{_optional?, value_type} ->
{return(value_type, [tuple_type], stack), context}
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :element, 2, expr, stack, context)
else
case tuple_fetch(tuple_type, index - 1) do
{_optional?, value_type} ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(:erlang, :element, [integer(), tuple_type], expr, stack, context)
:badtuple ->
remote_error(:erlang, :element, [integer(), tuple_type], expr, stack, context)
:badindex ->
remote_error({:badindex, index, tuple_type}, :erlang, :element, 2, expr, stack, context)
:badindex ->
remote_error({:badindex, index, tuple_type}, :erlang, :element, 2, expr, stack, context)
end
end
end
@@ -447,17 +488,46 @@ defmodule Module.Types.Apply do
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
case tuple_insert_at(tuple_type, index - 1, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :insert_element, 3, expr, stack, context)
else
case tuple_insert_at(tuple_type, index - 1, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [integer(), tuple_type, elem_type]
remote_error(:erlang, :insert_element, args_types, expr, stack, context)
:badtuple ->
args_types = [integer(), tuple_type, elem_type]
remote_error(:erlang, :insert_element, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index - 1, tuple_type}
remote_error(error, :erlang, :insert_element, 3, expr, stack, context)
:badindex ->
error = {:badindex, index - 1, tuple_type}
remote_error(error, :erlang, :insert_element, 3, expr, stack, context)
end
end
end
defp do_remote(:erlang, :setelement, [index, tuple, elem], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :setelement, 3, expr, stack, context)
else
case tuple_replace_at(tuple_type, index - 1, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [integer(), tuple_type, elem_type]
remote_error(:erlang, :setelement, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, :erlang, :setelement, 3, expr, stack, context)
end
end
end
@@ -466,16 +536,172 @@ defmodule Module.Types.Apply do
tuple_type = open_tuple(List.duplicate(term(), max(index, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
case tuple_delete_at(tuple_type, index - 1) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type], stack), context}
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :delete_element, 2, expr, stack, context)
else
case tuple_delete_at(tuple_type, index - 1) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(:erlang, :delete_element, [integer(), tuple_type], expr, stack, context)
:badtuple ->
remote_error(:erlang, :delete_element, [integer(), tuple_type], expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, :erlang, :delete_element, 2, expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, :erlang, :delete_element, 2, expr, stack, context)
end
end
end
defp do_remote(Kernel, :elem, [tuple, index], expected, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index, 0)) ++ [expected])
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Kernel, :elem, 2, expr, stack, context)
else
case tuple_fetch(tuple_type, index) do
{_optional?, value_type} ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(Kernel, :elem, [tuple_type, integer()], expr, stack, context)
:badindex ->
remote_error({:badindex, index + 1, tuple_type}, Kernel, :elem, 2, expr, stack, context)
end
end
end
defp do_remote(Kernel, :put_elem, [tuple, index, elem], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index + 1, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Kernel, :put_elem, 3, expr, stack, context)
else
case tuple_replace_at(tuple_type, index, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [tuple_type, integer(), elem_type]
remote_error(Kernel, :put_elem, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index + 1, tuple_type}
remote_error(error, Kernel, :put_elem, 3, expr, stack, context)
end
end
end
defp do_remote(Tuple, :duplicate, [elem, size], _, expr, stack, context, of_fun)
when is_integer(size) and size >= 0 do
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
{return(tuple(List.duplicate(elem_type, size)), [elem_type], stack), context}
end
defp do_remote(Tuple, :insert_at, [tuple, index, elem], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index, 0)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Tuple, :insert_at, 3, expr, stack, context)
else
case tuple_insert_at(tuple_type, index, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [tuple_type, integer(), elem_type]
remote_error(Tuple, :insert_at, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, Tuple, :insert_at, 3, expr, stack, context)
end
end
end
defp do_remote(Tuple, :delete_at, [tuple, index], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index + 1, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Tuple, :delete_at, 2, expr, stack, context)
else
case tuple_delete_at(tuple_type, index) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(Tuple, :delete_at, [tuple_type, integer()], expr, stack, context)
:badindex ->
error = {:badindex, index + 1, tuple_type}
remote_error(error, Tuple, :delete_at, 2, expr, stack, context)
end
end
end
defp do_remote(:lists, :member, [arg, list] = args, expected, expr, stack, context, of_fun)
when is_list(list) do
if list == [] or Enum.any?(list, &match?({:|, _, [_, _]}, &1)) do
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
else
case booleaness(expected) do
{polarity, _maybe_or_always} ->
{return, acc} =
case polarity do
true -> {@atom_true, none()}
false -> {@atom_false, term()}
end
{expected, singleton?, context} =
Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} ->
{type, context} = of_fun.(literal, term(), expr, stack, context)
if singleton?(type) do
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
{acc, all_singleton?, context}
else
acc = if polarity, do: union(acc, type), else: acc
{acc, false, context}
end
end)
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
cond do
# expected can have dynamic terms but, since we have verified
# them to be singletons, we can compute the upper bound.
singleton? and subtype?(arg_type, upper_bound(expected)) ->
{return, context}
# Singleton types with reverse polarity are negated, so we don't check for disjoint
(singleton? and not polarity) or not is_warning(stack) ->
{return(boolean(), [arg_type, expected], stack), context}
# Nothing in common between left and right, emit a warning
disjoint?(arg_type, expected) ->
error = {:mismatched_comparison, arg_type, list(expected)}
remote_error(error, :lists, :member, 2, expr, stack, context)
true ->
{return(boolean(), [arg_type, expected], stack), context}
end
_ ->
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
end
end
end
@@ -519,17 +745,29 @@ defmodule Module.Types.Apply do
{expected, precise?} =
case fun do
:length when :erlang.xor(polarity, literal > 0) ->
{@empty_list, literal == 0}
:length when polarity and literal == 0 ->
{@empty_list, true}
:length when not polarity and literal == 0 ->
{@non_empty_list, true}
:length when polarity ->
{@non_empty_list, false}
:length ->
{@non_empty_list, literal == 0}
{@list, false}
:map_size when :erlang.xor(polarity, literal > 0) ->
{@empty_map, literal == 0}
:map_size when polarity and literal == 0 ->
{@empty_map, true}
:map_size when not polarity and literal == 0 ->
{@non_empty_map, true}
:map_size when polarity ->
{@non_empty_map, false}
:map_size ->
{@non_empty_map, literal == 0}
{open_map(), false}
:tuple_size when polarity ->
{tuple(List.duplicate(term(), literal)), true}
@@ -539,6 +777,8 @@ defmodule Module.Types.Apply do
end
{actual, context} = of_fun.(arg, expected, expr, stack, context)
# expected here is a static type, so subtyping relation is fine
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
# We can skip return compare because literal is always an integer,
@@ -553,7 +793,7 @@ defmodule Module.Types.Apply do
compare(name, arg, literal, false, expr, stack, context, of_fun)
{boolean, _maybe_or_always} ->
{literal_type, context} = of_fun.(literal, term(), expr, stack, context)
{type, context} = of_fun.(literal, term(), expr, stack, context)
{polarity, return} =
case boolean do
@@ -563,28 +803,28 @@ defmodule Module.Types.Apply do
# This logic mirrors the code in `Pattern.of_pattern_tree`
# If it is a singleton, we can always be precise
if singleton?(literal_type) do
expected = if polarity, do: literal_type, else: negation(literal_type)
if singleton?(type) do
expected = if polarity, do: type, else: negation(type)
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
result = if subtype?(arg_type, expected), do: return, else: boolean()
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
# Because reverse polarity means we will infer negated types
# (which are naturally disjoint), we skip checks in such cases
skip_check? = not polarity
return_compare(name, arg_type, literal_type, result, skip_check?, expr, stack, context)
return_compare(name, arg_type, type, result, skip_check?, expr, stack, context)
else
expected =
cond do
# We are checking for `not x == 1` or similar, we can't say anything about x
polarity == false -> term()
# We are checking for `x == 1`, make sure x is integer or float
name in [:==, :"/="] -> numberize(literal_type)
name in [:==, :"/="] -> numberize(type)
# Otherwise we have the literal type as is
true -> literal_type
true -> type
end
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
return_compare(name, arg_type, literal_type, boolean(), false, expr, stack, context)
return_compare(name, arg_type, type, boolean(), false, expr, stack, context)
end
end
end
@@ -621,28 +861,44 @@ defmodule Module.Types.Apply do
end
end
defp sized_order(name, left, right, expected) do
if name in [:>=, :"=<", :>, :<] do
case {left, right} do
{{{:., _, [:erlang, fun]}, _, [arg]}, size} when is_data_size(fun, size) ->
case booleaness(expected) do
{true, _} -> sized_order(name, fun, size, arg, @atom_true)
{false, _} -> sized_order(invert_order(name), fun, size, arg, @atom_false)
_ -> :none
end
defp mismatched_ordered_comparison(left_type, right_type, stack) do
if is_warning(stack) do
cond do
# These checks are incomplete. After all, we could have numbers and
# structs nested inside tuples or lists, but we only check the direct
# value here.
not (number_type?(left_type) and number_type?(right_type)) and
disjoint?(left_type, right_type) ->
{:mismatched_comparison, left_type, right_type}
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
case booleaness(expected) do
{true, _} -> sized_order(invert_order(name), fun, size, arg, @atom_true)
{false, _} -> sized_order(name, fun, size, arg, @atom_false)
_ -> :none
end
match?({false, _}, map_fetch_key(dynamic(left_type), :__struct__)) and
match?({false, _}, map_fetch_key(dynamic(right_type), :__struct__)) ->
{:struct_comparison, left_type, right_type}
_ ->
:none
true ->
nil
end
else
:none
end
end
defp sized_order(name, left, right, expected) do
case {left, right} do
{{{:., _, [:erlang, fun]}, _, [arg]}, size} when is_data_size(fun, size) ->
case booleaness(expected) do
{true, _} -> sized_order(name, fun, size, arg, @atom_true)
{false, _} -> sized_order(invert_order(name), fun, size, arg, @atom_false)
_ -> :none
end
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
case booleaness(expected) do
{true, _} -> sized_order(mirror_order(name), fun, size, arg, @atom_true)
{false, _} -> sized_order(invert_order(mirror_order(name)), fun, size, arg, @atom_false)
_ -> :none
end
_ ->
:none
end
end
@@ -689,6 +945,11 @@ defmodule Module.Types.Apply do
defp invert_order(:>), do: :"=<"
defp invert_order(:<), do: :>=
defp mirror_order(:>=), do: :"=<"
defp mirror_order(:"=<"), do: :>=
defp mirror_order(:>), do: :<
defp mirror_order(:<), do: :>
@doc """
Returns the domain of an unknown module.
@@ -731,17 +992,40 @@ defmodule Module.Types.Apply do
{info, filter_domain(info, expected, 2), context}
end
def remote_domain(Kernel, :is_map_key, [_map, key], expected, _meta, _stack, context)
when is_atom(key) do
info =
{:strong, [open_map(), term()],
[
{[open_map([{key, term()}]), term()], atom([true])},
{[open_map([{key, not_set()}]), term()], atom([false])}
]}
{info, filter_domain(info, expected, 2), context}
end
def remote_domain(:erlang, :map_get, [key, _], expected, _meta, _stack, context)
when is_atom(key) do
domain = [term(), open_map([{key, expected}])]
{{:strong, nil, [{domain, term()}]}, domain, context}
end
def remote_domain(Map, :fetch!, [_, key], expected, _meta, _stack, context) when is_atom(key) do
domain = [open_map([{key, expected}]), term()]
{{:strong, nil, [{domain, term()}]}, domain, context}
end
def remote_domain(:maps, :get, [key, _], expected, _meta, _stack, context) when is_atom(key) do
domain = [term(), open_map([{key, expected}])]
{{:strong, nil, [{domain, term()}]}, domain, context}
end
def remote_domain(Map, :replace!, [_, key, _], _expected, _meta, _stack, context)
when is_atom(key) do
domain = [open_map([{key, term()}]), term(), term()]
{{:strong, nil, [{domain, open_map()}]}, domain, context}
end
def remote_domain(:maps, :update, [key, _, _], _expected, _meta, _stack, context)
when is_atom(key) do
domain = [term(), term(), open_map([{key, term()}])]
@@ -821,6 +1105,36 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(Map, :delete, _info, [map, key] = args_types, stack) do
case map_update(map, key, not_set(), false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :delete, args_types)}
{:error, _errors} -> {:ok, map}
end
end
defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{_, value} ->
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
{:ok, return(result, args_types, stack)}
:badmap ->
{:error, badremote(Map, :fetch, args_types)}
:error ->
{:error, {:badkeydomain, map, key, atom([:error])}}
end
end
defp remote_apply(Map, :fetch!, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{:ok, value} -> {:ok, return(value, args_types, stack)}
:badmap -> {:error, badremote(Map, :fetch!, args_types)}
:error -> {:error, {:badkeydomain, map, key, "raise"}}
end
end
defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)}
@@ -902,6 +1216,14 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(Map, :put, _info, [map, key, value] = args_types, stack) do
case map_update(map, key, value, false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :put, args_types)}
{:error, _errors} -> {:ok, map}
end
end
defp remote_apply(Map, :pop!, _info, [map, key] = args_types, stack) do
case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)}
@@ -920,6 +1242,16 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(Map, :replace!, _info, [map, key, value] = args_types, stack) do
fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
case map_update_fun(map, key, fun, false, false) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :replace!, args_types)}
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
end
end
defp remote_apply(Map, :replace_lazy, _info, args_types, stack) do
map_update_or_replace_lazy(:replace_lazy, args_types, stack, "do nothing")
end
@@ -1268,33 +1600,62 @@ defmodule Module.Types.Apply do
## Local
def local(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
{local_info, domain, context} = local_domain(fun, args, expected, meta, stack, context)
{updated_used?, info, domain, context} =
local_domain(fun, args, expected, meta, stack, context)
{args_types, context} =
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
local_apply(local_info, fun, args_types, expr, stack, context)
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
{_indexes, type, context} -> {type, context}
{type, context} -> {type, context}
end
end
def local_arrows(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
{updated_used?, info, domain, context} =
local_domain(fun, args, expected, meta, stack, context)
{args_types, context} =
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
{indexes, _type, context} -> {indexes_to_arrows(indexes, info), context}
{type, context} -> {[{Enum.map(args, fn _ -> type end), type}], context}
end
end
defp indexes_to_arrows(indexes, {_, _domain, clauses}),
do: indexes_to_arrows(Enum.reverse(indexes), clauses, 0)
defp indexes_to_arrows([index | indexes], [clause | clauses], index),
do: [clause | indexes_to_arrows(indexes, clauses, index + 1)]
defp indexes_to_arrows([_ | _] = indexes, [_clause | clauses], index),
do: indexes_to_arrows(indexes, clauses, index + 1)
defp indexes_to_arrows([], _clauses, _index),
do: []
defp local_domain(fun, args, expected, meta, stack, context) do
arity = length(args)
case stack.local_handler.(meta, {fun, arity}, stack, context) do
false ->
{{false, :none}, List.duplicate(term(), arity), context}
{false, :none, List.duplicate(term(), arity), context}
{kind, info, context} ->
update_used? = is_warning(stack) and kind == :defp
if info == :none do
{{update_used?, :none}, List.duplicate(term(), arity), context}
{update_used?, :none, List.duplicate(term(), arity), context}
else
{{update_used?, info}, filter_domain(info, expected, arity), context}
{update_used?, info, filter_domain(info, expected, arity), context}
end
end
end
defp local_apply({update_used?, :none}, fun, args_types, _expr, _stack, context) do
defp local_apply(update_used?, :none, fun, args_types, _expr, _stack, context) do
if update_used? do
{dynamic(), put_in(context.local_used[{fun, length(args_types)}], [])}
else
@@ -1302,7 +1663,7 @@ defmodule Module.Types.Apply do
end
end
defp local_apply({update_used?, info}, fun, args_types, expr, stack, context) do
defp local_apply(update_used?, info, fun, args_types, expr, stack, context) do
case local_apply(info, args_types, stack) do
{indexes, type} ->
context =
@@ -1314,7 +1675,7 @@ defmodule Module.Types.Apply do
context
end
{type, context}
{indexes, type, context}
:error ->
error = {:badlocal, info, args_types, expr, context}
@@ -1435,9 +1796,9 @@ defmodule Module.Types.Apply do
domain(domain, clauses)
end
defp filter_domain({_type, domain, clauses}, expected, arity) do
defp filter_domain({_type, domain, clauses}, expected, _arity) do
case filter_domain(clauses, expected, [], true) do
:none -> List.duplicate(term(), arity)
:none -> domain(domain, clauses)
:all -> domain(domain, clauses)
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end)
end
@@ -1537,7 +1898,7 @@ defmodule Module.Types.Apply do
{message, to_trace, hints} =
case reason do
{:badarg, domain} ->
{:badarg, domain, _empty?} ->
message = """
incompatible types given on #{mfa_or_call}:
@@ -1672,6 +2033,29 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({{:negindex, index}, mfac, expr, context}) do
traces = collect_traces(expr, context)
{mod, fun, arity, _converter} = mfac
mfa = Exception.format_mfa(mod, fun, arity)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a non-negative integer as index in #{mfa}:
#{expr_to_string(expr) |> indent(4)}
got the index:
#{index}
""",
format_traces(traces)
])
}
end
def format_diagnostic({{:badkeydomain, map, key, error}, mfac, expr, context}) do
{mod, fun, arity, _converter} = mfac
mfa = Exception.format_mfa(mod, fun, arity)
@@ -1845,7 +2229,7 @@ defmodule Module.Types.Apply do
end
def format_diagnostic({{:mismatched_comparison, left, right}, mfac, expr, context}) do
{_, name, _, _} = mfac
{mod, name, _, _} = mfac
traces = collect_traces(expr, context)
%{
@@ -1859,7 +2243,7 @@ defmodule Module.Types.Apply do
given types:
#{type_comparison_to_string(name, left, right) |> indent(4)}
#{type_comparison_to_string(mod, name, left, right) |> indent(4)}
""",
format_traces(traces),
"""
@@ -1873,7 +2257,7 @@ defmodule Module.Types.Apply do
end
def format_diagnostic({{:struct_comparison, left, right}, mfac, expr, context}) do
{_, name, _, _} = mfac
{mod, name, _, _} = mfac
traces = collect_traces(expr, context)
%{
@@ -1887,7 +2271,7 @@ defmodule Module.Types.Apply do
given types:
#{type_comparison_to_string(name, left, right) |> indent(4)}
#{type_comparison_to_string(mod, name, left, right) |> indent(4)}
""",
format_traces(traces),
"""
@@ -1902,17 +2286,10 @@ defmodule Module.Types.Apply do
end
def format_diagnostic({:undefined, :badmodule, module, fun, arity}) do
top =
if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}"
else
Exception.format_mfa(module, fun, arity)
end
%{
message:
IO.iodata_to_binary([
top,
Exception.format_mfa(module, fun, arity),
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)",
@@ -1922,14 +2299,6 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({:undefined, {:badfunction, _}, module, :__struct__, 0}) do
%{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
group: true
}
end
def format_diagnostic({:undefined, {:badfunction, _}, module, fun, arity}) do
_ = Code.ensure_loaded(module)
@@ -1985,8 +2354,12 @@ defmodule Module.Types.Apply do
alias Inspect.Algebra, as: IA
defp type_comparison_to_string(fun, left, right) do
{_, fun, _, _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right])
defp type_comparison_to_string(:lists, :member, left, right) do
type_comparison_to_string(Kernel, :in, left, right)
end
defp type_comparison_to_string(mod, fun, left, right) do
{_, fun, _, _} = :elixir_rewrite.erl_to_ex(mod, fun, [left, right])
{fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]}
|> Code.Formatter.to_algebra()
@@ -2019,7 +2392,8 @@ defmodule Module.Types.Apply do
defp args_to_quoted_string(args_types, domain, converter) do
docs =
Enum.zip_with(args_types, domain, fn actual, expected ->
if compatible?(actual, expected) or not has_simple_difference?(actual, expected) do
if compatible?(actual, expected) or not has_simple_difference?(actual, expected) or
term_type?(actual) do
actual |> to_quoted() |> Code.Formatter.to_algebra()
else
common = intersection(actual, expected)
File diff suppressed because it is too large Load Diff
+506 -310
View File
@@ -15,7 +15,6 @@ defmodule Module.Types.Expr do
list_of_modules = list(atom())
@try_catch atom([:error, :exit, :throw])
@atom_true atom([true])
@caller closed_map(
__struct__: atom([Macro.Env]),
@@ -35,13 +34,9 @@ defmodule Module.Types.Expr do
versioned_vars: open_map()
)
# An annotation for terms where the reverse arrow is not yet fully defined.
# Also revisit all users of dynamic() in this module in a later date.
@pending term()
# We do not make exception dynamic on purpose. If you do a blank rescue,
# then we will assume you need to statically handle all possible exceptions.
@exception open_map(__struct__: atom(), __exception__: @atom_true)
@exception open_map(__struct__: atom(), __exception__: term())
args_or_arity = union(list(term()), integer())
@@ -59,6 +54,9 @@ defmodule Module.Types.Expr do
)
)
@falsy atom([false, nil])
@truthy negation(atom([false, nil]))
# :atom
def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom),
do: {atom([atom]), context}
@@ -139,7 +137,7 @@ defmodule Module.Types.Expr do
end
# left = right
def of_expr({:=, _, [left_expr, right_expr]} = match, expected, expr, stack, context) do
def of_expr({:=, meta, [left_expr, right_expr]} = match, expected, expr, stack, context) do
{left_expr, right_expr} = repack_match(left_expr, right_expr)
case left_expr do
@@ -148,14 +146,20 @@ defmodule Module.Types.Expr do
of_expr(right_expr, expected, expr, stack, context)
_ ->
type_fun = fn pattern_type, context ->
# See if we can use the expected type to further refine the pattern type,
# if we cannot, use the pattern type as that will fail later on.
{_ok_or_error, type} = compatible_intersection(dynamic(pattern_type), expected)
of_expr(right_expr, type, expr, stack, context)
end
type_fun =
fn pattern_type, context ->
# See if we can use the expected type to further refine the pattern type,
# if we cannot, use the pattern type as that will fail later on.
type = intersection(pattern_type, expected)
type = if empty?(type), do: pattern_type, else: type
{result, context} = of_expr(right_expr, type, expr, stack, context)
Pattern.of_match(left_expr, type_fun, match, stack, context)
# The function may still return a too broad type,
# so we refine once again to assign the most appropriate to the pattern.
{intersection(result, expected), context}
end
Pattern.of_match(left_expr, type_fun, match, meta, stack, context)
end
end
@@ -210,28 +214,47 @@ defmodule Module.Types.Expr do
stack,
context
) do
# We pass the expected type as `term()` because the struct update
# operator already expects it to be a map at this point.
{map_type, context} = of_expr(map, term(), struct, stack, context)
{info, context} = Of.struct_info(module, :expr, meta, stack, context)
context =
with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__),
{:finite, [^module]} <- atom_fetch(struct_key_type) do
context
else
_ ->
error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
end
if info do
# We pass the expected type as `term()` because the struct update
# operator already expects it to be a map at this point.
{map_type, context} = of_expr(map, term(), struct, stack, context)
Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
# TODO: Once we support typed structs, we need to type check them here
{type, context} = of_expr(value, term(), expr, stack, context)
context =
with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__),
{:finite, [^module]} <- atom_fetch(struct_key_type) do
context
else
_ ->
error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
end
case map_put_key(acc, key, type) do
{:ok, acc} -> {acc, context}
_ -> {acc, context}
end
end)
Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
context =
if Enum.any?(info, &(&1.field == key)) do
context
else
Of.unknown_struct_field(module, key, :expr, meta, stack, context)
end
# TODO: Once we support typed structs, we need to type check them here
{type, context} = of_expr(value, term(), expr, stack, context)
case map_put_key(acc, key, type) do
{:ok, acc} -> {acc, context}
_ -> {acc, context}
end
end)
else
context =
Enum.reduce(pairs, context, fn {_key, value}, context ->
{_type, context} = of_expr(value, term(), expr, stack, context)
context
end)
{error_type(), context}
end
end
# %{...}
@@ -263,38 +286,152 @@ defmodule Module.Types.Expr do
of_expr(post, expected, post, stack, context)
end
def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected, expr, stack, original) do
clauses
|> reduce_non_empty({none(), original}, fn
{:->, meta, [[head], body]}, {acc, context}, last? ->
{head_type, context} = of_expr(head, @pending, head, stack, context)
def of_expr(
{:cond, meta,
[
[
{:do,
[
{:->, pos_meta, [[pos_head], pos_body]},
{:->, _neg_meta, [[neg_head], neg_body]}
]}
]
]},
expected,
expr,
stack,
acc_context
)
when is_atom(neg_head) and neg_head not in [false, nil] do
cache_result(meta, stack, acc_context, fn ->
{pos_head_type, context} =
of_expr(pos_head, term(), pos_head, %{stack | reverse_arrow: :cache}, acc_context)
context =
if is_warning(stack) do
case truthiness(head_type) do
:always_true when not last? ->
warning = {:badcond, "always match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
context =
maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false)
:always_false ->
warning = {:badcond, "never match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
{_, truthy_context} =
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :use}, context)
_ ->
context
end
else
context
end
# Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context)
{body_type, context} = of_expr(body, expected, expr, stack, context)
{union(body_type, acc), Of.reset_vars(context, original)}
{pos_body_type, pos_body_context} =
of_expr(pos_body, expected, expr, stack, truthy_context)
# Reset the context vars once again to compute the falsy type
context = Of.reset_vars(pos_body_context, context)
{_, falsy_context} =
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :use}, context)
falsy_context = reset_warnings(falsy_context, context)
{neg_body_type, neg_body_context} =
of_expr(neg_body, expected, expr, stack, falsy_context)
body_type = union(pos_body_type, neg_body_type)
context =
cond do
empty?(pos_body_type) -> Of.reset_vars(neg_body_context, falsy_context)
empty?(neg_body_type) -> Of.reset_vars(neg_body_context, truthy_context)
true -> Of.reset_vars(neg_body_context, acc_context)
end
dynamic_unless_static({body_type, context}, stack)
end)
|> dynamic_unless_static(stack)
end
def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, expr, stack, context) do
{case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context)
def of_expr({:cond, meta, [[{:do, clauses}]]}, expected, expr, stack, context) do
cache_result(meta, stack, context, fn ->
{body_type, acc_context} =
reduce_non_empty(clauses, {none(), context}, fn
{:->, meta, [[head], body]}, {acc, context}, last? ->
{head_type, context} =
of_expr(head, term(), head, %{stack | reverse_arrow: :cache}, context)
context =
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
{_, truthy_context} =
of_expr(head, @truthy, head, %{stack | reverse_arrow: :use}, context)
# Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context)
{body_type, body_context} = of_expr(body, expected, expr, stack, truthy_context)
# Reset the context vars to the head definition to compute the falsy type
context = Of.reset_vars(body_context, context)
context =
if last? do
context
else
{_, falsy_context} =
of_expr(head, @falsy, head, %{stack | reverse_arrow: :use}, context)
reset_warnings(falsy_context, context)
end
{union(body_type, acc), context}
end)
dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack)
end)
end
def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context)
when stack.reverse_arrow == :use do
version = Keyword.fetch!(meta, :version)
clauses = Map.fetch!(context.reverse_arrows, version)
original = context
context =
clauses
|> Enum.reduce({0, []}, fn {_arg_type, body_type, _clause} = triplet, {counter, acc} ->
if disjoint?(body_type, expected) do
{counter + 1, acc}
else
{counter, [triplet | acc]}
end
end)
|> case do
# Nothing skipped, just return the context as is
{0, _} ->
context
# If there is a single clause, we assume it is always evaluated
# by doing reverse arrows and incorporting all variables into the context.
# We have to evaluate the head again but it might have been preferred
# if we could somehow merge a previously computed context.
{_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} ->
{case_type, context} = of_expr(case_expr, arg_type, case_expr, stack, context)
{patterns, guards} = extract_head(head)
previous = Pattern.init_previous()
info = {{:case, meta, case_expr, case_type}, head}
{_, _, _, _, context} =
Pattern.of_head(patterns, guards, [case_type], previous, info, meta, stack, context)
{_, context} = of_expr(body, expected, body, stack, context)
reset_warnings(context, original)
{_, filtered} ->
case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2))
{_, context} = of_expr(case_expr, case_expected, case_expr, stack, context)
reset_warnings(context, original)
end
dynamic_unless_static({expected, context}, stack)
end
def of_expr({:case, meta, [case_expr, [do: clauses]]}, expected, _expr, stack, base_context) do
{case_type, context} =
of_expr(case_expr, term(), case_expr, %{stack | reverse_arrow: :cache}, base_context)
info = {:case, meta, case_expr, case_type}
added_meta =
if Macro.quoted_literal?(case_expr) do
@@ -306,145 +443,226 @@ defmodule Module.Types.Expr do
# If the expression is generated or the construct is a literal,
# it is most likely a macro code. However, if no clause is matched,
# we should still check for that.
if added_meta != [] do
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
else
clauses
end
|> of_case_clauses(case_type, expected, meta, case_expr, expr, stack, context)
|> dynamic_unless_static(stack)
end
# fn pat -> expr end
def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do
[{:->, _, [head, _]} | _] = clauses
{patterns, _guards} = extract_head(head)
domain = Enum.map(patterns, fn _ -> dynamic() end)
{acc, context} =
of_clauses_fun(clauses, domain, @pending, nil, :fn, stack, context, [], fn
trees, body, context, acc ->
args = Pattern.of_domain(trees, stack, context)
add_inferred(acc, args, body)
end)
{fun_from_inferred_clauses(acc), context}
end
def of_expr({:try, _meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
{after_block, blocks} = Keyword.pop(blocks, :after)
{else_block, blocks} = Keyword.pop(blocks, :else)
{type, context} =
if else_block do
{type, context} = of_expr(body, @pending, body, stack, original)
info = {:try_else, type}
of_clauses(else_block, [type], expected, expr, info, stack, context, none())
clauses =
if added_meta != [] do
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
else
of_expr(body, expected, expr, stack, original)
clauses
end
{type, context} =
blocks
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
{:rescue, clauses}, acc_context ->
Enum.reduce(clauses, acc_context, fn
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
{type, context} =
of_rescue(var, exceptions, body, expr, :rescue, meta, stack, context)
cache_arrows(meta, stack, fn ->
acc = {false, none(), []}
{union(type, acc), context}
{{none?, body_acc, clauses_acc}, context} =
of_clauses_fun(clauses, [case_type], info, stack, context, acc, fn
trees, precise?, {:->, _, [_, body]} = clause, context, acc ->
# Compute the arg type based on the clause itself
[arg_type] = Pattern.of_domain(trees, stack, context)
{:->, meta, [[var], body]}, {acc, context} ->
{type, context} =
of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context)
# Now we refine the case_expr context and use it to compute the body
{_, refined_context} =
of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context)
{union(type, acc), context}
end)
{body_type, context} =
of_expr(body, expected, body, stack, reset_warnings(refined_context, context))
{:catch, clauses}, {acc, context} ->
args = [@try_catch, dynamic()]
of_clauses(clauses, args, expected, expr, :try_catch, stack, context, acc)
end)
|> dynamic_unless_static(stack)
# Now we compute the return type and the clauses for reverse arrow
{none?, body_acc, clauses_acc} = acc
if after_block do
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
{type, context}
else
{type, context}
end
end
if precise? and empty?(body_type) do
{{true, body_acc, clauses_acc}, context}
else
[arg_type] = Pattern.of_domain(trees, stack, context)
clauses_acc = [{arg_type, body_type, clause} | clauses_acc]
{{none?, union(body_type, body_acc), clauses_acc}, context}
end
end)
@timeout_type union(integer(), atom([:infinity]))
context =
if none? or stack.mode != :static do
head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2))
def of_expr({:receive, _meta, [blocks]}, expected, expr, stack, original) do
blocks
|> Enum.reduce({none(), original}, fn
{:do, {:__block__, _, []}}, acc_context ->
acc_context
{_, refined_context} =
of_expr(case_expr, head_type, case_expr, %{stack | reverse_arrow: :use}, context)
{:do, clauses}, {acc, context} ->
of_clauses(clauses, [dynamic()], expected, expr, :receive, stack, context, acc)
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
{body_type, context} = of_expr(body, expected, expr, stack, context)
if compatible?(timeout_type, @timeout_type) do
{union(body_type, acc), Of.reset_vars(context, original)}
reset_warnings(refined_context, context)
else
error = {:badtimeout, timeout_type, timeout, context}
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
context
end
{body_acc, clauses_acc, context}
end)
|> dynamic_unless_static(stack)
end
def of_expr({:for, meta, [_ | _] = args}, expected, expr, stack, context) do
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
# fn pat -> expr end
def of_expr({:fn, meta, clauses}, _expected, _expr, stack, context) do
cache_result(meta, stack, context, fn ->
[{:->, _, [head, _]} | _] = clauses
{patterns, _guards} = extract_head(head)
domain = Enum.map(patterns, fn _ -> dynamic() end)
# We don't need to type check uniq, as it is a compile-time boolean.
# We handle reduce and into accordingly instead.
if Keyword.has_key?(opts, :reduce) do
reduce = Keyword.fetch!(opts, :reduce)
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
# TODO: We need to type check against dynamic() instead of using reduce_type
# because this is recursive. We need to infer the block type first.
of_clauses(block, [dynamic()], expected, expr, :for_reduce, stack, context, reduce_type)
else
# TODO: Use the collectable protocol for the output
into = Keyword.get(opts, :into, [])
{into_type, into_kind, context} = for_into(into, meta, stack, context)
{block_type, context} = of_expr(block, @pending, block, stack, context)
{acc, context} =
of_clauses_fun(clauses, domain, :fn, stack, context, [], fn
trees, _precise?, {:->, _, [_, body]}, context, acc ->
{body_type, context} = of_expr(body, term(), body, stack, context)
args_types = Pattern.of_domain(trees, stack, context)
{add_inferred(acc, args_types, body_type), context}
end)
case into_kind do
:bitstring ->
case compatible_intersection(block_type, bitstring()) do
{:ok, intersection} ->
{return_union(into_type, intersection, stack), context}
{:error, _} ->
error = {:badbitbody, block_type, block, context}
{error_type(), error(__MODULE__, error, meta, stack, context)}
end
:non_empty_list ->
{return_union(into_type, non_empty_list(block_type), stack), context}
:none ->
{into_type, context}
end
end
{fun_from_inferred_clauses(acc), context}
end)
end
# TODO: with pat <- expr do expr end
def of_expr({:with, _meta, [_ | _] = clauses}, _expected, _expr, stack, original) do
{clauses, [options]} = Enum.split(clauses, -1)
context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2))
context = Enum.reduce(options, context, &with_option(&1, stack, &2, original))
{dynamic(), context}
def of_expr({:try, meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
cache_result(meta, stack, original, fn ->
{after_block, blocks} = Keyword.pop(blocks, :after)
{else_block, blocks} = Keyword.pop(blocks, :else)
{type, context} =
if else_block do
{type, context} = of_expr(body, term(), body, stack, original)
info = {:try_else, type}
of_clauses(else_block, [type], expected, info, stack, context, none())
else
of_expr(body, expected, expr, stack, original)
end
{type, context} =
blocks
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
{:rescue, clauses}, {_acc, %{failed: failed?}} = acc_context ->
Enum.reduce(clauses, acc_context, fn
{:->, meta, [[head], body]}, {acc, context} ->
{failed?, context} = reset_failed(context, failed?)
context =
case head do
{:in, meta, [var, mods]} ->
of_rescue(var, mods, expr, :rescue, meta, stack, context)
var ->
of_rescue(var, [], var, :anonymous_rescue, meta, stack, context)
end
{type, context} = of_expr(body, expected, body, stack, context)
{union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
end)
{:catch, clauses}, {acc, context} ->
args = [@try_catch, dynamic()]
of_clauses(clauses, args, expected, :try_catch, stack, context, acc)
end)
|> dynamic_unless_static(stack)
if after_block do
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
{type, context}
else
{type, context}
end
end)
end
@timeout_type union(integer(), atom([:infinity]))
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
cache_result(meta, stack, original, fn ->
blocks
|> Enum.reduce({none(), original}, fn
{:do, {:__block__, _, []}}, acc_context ->
acc_context
{:do, clauses}, {acc, context} ->
of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc)
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
{body_type, context} = of_expr(body, expected, expr, stack, context)
if compatible?(timeout_type, @timeout_type) do
{union(body_type, acc), Of.reset_vars(context, original)}
else
error = {:badtimeout, timeout_type, timeout, context}
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
end
end)
|> dynamic_unless_static(stack)
end)
end
def of_expr({:for, meta, [_ | _] = args}, expected, expr, stack, context) do
cache_result(meta, stack, context, fn ->
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
# We don't need to type check uniq, as it is a compile-time boolean.
# We handle reduce and into accordingly instead.
if Keyword.has_key?(opts, :reduce) do
reduce = Keyword.fetch!(opts, :reduce)
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
# TODO: We need to type check against dynamic() instead of using reduce_type
# because this is recursive. We need to infer the block type first.
args = [dynamic()]
of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type)
else
# TODO: Use the collectable protocol for the output
into = Keyword.get(opts, :into, [])
{into_type, into_kind, context} = for_into(into, meta, stack, context)
case into_kind do
:bitstring ->
{block_type, context} = of_expr(block, bitstring(), block, stack, context)
intersection = intersection(block_type, bitstring())
if empty?(intersection) do
error = {:badbitbody, block_type, block, context}
{error_type(), error(__MODULE__, error, meta, stack, context)}
else
{union(into_type, intersection), context}
end
:non_empty_list ->
expected =
case list_hd(expected) do
{:ok, head} -> head
_ -> term()
end
{block_type, context} = of_expr(block, expected, block, stack, context)
{union(into_type, non_empty_list(block_type)), context}
:none ->
{_, context} = of_expr(block, term(), block, stack, context)
{into_type, context}
end
|> dynamic_unless_static(stack)
end
end)
end
def of_expr({:with, meta, [_ | _] = clauses}, expected, _expr, stack, original) do
cache_result(meta, stack, original, fn ->
{clauses, [[do: do_block] ++ options]} = Enum.split(clauses, -1)
{else_types, context} =
Enum.reduce(clauses, {none(), original}, &with_clause(&1, stack, &2))
{do_result, context} = of_expr(do_block, expected, do_block, stack, context)
context = Of.reset_vars(context, original)
{else_result, context} =
case options do
[else: clauses] ->
info = {:with_else, else_types}
of_clauses(clauses, [else_types], expected, info, stack, context, none())
[] ->
{else_types, context}
end
dynamic_unless_static({union(do_result, else_result), context}, stack)
end)
end
def of_expr({{:., _, [fun]}, _, args} = call, _expected, _expr, stack, context) do
@@ -452,7 +670,7 @@ defmodule Module.Types.Expr do
# TODO: Perform inference based on the strong domain of a function
{args_types, context} =
Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
Enum.map_reduce(args, context, &of_expr(&1, term(), &1, stack, &2))
Apply.fun(fun_type, args_types, call, stack, context)
end
@@ -509,8 +727,7 @@ defmodule Module.Types.Expr do
# var
def of_expr({_, meta, _} = var, expected, expr, stack, context) when is_var(var) do
version = Keyword.fetch!(meta, :version)
{type, context} = Of.refine_body_var(version, expected, expr, stack, context)
{type, Pattern.of_changed([version], stack, context)}
Of.refine_body_var(version, expected, expr, stack, context)
end
## Tuples
@@ -536,55 +753,57 @@ defmodule Module.Types.Expr do
## Try
defp of_rescue(var, exceptions, body, expr, info, meta, stack, original) do
args = [__exception__: @atom_true]
defp of_rescue(var, exceptions, expr, info, meta, stack, context) do
args = [__exception__: term()]
{structs, context} =
Enum.map_reduce(exceptions, original, fn exception, context ->
Enum.map_reduce(exceptions, context, fn exception, context ->
# Exceptions are not validated in the compiler,
# to avoid export dependencies. So we do it here.
if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do
{info, context} = Of.struct_info(exception, meta, stack, context)
{info, context} = Of.struct_info(exception, :expr, meta, stack, context)
if info do
{Of.struct_type(exception, info, args), context}
else
# If the exception cannot be found or is invalid, fetch the signature to emit warnings.
{_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context)
{error_type(), context}
end
end)
context =
case var do
{:_, _, _} ->
context
case var do
{:_, _, _} ->
context
_ ->
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
expr = {:__block__, [type_check: info], [expr]}
context = Of.declare_var(var, context)
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
context
end
{type, context} = of_expr(body, @pending, body, stack, context)
{type, Of.reset_vars(context, original)}
_ ->
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
expr = {:__block__, [type_check: info], [expr]}
context = Of.declare_var(var, context)
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
context
end
end
## Comprehensions
defp for_clause({:<-, meta, [left, right]}, stack, context) do
expr = {:<-, [type_check: :generator] ++ meta, [left, right]}
{pattern, guards} = extract_head([left])
meta = [type_check: :generator] ++ meta
expr = {:<-, meta, [left, right]}
{[pattern], guards} = extract_head([left])
# TODO: Extract the type from enumerable protocol
{_type, context} =
Apply.remote(Enumerable, :count, [right], dynamic(), expr, stack, context, &of_expr/5)
Apply.remote(Enumerable, :count, [right], term(), expr, stack, context, &of_expr/5)
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, stack, context)
{_tree, _precise, context} =
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, meta, stack, context)
context
end
defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do
{right_type, context} = of_expr(right, bitstring(), expr, stack, context)
context = Pattern.of_generator(left, [], bitstring(), :for, expr, stack, context)
{_tree, _precise?, context} =
Pattern.of_generator(left, [], bitstring(), :for, expr, meta, stack, context)
if compatible?(right_type, bitstring()) do
context
@@ -645,33 +864,29 @@ defmodule Module.Types.Expr do
end
end
defp return_union(left, right, stack) do
Apply.return(union(left, right), [left, right], stack)
end
## With
defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do
{pattern, guards} = extract_head([left])
{_type, context} = of_expr(right, @pending, right, stack, context)
Pattern.of_generator(pattern, guards, dynamic(), :with, expr, stack, context)
defp with_clause({:<-, meta, [left, right]} = expr, stack, {else_types, context}) do
{[pattern], guards} = extract_head([left])
{type, context} =
of_expr(right, term(), right, %{stack | reverse_arrow: :cache}, context)
{trees, precise?, context} =
Pattern.of_generator(pattern, guards, type, :with, expr, meta, stack, context)
[pattern_type] = Pattern.of_domain(trees, stack, context)
{_, refined_context} =
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :use}, context)
else_type = if precise?, do: difference(type, pattern_type), else: type
{union(else_type, else_types), reset_warnings(refined_context, context)}
end
defp with_clause(expr, stack, context) do
{_type, context} = of_expr(expr, @pending, expr, stack, context)
context
end
defp with_option({:do, body}, stack, context, original) do
{_type, context} = of_expr(body, @pending, body, stack, context)
Of.reset_vars(context, original)
end
defp with_option({:else, clauses}, stack, context, _original) do
{_, context} =
of_clauses(clauses, [dynamic()], @pending, nil, :with_else, stack, context, none())
context
defp with_clause(expr, stack, {else_types, context}) do
{_type, context} = of_expr(expr, term(), expr, stack, context)
{else_types, context}
end
## General helpers
@@ -700,88 +915,88 @@ defmodule Module.Types.Expr do
defp reduce_non_empty([head | tail], acc, fun),
do: reduce_non_empty(tail, fun.(head, acc, false), fun)
defp maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?) do
if is_warning(stack) do
case truthiness(head_type) do
:always_true when not last? ->
warning = {:badcond, "always match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
:always_false ->
warning = {:badcond, "never match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
_ ->
context
end
else
context
end
end
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
defp of_clauses(clauses, domain, expected, expr, info, stack, context, acc) do
fun = fn _trees, result, _context, acc -> union(result, acc) end
of_clauses_fun(clauses, domain, expected, expr, info, stack, context, acc, fun)
defp cache_result(meta, %{reverse_arrow: reverse_arrow}, context, fun) do
case reverse_arrow do
nil ->
fun.()
:cache ->
{result, context} = fun.()
version = Keyword.fetch!(meta, :version)
context = put_in(context.reverse_arrows[version], result)
{result, context}
:use ->
version = Keyword.fetch!(meta, :version)
{Map.fetch!(context.reverse_arrows, version), context}
end
end
defp of_clauses_fun(clauses, domain, expected, expr, info, stack, original, acc, fun) do
%{failed: failed?} = original
Enum.reduce(clauses, {acc, original}, fn {:->, meta, [head, body]}, {acc, context} ->
{failed?, context} = reset_failed(context, failed?)
{patterns, guards} = extract_head(head)
{trees, _precise?, context} =
Pattern.of_head(patterns, guards, domain, info, meta, stack, context)
{result, context} = of_expr(body, expected, expr || body, stack, context)
{fun.(trees, result, context, acc),
context |> set_failed(failed?) |> Of.reset_vars(original)}
end)
defp cache_arrows(_meta, %{reverse_arrow: nil}, fun) do
{result, _cache, context} = fun.()
{result, context}
end
defp of_case_clauses(clauses, domain, expected, case_meta, case_expr, expr, stack, original) do
defp cache_arrows(meta, %{reverse_arrow: :cache}, fun) do
{result, cache, context} = fun.()
version = Keyword.fetch!(meta, :version)
context = put_in(context.reverse_arrows[version], cache)
{result, context}
end
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc ->
{body_type, context} = of_expr(body, expected, body, stack, context)
{union(acc, body_type), context}
end
of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc)
end
defp of_clauses_fun(clauses, domain, base_info, stack, original, acc, of_acc) do
%{failed: failed?} = original
{result, _previous, context} =
Enum.reduce(clauses, {none(), none(), original}, fn
{:->, meta, [[clause] = head, body]}, {acc, previous, context} ->
Enum.reduce(clauses, {acc, Pattern.init_previous(), original}, fn
{:->, meta, [head, _]} = clause, {acc, previous, context} ->
{failed?, context} = reset_failed(context, failed?)
{patterns, guards} = extract_head(head)
info = {base_info, head}
clause_domain = difference(domain, previous)
info = {:case, case_meta, clause_domain, case_expr, previous}
{trees, precise?, _, previous, context} =
Pattern.of_head(patterns, guards, domain, previous, info, meta, stack, context)
{trees, precise?, context} =
Pattern.of_head(patterns, guards, [clause_domain], info, meta, stack, context)
# It failed, let's try to detect if it was due a previous or current clause.
# The current clause will be easier to understand, so we prefer that
{[{clause_tree, _, _}], precise?, context} =
if context.failed and previous != none() do
info = {:case, case_meta, domain, case_expr, nil}
case Pattern.of_head(patterns, guards, [domain], info, meta, stack, context) do
{_, _, %{failed: true}} = result -> result
_ -> {trees, precise?, context}
end
else
{trees, precise?, context}
end
{previous, context} =
if context.failed do
{previous, context}
else
clause_type = Pattern.of_pattern_tree(clause_tree, stack, context) |> upper_bound()
cond do
stack.mode != :infer and previous != none() and subtype?(clause_type, previous) ->
stack = %{stack | meta: meta}
{previous, Pattern.badpattern_error(clause, 0, info, stack, context)}
precise? ->
{union(previous, clause_type), context}
true ->
{previous, context}
end
end
{result, context} = of_expr(body, expected, expr || body, stack, context)
{union(result, acc), previous,
context |> set_failed(failed?) |> Of.reset_vars(original)}
{acc, context} = of_acc.(trees, precise?, clause, context, acc)
{acc, previous, context |> set_failed(failed?) |> Of.reset_vars(original)}
end)
{result, context}
end
defp reset_warnings(context, %{warnings: warnings}), do: %{context | warnings: warnings}
defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}}
defp reset_failed(context, _), do: {false, context}
@@ -829,25 +1044,6 @@ defmodule Module.Types.Expr do
## Warning formatting
def format_diagnostic({:badclause, {:case, meta, type, expr}, [head], context}) do
{expr, message} =
if meta[:type_check] == :expr do
{expr,
"""
the following conditional expression will always evaluate to #{to_quoted_string(type)}:
#{expr_to_string(expr) |> indent(4)}
"""}
else
{head, "the following clause has no effect because a previous clause will always match\n"}
end
%{
details: %{typing_traces: collect_traces(expr, context)},
message: message
}
end
def format_diagnostic({:badupdate, type, expr, context}) do
{:%, _, [module, {:%{}, _, [{:|, _, [map, _]}]}]} = expr
traces = collect_traces(map, context)
+114 -84
View File
@@ -329,6 +329,13 @@ defmodule Module.Types.Helpers do
end
end
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
call
else
{:in, meta, [expr, args]}
end
{{:., _, [Elixir.String.Chars, :to_string]}, meta, [arg]} ->
{:to_string, meta, [arg]}
@@ -355,99 +362,122 @@ defmodule Module.Types.Helpers do
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
{:case, meta, [expr, [do: clauses]]} ->
if meta[:type_check] == :expr do
case clauses do
[
{:->, _,
[
case meta[:type_check] do
{:case, op} ->
case clauses do
[
{:->, _,
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
true
]},
{:->, _, [[{:_, _, Kernel}], false]}
] ->
{:!, meta, [expr]}
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
true
]},
{:->, _, [[{:_, _, Kernel}], false]}
]
when op == :! ->
{:!, meta, [expr]}
[
{:->, _,
[
[
{:->, _,
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
right_side
]},
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
] ->
{:||, meta, [expr, right_side]}
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
right_side
]},
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
]
when op == :|| ->
{:||, meta, [expr, right_side]}
[
{:->, _,
[
[
{:->, _,
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
{var, _, Kernel}
]},
{:->, _, [[{:_, _, Kernel}], right_side]}
] ->
{:&&, meta, [expr, right_side]}
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
{var, _, Kernel}
]},
{:->, _, [[{:_, _, Kernel}], right_side]}
]
when op == :&& ->
{:&&, meta, [expr, right_side]}
[
{:->, _,
[
[
{:->, _,
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
else_block
]},
{:->, _, [[{:_, _, Kernel}], do_block]}
] ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
else_block
]},
{:->, _, [[{:_, _, Kernel}], do_block]}
]
when op == :if ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _, [[false], else_block]},
{:->, _, [[true], do_block]}
] ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _, [[false], else_block]},
{:->, _, [[true], do_block]}
]
when op == :if ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
_ ->
{:case, meta, [expr, [do: {:..., [], []}]]}
end
else
{:case, meta, [expr, [do: {:..., [], []}]]}
[
{:->, _, [[false], false]},
{:->, _, [[true], right]}
| _
]
when op == :and ->
{:and, meta, [expr, right]}
[
{:->, _, [[false], right]},
{:->, _, [[true], true]}
| _
]
when op == :or ->
{:or, meta, [expr, right]}
_ ->
{:case, meta, [expr, [do: {:..., [], []}]]}
end
_ ->
{:case, meta, [expr, [do: {:..., [], []}]]}
end
{:try, meta, [[do: _] ++ _]} ->
+126 -49
View File
@@ -48,7 +48,7 @@ defmodule Module.Types.Of do
@doc """
Declares a variable.
"""
def declare_var(var, context) do
def declare_var(var, type \\ term(), context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
@@ -58,7 +58,7 @@ defmodule Module.Types.Of do
vars ->
data = %{
type: term(),
type: type,
name: var_name,
context: var_context,
off_traces: [],
@@ -86,13 +86,14 @@ defmodule Module.Types.Of do
or if we are doing a guard analysis or occurrence typing.
Returns `true` if there was a refinement, `false` otherwise.
"""
def refine_body_var(var_or_version, type, expr, stack, context, allow_empty? \\ false)
@skip_refinement_for [term(), dynamic()]
def refine_body_var(var_or_version, type, expr, stack, context)
def refine_body_var({_, meta, _}, type, expr, stack, context, allow_empty?) do
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context, allow_empty?)
def refine_body_var({_, meta, _}, type, expr, stack, context) do
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
end
def refine_body_var(version, type, expr, stack, context, allow_empty?)
def refine_body_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
@@ -105,31 +106,50 @@ defmodule Module.Types.Of do
context
end
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
case compatible_intersection(old_type, type) do
{:error, _} when allow_empty? ->
data = %{
data
| type: none(),
off_traces: new_trace(expr, none(), stack, off_traces)
}
case context do
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
{old_type, context}
{none(), %{context | vars: %{vars | version => data}}}
%{pattern_info: %{guard_context: guard_context}} ->
new_type = intersection(old_type, type)
{:ok, new_type} when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
case empty?(new_type) do
true when guard_context == :orelse ->
data = %{
data
| type: none(),
off_traces: new_trace(expr, none(), stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
{none(), %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
else
{old_type, context}
false when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
_ ->
case gradual?(old_type) and compatible_intersection(old_type, type) do
{:ok, new_type} when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
end
end
@@ -187,6 +207,13 @@ defmodule Module.Types.Of do
}),
do: %{context | subpatterns: subpatterns, vars: vars, conditional_vars: conditional_vars}
@doc """
Returns true if all entries have the same conditional vars.
"""
def all_same_conditional_vars?([{_, cond} | tail]) do
Enum.all?(tail, fn {_, tail_cond} -> cond == tail_cond end)
end
@doc """
Executes the args with acc using conditional variables.
"""
@@ -434,48 +461,59 @@ defmodule Module.Types.Of do
@doc """
Handles instantiation of a new struct.
This is expanded and validated by the compiler, so don't need to check the fields.
"""
# TODO: Type check the fields match the struct
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
when is_atom(struct) do
{_info, context} = struct_info(struct, meta, stack, context)
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
if is_nil(info) do
{dynamic(), context}
else
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, meta, stack, context) do
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
case stack.no_warn_undefined do
%Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
{:ok, info} -> {info, context}
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
{:error, _desc} -> {nil, context}
end
_ ->
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and
struct.__info__(:struct)
{info, context}
if info do
{_, context} =
Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
{info, context}
else
error = {:unknown_struct, kind, struct, must_exist?}
{nil, error(error, meta, stack, context)}
end
end
end
@@ -492,6 +530,14 @@ defmodule Module.Types.Of do
closed_map(pairs)
end
@doc """
Returns shared error for unknown struct field.
"""
def unknown_struct_field(struct, field, kind, meta, stack, context) do
error = {:unknown_struct_field, kind, struct, field}
error(error, meta, stack, context)
end
## Bitstrings
@doc """
@@ -802,6 +848,37 @@ defmodule Module.Types.Of do
}
end
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
detail =
case {Code.ensure_loaded?(module), must_exist?} do
{true, false} ->
"there is such module but it does not define a struct"
{false, false} ->
"module #{inspect(module)} is not available or is yet to be defined"
{true, true} ->
"the module may have been redefined as it no longer defines a struct"
{false, true} ->
"the module was also only available but may have been removed during compilation"
end
%{
message: "struct #{inspect(module)} is undefined (#{detail})",
group: true,
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
}
end
def format_diagnostic({:unknown_struct_field, kind, module, field}) do
%{
message: "unknown key #{inspect(field)} for struct #{inspect(module)}",
group: true,
severity: if(kind == :pattern, do: :error, else: :warning)
}
end
defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
File diff suppressed because it is too large Load Diff
+58 -7
View File
@@ -35,8 +35,19 @@ defmodule Module.Types.Traverse do
end)
end
# Structs, map update, tail operator
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:%, :|] do
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]}, stack, context) do
context = of_expr(map, stack, context)
context = of_expr(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
def of_expr({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) do
context = of_expr(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
# Map update, tail operator
def of_expr({:|, _meta, [left, right]}, stack, context) do
context = of_expr(left, stack, context)
of_expr(right, stack, context)
end
@@ -46,9 +57,9 @@ defmodule Module.Types.Traverse do
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
end
# left = right, left <_ right
def of_expr({op, _meta, [_left, right]}, stack, context) when op in [:=, :<-] do
# Skip the left side (pattern), only traverse right
# left = right, left <- right
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:=, :<-] do
context = of_pattern(left, stack, context)
of_expr(right, stack, context)
end
@@ -65,8 +76,9 @@ defmodule Module.Types.Traverse do
end)
end
# Treat -> as patterns for simplicity
def of_expr({:->, _, [_head, body]}, stack, context) do
# All non-handled -> are patterns
def of_expr({:->, _, [head, body]}, stack, context) do
context = of_pattern(head, stack, context)
of_expr(body, stack, context)
end
@@ -150,6 +162,45 @@ defmodule Module.Types.Traverse do
context
end
defp of_pattern({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) when is_atom(module) do
context = of_pattern(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
defp of_pattern({left, _meta, right}, stack, context) do
context = of_pattern(left, stack, context)
of_pattern(right, stack, context)
end
defp of_pattern({left, right}, stack, context) do
context = of_pattern(left, stack, context)
of_pattern(right, stack, context)
end
defp of_pattern([_ | _] = list, stack, context) do
Enum.reduce(list, context, &of_pattern(&1, stack, &2))
end
defp of_pattern(_, _stack, context) do
context
end
defp of_struct(module, pairs, kind, meta, stack, context) do
{info, context} = Module.Types.Of.struct_info(module, kind, meta, stack, context)
if info do
Enum.reduce(pairs, context, fn {key, _value}, context ->
if Enum.any?(info, &(&1.field == key)) do
context
else
Module.Types.Of.unknown_struct_field(module, key, kind, meta, stack, context)
end
end)
else
context
end
end
defp local_fun(meta, fun, arity, stack, context) do
case stack.local_handler.(meta, {fun, arity}, stack, context) do
false -> context
+8 -2
View File
@@ -952,8 +952,14 @@ defmodule OptionParser do
case types |> List.delete(:keep) |> List.first(:string) do
:boolean ->
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
case to_switch(name) do
"--no-" <> _ = switch ->
add_aliases(switch, name, reverse_aliases)
switch ->
base = "#{switch}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
end
type ->
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
+8 -6
View File
@@ -170,6 +170,8 @@ defmodule PartitionSupervisor do
| {:max_seconds, non_neg_integer()}
| {:with_arguments, (args :: [term()], partition() -> updated_args :: [term()])}
defguardp is_name(name) when is_atom(name) or elem(name, 0) == :via
@doc false
def child_spec(opts) when is_list(opts) do
id =
@@ -366,7 +368,7 @@ defmodule PartitionSupervisor do
"""
@doc since: "1.18.0"
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
def resize!(name, partitions) when is_integer(partitions) do
def resize!(name, partitions) when is_name(name) and is_integer(partitions) do
supervisor =
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
@@ -421,8 +423,8 @@ defmodule PartitionSupervisor do
Returns the number of partitions for the partition supervisor.
"""
@doc since: "1.14.0"
@spec partitions(name()) :: pos_integer()
def partitions(name) do
@spec partitions(name()) :: non_neg_integer()
def partitions(name) when is_name(name) do
name |> table() |> partitions(name)
end
@@ -470,7 +472,7 @@ defmodule PartitionSupervisor do
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
]
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
def which_children(name) when is_name(name) do
Supervisor.which_children(name)
end
@@ -498,7 +500,7 @@ defmodule PartitionSupervisor do
supervisors: non_neg_integer,
workers: non_neg_integer
}
def count_children(supervisor) when is_atom(supervisor) do
def count_children(supervisor) when is_name(supervisor) do
Supervisor.count_children(supervisor)
end
@@ -514,7 +516,7 @@ defmodule PartitionSupervisor do
"""
@doc since: "1.14.0"
@spec stop(name(), reason :: term, timeout) :: :ok
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_atom(supervisor) do
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_name(supervisor) do
Supervisor.stop(supervisor, reason, timeout)
end
+1 -1
View File
@@ -485,7 +485,7 @@ defmodule Path do
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
case :file.get_cwd() do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
_ -> path
_ -> IO.chardata_to_string(path)
end
end
+27 -10
View File
@@ -387,8 +387,6 @@ defmodule Process do
automatically canceled when `dest` is an atom (as the atom resolution is done
on delivery).
Inlined by the compiler.
## Options
* `:abs` - (boolean) when `false`, `time` is treated as relative to the
@@ -979,11 +977,35 @@ defmodule Process do
@spec unalias(alias) :: boolean
defdelegate unalias(alias), to: :erlang
@doc """
Returns the label set for the process `pid` as set with `set_label/1`
or `:proc_lib.set_label/1`.
Defaults to the current process when `pid` is not passed.
## Examples
Process.set_label({:any, "term"})
Process.get_label()
#=> {:any, "term"}
Returns `nil` when not set:
Process.get_label(pid)
#=> nil
"""
@doc since: "1.20.0"
@spec get_label(pid()) :: term()
def get_label(pid \\ self()) do
nilify(:proc_lib.get_label(pid))
end
@doc """
Add a descriptive term to the current process.
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
Observer and in crash logs.
The term does not need to be unique, and will be shown in Observer and in
crash logs.
This label may be useful for identifying a process as one of multiple in a
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
@@ -997,12 +1019,7 @@ defmodule Process do
"""
@doc since: "1.17.0"
@spec set_label(term()) :: :ok
def set_label(label) do
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
Process.put(:"$process_label", label)
# mimic return value of `:proc_lib.set_label/2`
:ok
end
defdelegate set_label(label), to: :proc_lib
@compile {:inline, nilify: 1}
defp nilify(:undefined), do: nil
+24 -20
View File
@@ -786,7 +786,7 @@ defmodule Protocol do
end
defp fallback_clause_for(value, _protocol, meta) do
{meta, [quote(do: _)], [], value}
{meta, [{:_, [version: -1], __MODULE__}], [], value}
end
# Finally compile the module and emit its bytecode.
@@ -818,11 +818,9 @@ defmodule Protocol do
@fallback_to_any false
@undefined_impl_description ""
# Invoke the user given block
_ = unquote(block)
# Finalize expansion
res = unquote(block)
unquote(after_defprotocol())
res
end
end
end
@@ -932,7 +930,7 @@ defmodule Protocol do
quote bind_quoted: [built_in: built_in()] do
any_impl_for =
if @fallback_to_any do
__MODULE__.Any
Protocol.__concat__(__MODULE__, "Any")
else
nil
end
@@ -968,16 +966,6 @@ defmodule Protocol do
built_in
)
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
# destructuring opaque types is illegal, Dialyzer will think none of the
# previous clauses matches opaque types, and without this clause, will
# conclude that impl_for can't handle an opaque argument). This is a hack
# since it relies on Dialyzer not being smart enough to conclude that all
# opaque types will get the any_impl_for/0 implementation.
Kernel.def impl_for(_) do
unquote(any_impl_for)
end
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
@@ -1021,8 +1009,22 @@ defmodule Protocol do
)
end
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
# destructuring opaque types is illegal, Dialyzer will think none of the
# previous clauses matches opaque types, and without this clause, will
# conclude that impl_for can't handle an opaque argument). This is a hack
# since it relies on Dialyzer not being smart enough to conclude that all
# opaque types will get the any_impl_for/0 implementation.
impl_for_fallback =
quote generated: true, bind_quoted: [] do
Kernel.def impl_for(_) do
unquote(any_impl_for)
end
end
quote generated: true do
unquote(prefix)
unquote(impl_for_fallback)
@doc false
@spec impl_for!(term) :: atom
@@ -1221,10 +1223,12 @@ defmodule Protocol do
end
@doc false
def __concat__(left, right) do
String.to_atom(
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
)
def __concat__(left, right) when is_atom(right) do
__concat__(left, remove_prefix(Atom.to_string(right)))
end
def __concat__(left, right) when is_binary(right) do
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
end
defp ensure_prefix("Elixir." <> _ = left), do: left
+2 -3
View File
@@ -78,8 +78,7 @@ defmodule Regex do
* `:caseless` (i) - adds case insensitivity
* `:dotall` (s) - causes dot to match newlines and also sets newline to
`(*ANYCRLF)`.\
* `:dotall` (s) - causes dot to match newlines and also sets newline to `(*ANYCRLF)`.
The new line setting, as described in the [`:re` documentation](`:re`),
can be overridden by starting the regular expression pattern with:
* `(*CR)` - carriage return
@@ -87,7 +86,7 @@ defmodule Regex do
* `(*CRLF)` - carriage return, followed by line feed
* `(*ANYCRLF)` - any of the three above
* `(*ANY)` - all Unicode newline sequences
* _Starting from Erlang/OTP 28, `(*NUL)` - the NUL character (binary zero)_
* `(*NUL)` - the NUL character (binary zero) (from Erlang/OTP 28)
* `:multiline` (m) - causes `^` and `$` to mark the beginning and end of
each line; use `\A` and `\z` to match the end or beginning of the string
+191 -76
View File
@@ -258,7 +258,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case safe_lookup_second(key_ets, key) do
case lookup_second(:unique, key_ets, key) do
{pid, _} ->
if Process.alive?(pid), do: pid, else: :undefined
@@ -369,7 +369,10 @@ defmodule Registry do
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
many different keys (e.g., many topics with few subscribers each). This makes
key-based lookups more efficient as they only need to check a single partition
instead of all partitions.
instead of all partitions. This option uses a different internal ETS table type
(`ordered_set` instead of `duplicate_bag`), which means match specs passed to
`select/2` and `count_select/2` that reference `:"$_"` may behave differently.
Use named match variables (`:"$1"`, `:"$2"`, etc.) instead.
"""
@doc since: "1.5.0"
@@ -532,13 +535,17 @@ defmodule Registry do
given as an option, the dispatching happens in parallel. In both cases,
the callback is only invoked if there are entries for that partition.
For `{:duplicate, :key}` registries, all entries for a given key are in a
single partition, so the `:parallel` option has no effect.
See the module documentation for examples of using the `dispatch/3`
function for building custom dispatching or a pubsub system.
## Options
* `:parallel` - if `true`, the dispatching is done in parallel
across all partitions. Defaults to `false`.
across all partitions. Only meaningful for `{:duplicate, :pid}`
registries. Defaults to `false`.
"""
@doc since: "1.4.0"
@@ -549,17 +556,24 @@ defmodule Registry do
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
case key_info!(registry) do
{:unique, partitions, key_ets} ->
(key_ets || key_ets!(registry, key, partitions))
|> safe_lookup_second(key)
key_ets = key_ets || key_ets!(registry, key, partitions)
:unique
|> lookup_second(key_ets, key)
|> List.wrap()
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, _}, 1, key_ets} ->
key_ets
|> safe_lookup_second(key)
{{:duplicate, _} = kind, 1, key_ets} ->
kind
|> lookup_second(key_ets, key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, _}, partitions, _} ->
{{:duplicate, :key}, partitions, _} ->
{:duplicate, :key}
|> lookup_second(key_ets!(registry, key, partitions), key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, :pid}, partitions, _} ->
if Keyword.get(opts, :parallel, false) do
registry
|> dispatch_parallel(key, mfa_or_fun, partitions)
@@ -579,9 +593,8 @@ defmodule Registry do
defp dispatch_serial(registry, key, mfa_or_fun, partition) do
partition = partition - 1
registry
|> key_ets!(partition)
|> safe_lookup_second(key)
{:duplicate, :pid}
|> lookup_second(key_ets!(registry, partition), key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
dispatch_serial(registry, key, mfa_or_fun, partition)
@@ -597,9 +610,8 @@ defmodule Registry do
task =
Task.async(fn ->
registry
|> key_ets!(partition)
|> safe_lookup_second(key)
{:duplicate, :pid}
|> lookup_second(key_ets!(registry, partition), key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
Process.unlink(parent)
@@ -663,7 +675,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case safe_lookup_second(key_ets, key) do
case lookup_second(:unique, key_ets, key) do
{_, _} = pair ->
[pair]
@@ -671,16 +683,15 @@ defmodule Registry do
[]
end
{{:duplicate, _}, 1, key_ets} ->
safe_lookup_second(key_ets, key)
{{:duplicate, _} = kind, 1, key_ets} ->
lookup_second(kind, key_ets, key)
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
safe_lookup_second(key_ets!(registry, partition), key)
lookup_second({:duplicate, :key}, key_ets!(registry, key, partitions), key)
{{:duplicate, :pid}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- safe_lookup_second(key_ets!(registry, partition), key),
pair <- lookup_second({:duplicate, :pid}, key_ets!(registry, partition), key),
do: pair
end
end
@@ -791,21 +802,18 @@ defmodule Registry do
@doc since: "1.4.0"
@spec match(registry, key, match_pattern, guards) :: [{pid, term}]
def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
case key_info!(registry) do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
{{:duplicate, :pid}, partitions, nil} ->
spec = match_spec({:duplicate, :pid}, key, pattern, guards)
{{:duplicate, _}, 1, key_ets} ->
:ets.select(key_ets, spec)
{{:duplicate, _}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- :ets.select(key_ets!(registry, partition), spec),
do: pair
{kind, partitions, key_ets} ->
spec = match_spec(kind, key, pattern, guards)
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
end
end
@@ -943,7 +951,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case safe_lookup_second(key_ets, key) do
case lookup_second(:unique, key_ets, key) do
{^pid, value} ->
[value]
@@ -951,18 +959,17 @@ defmodule Registry do
[]
end
{{:duplicate, _}, 1, key_ets} ->
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, _} = kind, 1, key_ets} ->
for {^pid, value} <- lookup_second(kind, key_ets, key), do: value
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
key_ets = key_ets!(registry, key, partitions)
for {^pid, value} <- lookup_second({:duplicate, :key}, key_ets, key), do: value
{{:duplicate, :pid}, partitions, _key_ets} ->
partition = hash(pid, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
for {^pid, value} <- lookup_second({:duplicate, :pid}, key_ets, key), do: value
end
end
@@ -1016,7 +1023,8 @@ defmodule Registry do
# Remove first from the key_ets because in case of crashes
# the pid_ets will still be able to clean up. The last step is
# to clean if we have no more entries.
true = __unregister__(key_ets, {key, {self, :_}}, 1)
true = __unregister__(key_ets, {key, {self, :_}}, 1, ordered?(kind))
true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2)
unlink_if_unregistered(pid_server, pid_ets, self)
@@ -1081,13 +1089,10 @@ defmodule Registry do
# to clean if we have no more entries.
# Here we want to count all entries for this pid under this key, regardless of pattern.
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
{total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards)
total = :ets.select_count(key_ets, total_spec)
# We only want to delete things that match the pattern
delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]
case :ets.select_delete(key_ets, delete_spec) do
# We deleted everything, we can just delete the object
^total ->
@@ -1121,6 +1126,25 @@ defmodule Registry do
:ok
end
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
else
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
end
end
defp unregister_match_specs(_kind, key, self, pattern, guards) do
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
end
@doc """
Registers the current process under the given `key` in `registry`.
@@ -1180,7 +1204,14 @@ defmodule Registry do
counter = System.unique_integer()
true = :ets.insert(pid_ets, {self, key, key_ets, counter})
case register_key(kind, key_ets, key, {key, {self, value}}) do
key_entry =
if ordered?(kind) do
{{key, self, counter}, value}
else
{key, {self, value}}
end
case register_key(kind, key_ets, key, key_entry) do
:ok ->
for listener <- listeners do
Kernel.send(listener, {:register, registry, key, self, value})
@@ -1409,21 +1440,18 @@ defmodule Registry do
@spec count_match(registry, key, match_pattern, guards) :: non_neg_integer()
def count_match(registry, key, pattern, guards \\ [])
when is_atom(registry) and is_list(guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
spec = [{{:_, {:_, pattern}}, guards, [true]}]
case key_info!(registry) do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
{{:duplicate, :pid}, partitions, nil} ->
spec = count_match_spec({:duplicate, :pid}, key, pattern, guards)
{{:duplicate, _}, 1, key_ets} ->
:ets.select_count(key_ets, spec)
{{:duplicate, _}, partitions, _key_ets} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
{kind, partitions, key_ets} ->
spec = count_match_spec(kind, key, pattern, guards)
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
end
end
@@ -1452,6 +1480,9 @@ defmodule Registry do
some operations like `:element` to modify the output format.
Do not use special match variables `:"$_"` and `:"$$"`, because they might not work as expected.
In particular, `{:duplicate, :key}` registries use a different internal ETS layout, so match specs
that reference the underlying entry structure via `:"$_"` will return different results.
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
Note that for large registries with many partitions this will be costly as it builds the result by
concatenating all the partitions.
@@ -1479,15 +1510,16 @@ defmodule Registry do
@spec select(registry, spec) :: [term]
def select(registry, spec)
when is_atom(registry) and is_list(spec) do
spec = group_match_headers(spec, __ENV__.function)
case key_info!(registry) do
{_kind, partitions, nil} ->
{kind, partitions, nil} ->
spec = group_match_headers(spec, kind, __ENV__.function)
Enum.flat_map(0..(partitions - 1), fn partition_index ->
:ets.select(key_ets!(registry, partition_index), spec)
end)
{_kind, 1, key_ets} ->
{kind, 1, key_ets} ->
spec = group_match_headers(spec, kind, __ENV__.function)
:ets.select(key_ets, spec)
end
end
@@ -1510,24 +1542,30 @@ defmodule Registry do
@spec count_select(registry, spec) :: non_neg_integer()
def count_select(registry, spec)
when is_atom(registry) and is_list(spec) do
spec = group_match_headers(spec, __ENV__.function)
case key_info!(registry) do
{_kind, partitions, nil} ->
{kind, partitions, nil} ->
spec = group_match_headers(spec, kind, __ENV__.function)
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
{_kind, 1, key_ets} ->
{kind, 1, key_ets} ->
spec = group_match_headers(spec, kind, __ENV__.function)
:ets.select_count(key_ets, spec)
end
end
defp group_match_headers(spec, {fun, arity}) do
defp group_match_headers(spec, kind, {fun, arity}) do
ordered = ordered?(kind)
for part <- spec do
case part do
{{key, pid, value}, guards, select} ->
{{key, {pid, value}}, guards, select}
{{key, pid, value}, guards, body} when ordered ->
{{{key, pid, :_}, value}, guards, body}
{{key, pid, value}, guards, body} ->
{{key, {pid, value}}, guards, body}
_ ->
raise ArgumentError,
@@ -1578,7 +1616,23 @@ defmodule Registry do
:ets.lookup_element(registry, partition, 3)
end
defp safe_lookup_second(ets, key) do
defp lookup_second({:duplicate, :key}, ets, key) do
spec =
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
else
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
end
try do
:ets.select(ets, spec)
catch
:error, :badarg -> []
end
end
defp lookup_second(_kind, ets, key) do
try do
:ets.lookup_element(ets, key, 2)
catch
@@ -1607,7 +1661,19 @@ defmodule Registry do
end
@doc false
def __unregister__(table, match, pos) do
def __unregister__(table, match, pos, ordered \\ false)
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
else
:ets.match_delete(table, {{key, pid, :_}, :_})
end
end
def __unregister__(table, match, pos, false = _ordered) do
key = :erlang.element(pos, match)
# We need to perform an element comparison if we have a special atom key.
@@ -1620,9 +1686,52 @@ defmodule Registry do
end
end
# For {:duplicate, :key} registries, the ETS entry layout is
# {{key, pid, counter}, value} (ordered_set with composite key),
# so :"$_" refers to that whole tuple. The body extracts {pid, value}
# to match the public API's return shape of [{pid, value}].
defp match_spec({:duplicate, :key}, key, pattern, guards) do
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, body}]
else
[{{{key, :_, :_}, pattern}, guards, body}]
end
end
defp match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
end
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, [true]}]
else
[{{{key, :_, :_}, pattern}, guards, [true]}]
end
end
defp count_match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [true]}]
end
defp reserved_atom?("_"), do: true
defp reserved_atom?("$" <> _), do: true
defp reserved_atom?(_), do: false
defp ordered?({:duplicate, :key}), do: true
defp ordered?(_), do: false
end
defmodule Registry.Supervisor do
@@ -1734,7 +1843,8 @@ defmodule Registry.Partition do
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
end
{:ok, {pid_ets, %{}}}
ordered = kind == {:duplicate, :key}
{:ok, {pid_ets, %{}, ordered}}
end
# The key partition is a set for unique keys,
@@ -1744,7 +1854,12 @@ defmodule Registry.Partition do
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
defp init_key_ets({:duplicate, :key}, key_partition, compressed) do
opts = [:ordered_set, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets({:duplicate, :pid}, key_partition, compressed) do
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
@@ -1768,7 +1883,7 @@ defmodule Registry.Partition do
{:reply, :ok, state}
end
def handle_call({:lock, key}, from, {ets, lock}) do
def handle_call({:lock, key}, from, {ets, lock, ordered}) do
lock =
case lock do
%{^key => queue} ->
@@ -1779,10 +1894,10 @@ defmodule Registry.Partition do
Map.put(lock, key, :queue.new())
end
{:noreply, {ets, lock}}
{:noreply, {ets, lock, ordered}}
end
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
def handle_info({:EXIT, pid, _reason}, {ets, lock, ordered}) do
entries = :ets.take(ets, pid)
for {_pid, key, key_ets, _counter} <- entries do
@@ -1797,13 +1912,13 @@ defmodule Registry.Partition do
end
try do
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1)
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1, ordered)
catch
:error, :badarg -> :badarg
end
end
{:noreply, {ets, lock}}
{:noreply, {ets, lock, ordered}}
end
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
@@ -1815,7 +1930,7 @@ defmodule Registry.Partition do
unlock(key, state)
end
defp unlock(key, {ets, lock}) do
defp unlock(key, {ets, lock, ordered}) do
%{^key => queue} = lock
lock =
@@ -1824,7 +1939,7 @@ defmodule Registry.Partition do
{:not_empty, queue} -> Map.put(lock, key, queue)
end
{:noreply, {ets, lock}}
{:noreply, {ets, lock, ordered}}
end
defp dequeue(queue, key) do
+18 -2
View File
@@ -1437,7 +1437,7 @@ defmodule Stream do
do_cycle(cycle, [], cycle, fun.(element, acc), fun)
{_, []} ->
do_cycle(cycle, [], cycle, {:cont, acc}, fun)
do_cycle(check_cycle_subsequent_element(cycle), [], cycle, {:cont, acc}, fun)
end
end
@@ -1446,10 +1446,26 @@ defmodule Stream do
end
defp check_cycle_first_element(reduce) do
check_cycle_non_empty(
reduce,
ArgumentError,
"cannot cycle over an empty enumerable"
)
end
defp check_cycle_subsequent_element(reduce) do
check_cycle_non_empty(
reduce,
RuntimeError,
"cycled enumerable became empty after a previous iteration produced elements"
)
end
defp check_cycle_non_empty(reduce, exception, message) do
fn acc ->
case reduce.(acc) do
{state, []} when state in [:done, :halted] and elem(acc, 0) != :halt ->
raise ArgumentError, "cannot cycle over an empty enumerable"
raise exception, message
other ->
other
+32 -4
View File
@@ -745,7 +745,15 @@ defmodule String do
iex> String.count("hello world", "")
12
The `pattern` can also be a compiled pattern:
The `pattern` can also be a list:
iex> String.count("hello world", ["e", "o"])
3
iex> String.count("hello world", [])
0
Or a compiled pattern:
iex> pattern = :binary.compile_pattern([" ", "!"])
iex> String.count("foo bar baz!!", pattern)
@@ -756,6 +764,8 @@ defmodule String do
@doc since: "1.19.0"
def count(string, <<>>), do: length(string) + 1
def count(_string, []), do: 0
def count(string, pattern) when is_struct(pattern, Regex) do
Kernel.length(Regex.scan(pattern, string, return: :index))
end
@@ -2290,6 +2300,19 @@ defmodule String do
end
end
# 56-bit SWAR guard: all 7 bytes are ASCII (< 128) and none is \r (0x0D).
# Uses Mycroft's zero-byte detection: XOR with 0x0D and check for zero bytes.
defguardp ascii_no_cr_swar?(w)
when Bitwise.band(w, 0x80808080808080) == 0 and
Bitwise.band(
Bitwise.bxor(w, 0x0D0D0D0D0D0D0D) - 0x01010101010101,
0x80808080808080
) == 0
defp skip_length(<<w::56, b, rest::binary>>, acc)
when b <= 127 and b != ?\r and ascii_no_cr_swar?(w),
do: skip_length(rest, acc + 8)
defp skip_length(<<byte, rest::binary>>, acc)
when byte <= 127 and byte != ?\r,
do: skip_length(rest, acc + 1)
@@ -3168,8 +3191,7 @@ defmodule String do
def jaro_distance("", _string), do: 0.0
def jaro_distance(string1, string2) when is_binary(string1) and is_binary(string2) do
# TODO: Replace by :string.jaro_similarity/2 when we require Erlang/OTP 27+
:elixir_utils.jaro_similarity(string1, string2)
:string.jaro_similarity(string1, string2)
end
@doc """
@@ -3214,7 +3236,13 @@ defmodule String do
byte_size_unicode(unicode)
end
defp byte_size_remaining_at(unicode, n) do
defp byte_size_remaining_at(<<byte1, byte2, rest::binary>> = binary, n)
when n > 0 and byte1 <= 127 and byte1 != ?\r and byte2 <= 127 and byte2 != ?\r do
skip = min(skip_length(rest, 1), n)
byte_size_remaining_at(binary_part(binary, skip, byte_size(binary) - skip), n - skip)
end
defp byte_size_remaining_at(unicode, n) when n > 0 do
case :unicode_util.gc(unicode) do
[_] -> 0
[_ | rest] -> byte_size_remaining_at(rest, n - 1)
+34 -13
View File
@@ -556,19 +556,29 @@ defmodule Supervisor do
Developers typically invoke `Supervisor.init/2` at the end of their
init callback to return the proper supervision flags.
See `start_link/2` for more information.
"""
@callback init(init_arg :: term) ::
{:ok,
{sup_flags(), [child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
| :ignore
@typedoc "Return values of `start_link/2` and `start_link/3`."
@typedoc """
Return values of `start_link/2` and `start_link/3`.
See the documentation for those functions for more information.
"""
@type on_start ::
{:ok, pid}
| :ignore
| {:error, {:already_started, pid} | {:shutdown, term} | term}
@typedoc "Return values of `start_child/2`."
@typedoc """
Return values of `start_child/2`.
See the documentation for those functions for more information.
"""
@type on_start_child ::
{:ok, child}
| {:ok, child, info :: term}
@@ -698,15 +708,18 @@ defmodule Supervisor do
If the supervisor and all child processes are successfully spawned
(if the start function of each child process returns `{:ok, child}`,
`{:ok, child, info}`, or `:ignore`), this function returns
`{:ok, pid}`, where `pid` is the PID of the supervisor. If the supervisor
is given a name and a process with the specified name already exists,
the function returns `{:error, {:already_started, pid}}`, where `pid`
is the PID of that process.
`{:ok, pid}`, where `pid` is the PID of the supervisor.
If the start function of any of the child processes fails or returns an error
tuple or an erroneous value, the supervisor first terminates with reason
`:shutdown` all the child processes that have already been started, and then
terminates itself and returns `{:error, {:shutdown, reason}}`.
If the supervisor is given a name and a process with the specified name
already exists, the function returns `{:error, {:already_started, pid}}`,
where `pid` is the PID of that process.
If the start function of any of the child processes fails or returns
an error tuple or an erroneous value, the supervisor first terminates
with reason `:shutdown` all the child processes that have already been
started, and then terminates itself. If the caller process is trapping
exits, this function then returns
`{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits
@@ -939,9 +952,17 @@ defmodule Supervisor do
If the `c:init/1` callback returns `:ignore`, this function returns
`:ignore` as well and the supervisor terminates with reason `:normal`.
If it fails or returns an incorrect value, this function returns
`{:error, term}` where `term` is a term with information about the
error, and the supervisor terminates with reason `term`.
If the `c:init/1` callback fails or returns an incorrect value, the supervisor
will shutdown with reason `term`, where `term` contains information
about the error. If the caller process is trapping exits, this function then
returns `{:error, term()}`.
If the start function of any of the child processes returned by `c:init/1`
fail or return an error tuple or an erroneous value, the supervisor first
terminates with reason `:shutdown` all the child processes that have already
been started, and then terminates itself. If the caller process is trapping
exits, this function then returns `{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
The `:name` option can also be given in order to register a supervisor
name, the supported values are described in the "Name registration"
+1 -1
View File
@@ -105,7 +105,7 @@ defmodule Task do
], strategy: :one_for_one)
And now you can use async/await by passing the name of
the supervisor instead of the pid:
the supervisor instead of the PID:
Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
# Do something
+2 -2
View File
@@ -122,8 +122,8 @@ defmodule Task.Supervised do
function: fun,
args: args,
reason: {log_value(kind, value), __STACKTRACE__},
# TODO use Process.get_label/0 when we require Erlang/OTP 27+
process_label: Process.get(:"$process_label", :undefined)
# using :proc_lib over Process because we want the :undefined default, not nil
process_label: :proc_lib.get_label(self())
}
},
%{
+41 -9
View File
@@ -24,6 +24,12 @@ defmodule URI do
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
The fields contain the encoded URI components as they appear in the URI
itself. For example, a slash inside the userinfo must be stored as `%2F`,
not as `/`. Functions such as `parse/1` and `new/1` preserve existing
percent-encoded sequences in those fields, and functions such as `to_string/1`
expects those fields to already be encoded as needed. Whenever setting or
modifying the fields directly, you must encode them accordingly.
Note the `authority` field is deprecated. `parse/1` will still
populate it for backwards compatibility but you should generally
@@ -520,6 +526,16 @@ defmodule URI do
for the URI's scheme is used for the `:port` field. The scheme is also
normalized to lowercase.
> #### Browser compatibility {: .warning}
>
> This function does not follow the same parsing rules as browsers.
> Browsers adhere to the WHATWG URL standard, while this function
> implements RFC 3986. You should expect different behaviours between
> them and this function will often reject URLs that are accepted by
> browsers (as they tend to be permissive). Common examples include
> unencoded square brackets in query strings (such as `foo[bar]=baz`)
> and backslashes used as path separators.
## Examples
iex> URI.new("https://elixir-lang.org/")
@@ -693,18 +709,26 @@ defmodule URI do
This function can parse both absolute and relative URLs. You can check
if a URI is absolute or relative by checking if the `scheme` field is
nil or not. Furthermore, this function expects both absolute and
relative URIs to be well-formed and does not perform any validation.
See the "Examples" section below. Use `new/1` if you want to validate
the URI fields after parsing.
nil or not.
This function expects both absolute and relative URIs to be well-formed
and does not perform any validation, it simply breaks the URL into parts.
Use `new/1` if you want to validate the URI fields after parsing.
When a URI is given without a port, the value returned by `URI.default_port/1`
for the URI's scheme is used for the `:port` field. The scheme is also
normalized to lowercase.
If a `%URI{}` struct is given to this function, this function returns it
If a `URI` struct is given to this function, this function returns it
unmodified.
> #### Browser compatibility {: .warning}
>
> This function does not follow the same parsing rules as browsers.
> Browsers adhere to the WHATWG URL standard, while this function
> implements RFC 3986. You should expect different behaviours between
> them, especially around corner cases.
> #### `:authority` field {: .info}
>
> This function sets the deprecated field `:authority` for backwards-compatibility reasons.
@@ -757,8 +781,9 @@ defmodule URI do
userinfo: nil
}
In contrast to `URI.new/1`, this function will parse poorly-formed
URIs, for example:
In contrast to `URI.new/1`, this function will accept poorly-formed
URIs and break them into components, as it does not perform validation.
For example:
iex> URI.parse("/invalid_greater_than_in_path/>")
%URI{
@@ -772,8 +797,8 @@ defmodule URI do
}
Another example is a URI with brackets in query strings. It is accepted
by `parse/1`, it is commonly accepted by browsers, but it will be refused
by `new/1`:
by `parse/1` for the same reasons as above (no validation), but it will
be refused by `new/1`:
iex> URI.parse("/?foo[bar]=baz")
%URI{
@@ -786,6 +811,9 @@ defmodule URI do
userinfo: nil
}
Generally speaking, square brackets are strictly invalid even in the WHATWG
URL standard used by browsers, but browsers tend to be permissive and often
allow invalid characters to pass through.
"""
@spec parse(t | binary) :: t
def parse(%URI{} = uri), do: uri
@@ -875,6 +903,9 @@ defmodule URI do
@doc """
Returns the string representation of the given [URI struct](`t:t/0`).
This function assembles the URI components into a string, assuming each
field is valid and escaped as done by `parse/1` and `new/1`.
## Examples
iex> uri = URI.parse("http://google.com")
@@ -942,6 +973,7 @@ defmodule URI do
end
defp merge_paths(nil, rel_path), do: merge_paths("/", rel_path)
defp merge_paths("", rel_path), do: merge_paths("/", rel_path)
defp merge_paths(_, "/" <> _ = rel_path), do: remove_dot_segments_from_path(rel_path)
defp merge_paths(base_path, rel_path) do
+18 -1
View File
@@ -18,12 +18,16 @@ defmodule Version do
MAJOR.MINOR.PATCH
Each numeric component is limited to at most 14 digits.
Pre-releases are supported by optionally appending a hyphen and a series of
period-separated identifiers immediately following the patch version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
"1.0.0-alpha.3"
Numeric pre-release identifiers are also limited to at most 14 digits.
Build information can be added by appending a plus sign and a series of
dot-separated identifiers immediately following the patch or pre-release version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
@@ -520,6 +524,8 @@ defmodule Version do
defmodule Parser do
@moduledoc false
@max_numeric_component_digits 14
operators = [
{">=", :>=},
{"<=", :<=},
@@ -621,7 +627,9 @@ defmodule Version do
defp require_digits(nil), do: :error
defp require_digits(string) do
if leading_zero?(string), do: :error, else: parse_digits(string, "")
if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits,
do: :error,
else: parse_digits(string, "")
end
defp leading_zero?(<<?0, _, _::binary>>), do: true
@@ -649,6 +657,11 @@ defmodule Version do
end
end
defp convert_parts_to_integer([part | rest], acc)
when byte_size(part) > @max_numeric_component_digits do
if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc])
end
defp convert_parts_to_integer([part | rest], acc) do
case parse_digits(part, "") do
{:ok, integer} ->
@@ -667,6 +680,10 @@ defmodule Version do
{:ok, Enum.reverse(acc)}
end
defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest)
defp all_digits?(<<>>), do: true
defp all_digits?(_other), do: false
defp valid_identifier?(<<char, rest::binary>>)
when char in ?0..?9
when char in ?a..?z
@@ -75,7 +75,7 @@ iex> trunc(3.58)
3
```
Finally, we work with different data types, we will learn Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
Finally, as we work with different data types, we will learn that Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
```elixir
iex> is_integer(1)
@@ -310,4 +310,4 @@ false
The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](`Kernel#module-structural-comparison`).
Elixir also provides data-types for expressing collections, such as lists and tuples, which we learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come on later chapters. Let's move forward.
Elixir also provides data-types for expressing collections, such as lists and tuples, which we'll learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come in later chapters. Let's move forward.
@@ -160,8 +160,6 @@ iex> :observer.start()
> When running `iex` inside a project with `iex -S mix`, `observer` won't be available as a dependency. To do so, you will need to call the following functions before:
>
> ```elixir
> iex> Mix.ensure_application!(:wx) # Not necessary on Erlang/OTP 27+
> iex> Mix.ensure_application!(:runtime_tools) # Not necessary on Erlang/OTP 27+
> iex> Mix.ensure_application!(:observer)
> iex> :observer.start()
> ```
@@ -110,4 +110,4 @@ The example above will fetch the first 10 lines of the file you have selected. T
The `Enum` and `Stream` modules provide a wide range of functions, but you don't have to know all of them by heart. Familiarize yourself with `Enum.map/2`, `Enum.reduce/3` and other functions with either `map` or `reduce` in their names, and you will naturally build an intuition around the most important use cases. You may also focus on the `Enum` module first and only move to `Stream` for the particular scenarios where laziness is required, to either deal with slow resources or large, possibly infinite, collections.
Next, we'll look at a feature central to Elixir, Processes, which allows us to write concurrent, parallel and distributed programs in an easy and understandable way.
Next, we will look at an alternate approach of operating on collections: Comprehensions.
@@ -15,8 +15,8 @@ Let's get started.
If you haven't yet installed Elixir, visit our [installation page](https://elixir-lang.org/install.html). Once you are done, you can run `elixir --version` to get the current Elixir version. The requirements for this guide are:
* Elixir 1.15.0 onwards
* Erlang/OTP 26 onwards
* Elixir 1.18.0 onwards
* Erlang/OTP 27 onwards
If you are looking for other resources for learning Elixir, you can also consult the [learning page](https://elixir-lang.org/learning.html) of the official website.
@@ -206,3 +206,5 @@ We packed a lot into this small section, so let's break it down:
* the choice between iodata and chardata depends on the encoding of the IO device. If the file is opened without encoding, the file expects iodata, and the functions in the `IO` module starting with `bin*` must be used. The default IO device (`:stdio`) and files opened with `:utf8` encoding expect chardata and work with the remaining functions in the `IO` module;
This finishes our tour of IO devices and IO related functionality. We have learned about three Elixir modules - `IO`, `File`, and `Path` - as well as how the VM uses processes for the underlying IO mechanisms and how to use `chardata` and `iodata` for IO operations.
Next we will talk about a very important subject to Elixir developers: writing documentation.
@@ -151,6 +151,7 @@ defmodule Concat do
# A function head declaring defaults
def join(a, b, sep \\ " ")
# The separator is unused in this case, so we prefix it with underscore
def join(a, b, _sep) when b == "" do
a
end
@@ -165,7 +166,7 @@ IO.puts(Concat.join("Hello", "world")) #=> Hello world
IO.puts(Concat.join("Hello", "world", "_")) #=> Hello_world
```
When a variable is not used by a function or a clause, we add a leading underscore (`_`) to its name to signal this intent. This rule is also covered in our [Naming Conventions](../references/naming-conventions.md#underscore-_foo) document.
Function heads cannot have patterns nor guards. They may only define the argument names and their default values.
## Understanding Aliases
@@ -182,8 +183,7 @@ true
By using the `alias/2` directive, we are changing the atom the alias expands to.
Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms. By namespacing
those atoms elixir modules avoid conflicting with existing erlang modules.
Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms. By namespacing those atoms, Elixir modules avoid conflicting with existing Erlang modules.
```elixir
iex> List.flatten([1, [2], 3])
@@ -212,9 +212,11 @@ end
The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above.
> #### Module names are isolated {: .info}
>
> You don't have to define the `Foo` module before defining the `Foo.Bar` module, as they are effectively independent.
**Note**: in Elixir, you don't have to define the `Foo` module before being able to define the `Foo.Bar` module, as they are effectively independent. The above could also be written as:
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive, which we will learn in the next chapter:
```elixir
defmodule Foo.Bar do
@@ -225,20 +227,3 @@ defmodule Foo do
# Can still access it as `Bar`
end
```
Aliasing a nested module does not bring parent modules into scope. Consider the following example:
```elixir
defmodule Foo do
defmodule Bar do
defmodule Baz do
end
end
end
alias Foo.Bar.Baz
# The module `Foo.Bar.Baz` is now available as `Baz`
# However, the module `Foo.Bar` is *not* available as `Bar`
```
As we will see in later chapters, aliases also play a crucial role in macros, to guarantee they are hygienic.
+3 -7
View File
@@ -91,7 +91,7 @@ iex> ~w(foo bar bat)a
## Interpolation and escaping in textual sigils
Elixir supports some sigil variants to deal with escaping characters and interpolation. In particular, uppercase-letter textual sigils do not perform interpolation nor escaping. For example, although both `~s` and `~S` will return strings, the former allows escape codes and interpolation while the latter does not:
Sigils also help deal with escaping characters and interpolation. In particular, uppercase-letter textual sigils do not perform interpolation nor escaping, and most lowercase sigils have an uppercase variant. For example, although both `~s` and `~S` will return strings, the former allows escape codes and interpolation while the latter does not:
```elixir
iex> ~s(String with escape codes \x26 #{"inter" <> "polation"})
@@ -117,8 +117,6 @@ The following escape codes can be used in textual sigils:
* `\xDD` - represents a single byte in hexadecimal (such as `\x13`)
* `\uDDDD` and `\u{D...}` - represents a Unicode codepoint in hexadecimal (such as `\u{1F600}`)
In addition to those, a double quote inside a double-quoted string needs to be escaped as `\"`, and, analogously, a single quote inside a single-quoted char list needs to be escaped as `\'`. Nevertheless, it is better style to change delimiters as seen above than to escape them.
Sigils also support heredocs, that is, three double-quotes or single-quotes as separators:
```elixir
@@ -160,7 +158,7 @@ def convert(...)
## Calendar sigils
Elixir offers several sigils to deal with various flavors of times and dates.
Elixir offers sigils to deal with various flavors of times and dates.
### Date
@@ -240,6 +238,4 @@ iex> ~i(42)n
-42
```
Custom sigils may be either a single lowercase character, or an uppercase character followed by more uppercase characters and digits.
Sigils can also be used to do compile-time work with the help of macros. For example, regular expressions in Elixir are compiled into an efficient representation during compilation of the source code, therefore skipping this step at runtime. If you're interested in the subject, you can learn more about macros and check out how sigils are implemented in the `Kernel` module (where the `sigil_*` functions are defined).
Custom sigils may be either a single lowercase character, or an uppercase character followed by more uppercase characters and digits. In practice, they are often used to embed templating languages or even represent regular languages within Elixir itself. If you're interested in learning more, check out how sigils are implemented in the `Kernel` module (where the `sigil_*` functions/macros are defined) for a starting point.
@@ -303,4 +303,6 @@ iex> try do
** (CompileError) undefined variable "another_what_happened"
```
This finishes our introduction on `try`, `catch`, and `rescue`. You will find they are used less frequently in Elixir than in other languages. Next we will talk about a very important subject to Elixir developers: writing documentation.
This finishes our introduction on `try`, `catch`, and `rescue`. You will find they are used less frequently in Elixir than in other languages.
Next, we'll look at a feature central to Elixir, Processes, which allows us to write concurrent, parallel and distributed programs in an easy and understandable way.
@@ -241,7 +241,7 @@ $ mix help
mix deps # Lists dependencies and their status
mix deps.clean # Deletes the given dependencies' files
mix deps.compile # Compiles dependencies
mix deps.get # Gets all out of date dependencies
mix deps.get # Fetches unavailable and out of date dependencies
mix deps.tree # Prints the dependency tree
mix deps.unlock # Unlocks the given dependencies
mix deps.update # Updates the given dependencies
@@ -143,7 +143,7 @@ With this change, we are saying that we want to run `KV.Server.accept(4040)` as
Now that the server is part of the supervision tree, it should start automatically when we run the application. Run `iex -S mix` to boot the app and use the `telnet` client to make sure that everything still works:
```console
$ telnet 127.0.0.1 4321
$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
@@ -158,7 +158,7 @@ Yes, it works! However, can it handle more than one client?
Try to connect two telnet clients at the same time. When you do so, you will notice that the second client doesn't echo:
```console
$ telnet 127.0.0.1 4321
$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
@@ -14,12 +14,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Development
1.19 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
@@ -49,7 +48,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
Elixir version | Supported Erlang/OTP versions
:------------- | :-------------------------------
1.20 | 26 - 28
1.20 | 27 - 29
1.19 | 26 - 28
1.18 | 25 - 27
1.17 | 25 - 27
@@ -243,4 +242,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md#4-hard-deprecations
@@ -231,20 +231,62 @@ Inferring type signatures comes with a series of trade-offs:
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide type inference across dependencies: our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing, without requiring any effort from them and keeping the expressiveness of the language.
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
In the long term, Elixir developers who want static typing guarantees may explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
### False positives
Elixir's type inference generally avoids emitting false positive type violations: which are warnings emitted by the type checker when there are no runtime errors. However, in some situations, those may happen and are documented below.
#### `for`-comprehensions assume they are executed at least once
For comprehensions in Elixir assume they are executed at least once. Take this code:
```elixir
def example(x, list) do
for _i <- list do
Atom.to_string(x)
end
x + 1
end
```
`x + 1` will fail because it assumes `x` is an atom from the `Atom.to_string(x)` call, even though the function may raise no runtime error if `list` is an empty list. This is intentional, as it helps find discrepancies inside and outside comprehensions. You can address this by explicitly wrapping the comprehension in a `if list != [] do` block (or similar condition).
#### Struct update syntax must be statically proven
Elixir will warn if you use the struct update syntax and it is not statically proven that the given value does not have said struct type. For example:
```elixir
user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
Even though it is guaranteed at runtime that user is always a `User` struct. If the type system cannot prove it, it will emit a typing violation. This is how stuct updates work by design. In such cases, you can address it by matching on the struct when the user variable is defined:
```elixir
%User{} = user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
## Roadmap
The current milestone is to implement type inference of existing codebases, as well as type checking of all language constructs, without changes to the Elixir language. At this stage, we want to collect feedback on the quality of error messages and performance, and therefore the type system has no user facing API. Full type inference of patterns was released in Elixir v1.18, and complete inference is expected as part of Elixir v1.20.
At this moment, Elixir implements type inference of all language constructs. The goal is to assess performance and collect feedback on the quality of error messages, before introducing user facing types.
If the results are satisfactory, the next milestone will include a mechanism for defining typed structs. Elixir programs frequently pattern match on structs, which reveals information about the struct fields, but it knows nothing about their respective types. By propagating types from structs and their fields throughout the program, we will increase the type system’s ability to find errors while further straining our type system implementation. Proposals including the required changes to the language surface will be sent to the community once we reach this stage.
The third milestone is to introduce set-theoretic type signatures for functions. Unfortunately, the existing Erlang Typespecs are not precise enough for set-theoretic types and they will be phased out of the language and have their postprocessing moved into a separate library once this stage concludes.
## Resources
* (Paper) ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391)
* (Video) ["The foundations of the Elixir type system" by José Valim](https://youtu.be/giYbq4HmfGA)
* (Video) ["Precision in type system design" by José Valim](https://youtu.be/Ay-gnCqDw9o?t=102)
## Acknowledgements
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), and [Tidewave](https://tidewave.ai/).
@@ -72,6 +72,15 @@ iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
```
However, a variable cannot be defined through itself in the same pattern. This includes cases where the variable appears on both sides of a nested match operator:
```elixir
{:ok, x} = {x, :ok}
%{a: value} = %{b: value}
```
Such patterns are rejected as cyclic variable definitions. You can rewrite them by matching once and then using guards or separate comparisons for the additional constraints.
The underscore variable (`_`) has a special meaning as it can never be bound to any value. It is especially useful when you don't care about certain value in a pattern:
```iex
+149
View File
@@ -0,0 +1,149 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Software Bill of Materials
A Software Bill of Materials (SBoM) is a structured inventory of the components that make up a software system. This guide explains what SBoMs are, why they matter, and how to generate them for Elixir projects.
## What is an SBoM?
An SBoM is a formal, machine-readable record of all components in a piece of software. Think of it as a detailed ingredient list for your application. A typical SBoM includes:
* **Dependencies**: all libraries and packages your project uses
* **Versions**: the exact version of each component
* **Source locations**: where each component was obtained (Hex, GitHub, etc.)
* **Checksums**: cryptographic hashes to verify integrity
* **Licensing information**: the license under which each component is distributed
Two widely adopted standards exist for SBoM formats:
* [CycloneDX](https://cyclonedx.org/): a lightweight standard designed for security contexts
* [SPDX](https://spdx.dev/): a more comprehensive standard originally focused on licensing
Both formats are machine-readable (JSON, XML) and designed to be consumed by automated tooling.
> #### SBoM is an inventory, not a certification {: .info}
>
> An SBoM does not claim that software is secure, compliant, or free of vulnerabilities.
> It simply provides a detailed inventory that enables further analysis. Security and
> compliance assessments are performed by separate tools that consume the SBoM.
## Why generate SBoMs?
There are three main reasons to generate SBoMs for your projects.
### Vulnerability analysis
When a security vulnerability (CVE) is discovered in a library, you need to quickly determine if your projects are affected. SBoMs make this possible by providing a complete inventory of your dependencies.
Tools like [OWASP Dependency-Track](https://dependencytrack.org/) continuously monitor your SBoMs against vulnerability databases. When a new CVE is published, you get notified if any of your projects use the affected component.
Without an SBoM, answering "are we affected by CVE-2024-XXXXX?" means manually checking each project, which is time-consuming and error-prone.
### Regulatory requirements
SBoMs are required by some regulations and procurement policies:
* **US Executive Order 14028** (2021) requires SBoMs for certain software supplied to the U.S. federal government, particularly for designated critical software
* **EU Cyber Resilience Act** introduces requirements for component inventories (commonly fulfilled using SBoMs) for products with digital elements placed on the EU market
* **Safety-critical industries** (medical devices, automotive, aerospace) often require detailed component inventories as part of certification
Customers and partners may also request SBoMs as part of their own compliance efforts.
### License compliance
Every dependency in your project comes with a license. An SBoM provides a starting point for license review by listing the declared license for each package. This helps you:
* Get an overview of licenses in your dependency tree
* Flag packages that may need closer review
* Support due diligence for acquisitions, audits, or legal review
Note that package-level license information (as provided by mix_sbom) reflects what the package declares, not necessarily all licenses present in its source files. For thorough license compliance, file-level scanning tools like ORT provide deeper analysis.
## Generating SBoMs with mix_sbom
[mix_sbom](https://github.com/erlef/mix_sbom) is an EEF project that generates CycloneDX SBoMs for Elixir projects.
### Installation
There are several ways to install mix_sbom:
* **As a project dependency**: add `sbom` to your `mix.exs`
* **As a global escript**: run `mix escript.install hex sbom` (requires Elixir 1.19.4+)
* **As a standalone binary**: download from the [releases page](https://github.com/erlef/mix_sbom/releases)
The standalone binary is useful for CI environments or when you don't want to modify your project's dependencies. It requires no local Elixir or Erlang installation.
### Basic usage
To generate an SBoM for your project using the standalone binary:
```console
$ mix_sbom cyclonedx /path/to/your/project
```
This creates a `bom.cdx.json` file in CycloneDX format containing your project's complete dependency tree.
### Common options
The most useful options are:
* `-o, --output PATH`: specify the output file (default: `bom.cdx.json`)
* `-t, --format FORMAT`: output format (`json`, `xml`, or `protobuf`)
* `-s, --schema VERSION`: CycloneDX schema version
For example, to generate an XML format SBoM:
```console
$ mix_sbom cyclonedx --format xml --output sbom.xml /path/to/project
```
## CI integration
For automated SBoM generation, mix_sbom provides a GitHub Action:
```yaml
name: Generate SBoM
on:
release:
types: [published]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/mix_sbom@v0
with:
path: "."
format: "json"
- uses: actions/upload-artifact@v4
with:
name: sbom
path: bom.cdx.json
```
This workflow generates an SBoM whenever you publish a release and uploads it as a build artifact. See the [action's documentation](https://github.com/erlef/mix_sbom) for additional options.
## Deeper analysis with ORT
mix_sbom provides package-level license information based on what each dependency declares. However, some compliance workflows require file-level scanning. A package might declare an MIT license but contain individual files under different licenses, or include vendored code with its own licensing terms.
The [OSS Review Toolkit (ORT)](https://oss-review-toolkit.org/) addresses this by scanning actual source files for license headers and copyright notices. ORT supports Mix projects and provides:
* **File-level license detection**: scans source code for license texts and SPDX identifiers
* **Copyright holder identification**: extracts copyright notices from files
* **Policy enforcement**: define rules for allowed and denied licenses
* **Multi-ecosystem support**: analyze projects that span multiple package managers
For organizations with strict compliance requirements, ORT complements mix_sbom by providing the deeper analysis needed for thorough license audits.
See the [ORT Mix plugin documentation](https://oss-review-toolkit.org/ort/docs/plugins/package-managers/Mix) for details.
## Next steps
* [CycloneDX Specification](https://cyclonedx.org/specification/overview/): learn more about the SBoM format
* [OWASP Dependency-Track](https://dependencytrack.org/): continuous SBoM analysis platform
* [mix_sbom documentation](https://hexdocs.pm/sbom): full documentation and advanced options
+6 -1
View File
@@ -3,7 +3,11 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
# Returns config for Elixir docs (exclusively)
canonical = System.fetch_env!("CANONICAL")
canonical =
case System.fetch_env!("CANONICAL") do
"" -> System.version() <> "/"
canonical -> canonical
end
[
search: [
@@ -61,6 +65,7 @@ canonical = System.fetch_env!("CANONICAL")
"lib/elixir/pages/references/operators.md",
"lib/elixir/pages/references/patterns-and-guards.md",
"lib/elixir/pages/references/syntax-reference.md",
"lib/elixir/pages/references/sbom.md",
"lib/elixir/pages/references/typespecs.md",
"lib/elixir/pages/references/unicode-syntax.md",
"lib/elixir/pages/mix-and-otp/introduction-to-mix.md",
+5 -1
View File
@@ -2,7 +2,11 @@
# SPDX-FileCopyrightText: 2021 The Elixir Team
# Returns config for other apps except Elixir
canonical = System.fetch_env!("CANONICAL")
canonical =
case System.fetch_env!("CANONICAL") do
"" -> System.version() <> "/"
canonical -> canonical
end
[
search: [
+102 -91
View File
@@ -9,10 +9,9 @@
-export([start_cli/0, start/0]).
-export([start/2, stop/1, config_change/3]).
-export([
string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5,
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3,
eval_quoted/4, eval_local_handler/2, eval_external_handler/3,
format_token_error/1
string_to_tokens/5, tokens_to_quoted/3, string_to_quoted/5, 'string_to_quoted!'/5,
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_forms/4, eval_quoted/3, eval_quoted/4,
erl_eval/3, eval_local_handler/2, eval_external_handler/3, emit_warnings/3
]).
-include("elixir.hrl").
-define(system, 'Elixir.System').
@@ -64,11 +63,6 @@ start(_Type, _Args) ->
application:set_env(elixir, ansi_enabled, prim_tty:isatty(stdout) == true)
end,
%% Store the initial dbg_callback value before any runtime modifications.
%% This allows Mix compiler to detect config changes vs runtime changes
%% (e.g., Kino wrapping dbg_callback at runtime should not trigger recompilation).
{ok, InitialDbgCallback} = application:get_env(elixir, dbg_callback),
Tokenizer = case code:ensure_loaded('Elixir.String.Tokenizer') of
{module, Mod} -> Mod;
_ -> elixir_tokenizer
@@ -92,16 +86,16 @@ start(_Type, _Args) ->
{no_halt, false},
%% Compiler options
{debug_info, true},
{docs, true},
{ignore_already_consolidated, false},
{ignore_module_conflict, false},
{initial_dbg_callback, InitialDbgCallback},
{infer_signatures, [elixir]},
{module_definition, compiled},
{no_warn_undefined, []},
{on_undefined_variable, raise},
{parser_options, [{columns, true}]},
{debug_info, true},
{relative_paths, true},
{no_warn_undefined, []},
{tracers, []}
| URIConfig
],
@@ -133,20 +127,20 @@ preload_common_modules() ->
parse_otp_release() ->
%% Whenever we change this check, we should also change Makefile.
case string:to_integer(erlang:system_info(otp_release)) of
{Num, _} when Num >= 26 ->
{Num, _} when Num >= 27 ->
case Num == 28 andalso (code:ensure_loaded(re) == {module, re}) andalso not erlang:function_exported(re, import, 1) of
true ->
io:format(standard_error,
"warning! Erlang/OTP 28.0 detected.~n"
"Regexes will be re-compiled from source at runtime, which will cause degraded performance.~n"
"This can be fixed by using Erlang OTP 28.1+ or 27-.~n"
"This can be fixed by using Erlang OTP 28.1+ or 27.~n"
, []);
false ->
ok
end,
Num;
_ ->
io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 26+~n", []),
io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 27+~n", []),
erlang:halt(1)
end.
@@ -310,53 +304,70 @@ eval_forms(Tree, Binding, OrigE) ->
eval_forms(Tree, Binding, OrigE, []).
eval_forms(Tree, Binding, OrigE, Opts) ->
Prune = proplists:get_value(prune_binding, Opts, false),
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
E = elixir_env:with_vars(OrigE, ExVars),
ExS = elixir_env:env_to_ex(E),
ErlS = elixir_erl_var:from_env(E, ErlVars),
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
%% We save and restore the previous dbg_callback so nested eval calls in
%% the same process do not clobber the outer callback.
PreviousDbg = erlang:get({elixir, dbg_callback}),
case proplists:get_value(dbg_callback, Opts) of
undefined -> ok;
DbgCallback -> erlang:put({elixir, dbg_callback}, DbgCallback)
end,
try
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
E = elixir_env:with_vars(OrigE, ExVars),
ExS = elixir_env:env_to_ex(E),
ErlS = elixir_erl_var:from_env(E, ErlVars),
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
case Erl of
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
if
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
true -> {Value, Binding, NewE}
end;
case Erl of
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
if
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
true -> {Value, Binding, NewE}
end;
_ ->
Exprs =
case Erl of
{block, _, BlockExprs} -> BlockExprs;
_ -> [Erl]
end,
_ ->
{value, Value, NewBinding} = erl_eval(Erl, ErlBinding, NewE),
PruneBefore = if Prune -> length(Binding); true -> -1 end,
%% We use remote names so eval works across Elixir versions.
LocalHandler = {value, fun ?MODULE:eval_local_handler/2},
ExternalHandler = {value, fun ?MODULE:eval_external_handler/3},
{DumpedBinding, DumpedVars} =
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
{value, Value, NewBinding} =
try
%% ?elixir_eval_env is used by the external handler.
%%
%% The reason why we use the process dictionary to pass the environment
%% is because we want to avoid passing closures to erl_eval, as that
%% would effectively tie the eval code to the Elixir version and it is
%% best if it depends solely on Erlang/OTP.
%%
%% The downside is that functions that escape the eval context will no
%% longer have the original environment they came from.
erlang:put(?elixir_eval_env, NewE),
erl_eval:exprs(Exprs, ErlBinding, LocalHandler, ExternalHandler)
after
erlang:erase(?elixir_eval_env)
end,
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
end
after
case PreviousDbg of
undefined -> erlang:erase({elixir, dbg_callback});
_ -> erlang:put({elixir, dbg_callback}, PreviousDbg)
end
end.
PruneBefore = if Prune -> length(Binding); true -> -1 end,
%% Evaluate Erlang code with careful handling of local and external functions
erl_eval(Expr, Binding, Env) ->
%% We use remote names so eval works across Elixir versions
LocalHandler = {value, fun ?MODULE:eval_local_handler/2},
ExternalHandler = {value, fun ?MODULE:eval_external_handler/3},
{DumpedBinding, DumpedVars} =
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
%% ?elixir_eval_env is used by the external handler.
%%
%% The reason why we use the process dictionary to pass the environment
%% is because we want to avoid passing closures to erl_eval, as that
%% would effectively tie the eval code to the Elixir version and it is
%% best if it depends solely on Erlang/OTP.
%%
%% The downside is that functions that escape the eval context will no
%% longer have the original environment they came from.
%%
%% We save and restore the previous env so nested eval calls in the same
%% process do not clobber the outer env.
PreviousEvalEnv = erlang:get(?elixir_eval_env),
erlang:put(?elixir_eval_env, Env),
try
erl_eval:expr(Expr, Binding, LocalHandler, ExternalHandler)
after
case PreviousEvalEnv of
undefined -> erlang:erase(?elixir_eval_env);
_ -> erlang:put(?elixir_eval_env, PreviousEvalEnv)
end
end.
eval_local_handler(FunName, Args) ->
@@ -405,10 +416,8 @@ eval_external_handler(Ann, FunOrModFun, Args) ->
%% Add file+line information at the bottom
Bottom =
case erlang:get(?elixir_eval_env) of
#{file := File} ->
[{elixir_eval, '__FILE__', 1,
[{file, elixir_utils:characters_to_list(File)}, {line, erl_anno:line(Ann)}]}];
#{'__struct__' := 'Elixir.Macro.Env'} = E ->
'Elixir.Macro.Env':stacktrace(E#{line := erl_anno:line(Ann)});
_ ->
[]
end,
@@ -443,40 +452,36 @@ quoted_to_erl(Quoted, ErlS, ExS, Env) ->
%% Converts a given string (charlist) into quote expression
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_binary(File) ->
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_integer(StartColumn), is_binary(File) ->
case elixir_tokenizer:tokenize(String, StartLine, StartColumn, Opts) of
{ok, _Line, _Column, [], Tokens, []} ->
{ok, lists:reverse(Tokens)};
{ok, _Line, _Column, Warnings, Tokens, Terminators} ->
(lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso
[elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)],
{ok, lists:reverse(Tokens, Terminators)};
{ok, lists:reverse(Tokens, Terminators), Warnings};
{error, Info, _Rest, _Warnings, _SoFar} ->
{error, format_token_error(Info)}
{error, elixir_tokenizer:format_error(Info)}
end.
format_token_error({Location, {ErrorPrefix, ErrorSuffix}, Token}) ->
{Location, {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)};
format_token_error({Location, Error, Token}) ->
{Location, to_binary(Error), to_binary(Token)}.
tokens_to_quoted(Tokens, WarningFile, Opts) ->
handle_parsing_opts(WarningFile, Opts),
tokens_to_quoted(Tokens, _WarningFile, Opts) ->
put_parsing_state(Opts),
try elixir_parser:parse(Tokens) of
{ok, Forms} ->
{ok, Forms};
{ok, Forms, get(elixir_parser_warnings)};
{error, {Line, _, [{ErrorPrefix, ErrorSuffix}, Token]}} ->
{error, {parser_location(Line), {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)}};
{error, {Line, _, [Error, Token]}} ->
{error, {parser_location(Line), to_binary(Error), to_binary(Token)}}
after
erase(elixir_parser_warning_file),
erase(elixir_parser_warnings),
erase(elixir_parser_columns),
erase(elixir_token_metadata),
erase(elixir_literal_encoder)
end.
emit_warnings(Warnings, File, Opts) ->
(Warnings /= []) andalso
(lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso
[elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)].
parser_location({Line, Column, _}) ->
[{line, Line}, {column, Column}];
parser_location(Meta) ->
@@ -491,17 +496,28 @@ parser_location(Meta) ->
false -> [{line, Line}]
end.
'string_to_quoted!'(String, StartLine, StartColumn, File, Opts) ->
string_to_quoted(String, StartLine, StartColumn, File, Opts) ->
case string_to_tokens(String, StartLine, StartColumn, File, Opts) of
{ok, Tokens} ->
{ok, Tokens, Warnings1} ->
emit_warnings(Warnings1, File, Opts),
case tokens_to_quoted(Tokens, File, Opts) of
{ok, Forms} ->
Forms;
{error, {Meta, Error, Token}} ->
Indentation = proplists:get_value(indentation, Opts, 0),
Input = {String, StartLine, StartColumn, Indentation},
elixir_errors:parse_error(Meta, File, Error, Token, Input)
{ok, Forms, Warnings2} ->
emit_warnings(Warnings2, File, Opts),
{ok, Forms};
{error, Error} ->
{error, Error}
end;
{error, Error} ->
{error, Error}
end.
'string_to_quoted!'(String, StartLine, StartColumn, File, Opts) ->
case string_to_quoted(String, StartLine, StartColumn, File, Opts) of
{ok, Forms} ->
Forms;
{error, {Meta, Error, Token}} ->
Indentation = proplists:get_value(indentation, Opts, 0),
Input = {String, StartLine, StartColumn, Indentation},
@@ -511,12 +527,7 @@ parser_location(Meta) ->
to_binary(List) when is_list(List) -> elixir_utils:characters_to_binary(List);
to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom).
handle_parsing_opts(File, Opts) ->
WarningFile =
case lists:keyfind(emit_warnings, 1, Opts) of
{emit_warnings, false} -> nil;
_ -> File
end,
put_parsing_state(Opts) ->
LiteralEncoder =
case lists:keyfind(literal_encoder, 1, Opts) of
{literal_encoder, Fun} -> Fun;
@@ -524,7 +535,7 @@ handle_parsing_opts(File, Opts) ->
end,
TokenMetadata = lists:keyfind(token_metadata, 1, Opts) == {token_metadata, true},
Columns = lists:keyfind(columns, 1, Opts) == {columns, true},
put(elixir_parser_warning_file, WarningFile),
put(elixir_parser_warnings, []),
put(elixir_parser_columns, Columns),
put(elixir_token_metadata, TokenMetadata),
put(elixir_literal_encoder, LiteralEncoder).
+8 -3
View File
@@ -11,6 +11,9 @@
-define(tracker, 'Elixir.Kernel.LexicalTracker').
-record(elixir_ex, {
%% Stores that the function will fail, which can be used
%% to run more expensive checks upfront
tainted_function=false,
%% Stores if __CALLER__ is allowed
caller=false,
%% Stores the variables available before a match.
@@ -28,8 +31,8 @@
prematch=none,
%% Stores if __STACKTRACE__ is allowed
stacktrace=false,
%% A map of unused vars and a version counter for vars
unused={#{}, 0},
%% A map of unused vars
unused=#{},
%% A list of modules defined in functions (runtime)
runtime_modules=[],
%% A tuple with maps of read and optional write current vars.
@@ -43,7 +46,9 @@
%% if you write foo(a = 123), the value of `a` cannot be
%% read in the following argument, only after the call
%%
vars={#{}, false}
vars={#{}, false},
%% Stores expression version counter
version=0
}).
-record(elixir_erl, {
+4 -2
View File
@@ -61,7 +61,7 @@ expand(BitstrMeta, Fun, [H | T], Acc, S, E, Alignment, RequireSize) ->
InferredMeta = [{inferred_bitstring_spec, true} | Meta],
EAcc = concat_or_prepend_bitstring(InferredMeta, ELeft, ERight, Acc, ES, MatchOrRequireSize),
expand(Meta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
expand(BitstrMeta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
extract_meta({_, Meta, _}, _) -> Meta;
extract_meta(_, Meta) -> Meta.
@@ -115,7 +115,9 @@ concat_or_prepend_bitstring(Meta, {'<<>>', PartsMeta, Parts} = ELeft, ERight, Ac
[{'::', Meta, [ELeft, ERight]} | Acc]
end;
{bitstring, _, nil} ->
lists:reverse(Parts, Acc)
lists:reverse(Parts, Acc);
_ ->
[{'::', Meta, [ELeft, ERight]} | Acc]
end;
concat_or_prepend_bitstring(Meta, ELeft, ERight, Acc, _E, _RequireSize) ->
[{'::', Meta, [ELeft, ERight]} | Acc].
+15 -10
View File
@@ -155,13 +155,14 @@ recur_cycles(Cycles, Current, Source, Seen, SkipList, Meta, Expr, E) ->
%% Match
match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
#elixir_ex{vars=Current, unused={_, Counter} = Unused} = AfterS,
#elixir_ex{vars=Current, unused=Unused, version=Counter} = AfterS,
#elixir_ex{vars={Read, _}, prematch=Prematch} = BeforeS,
CallS = BeforeS#elixir_ex{
prematch={Read, {#{}, []}, Counter},
unused=Unused,
vars=Current
vars=Current,
version=Counter
},
CallE = E#{context := match},
@@ -170,7 +171,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
#elixir_ex{
vars=NewCurrent,
unused=NewUnused,
prematch={_, Cycles, _}
prematch={_, Cycles, _},
version=NewCounter
} = SE,
validate_cycles(Cycles, Meta, {match, Expr}, E),
@@ -178,7 +180,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
EndS = AfterS#elixir_ex{
prematch=Prematch,
unused=NewUnused,
vars=NewCurrent
vars=NewCurrent,
version=NewCounter
},
EndE = EE#{context := ?key(E, context)},
@@ -319,7 +322,9 @@ with(Meta, Args, S, E) ->
ok
end,
{{with, Meta, EExprs ++ [[{do, EDo} | EOpts]]}, S3, E}.
#elixir_ex{version=Counter} = S3,
{{with, [{version, Counter} | Meta], EExprs ++ [[{do, EDo} | EOpts]]},
S3#elixir_ex{version=Counter+1}, E}.
expand_with({'<-', Meta, [Left, Right]}, {S, E, HasMatch}) ->
{ERight, SR, ER} = elixir_expand:expand(Right, S, E),
@@ -535,8 +540,8 @@ warn_catch_before_rescue([_ | Rest], Meta, E, Found) ->
warn_catch_before_rescue(Rest, Meta, E, Found).
origin(Meta, Default) ->
case lists:keyfind(origin, 1, Meta) of
{origin, Origin} -> Origin;
case lists:keyfind(definition, 1, Meta) of
{definition, Definition} -> Definition;
false -> Default
end.
@@ -557,16 +562,16 @@ format_error({recursive, Vars, TypeExpr}) ->
Message =
case lists:map(fun({Name, Context}) -> elixir_utils:var_info(Name, Context) end, lists:sort(Vars)) of
[Var] ->
io_lib:format("the variable ~ts is defined in function of itself", [Var]);
io_lib:format("the variable ~ts depends on itself through the pattern", [Var]);
[Var1, Var2] ->
io_lib:format("the variable ~ts is defined recursively in function of ~ts", [Var1, Var2]);
io_lib:format("the variable ~ts depends on ~ts through the pattern", [Var1, Var2]);
[Head | Tail] ->
List = lists:foldl(fun(X, Acc) -> [Acc, $,, $\s, X] end, Head, Tail),
io_lib:format("the following variables form a cycle: ~ts", [List])
end,
io_lib:format(
"recursive variable definition in patterns:~n~n~ts~n~n~ts",
"cyclic variable definition in patterns:~n~n~ts~n~n~ts",
[Code, Message]
);
+19 -2
View File
@@ -59,6 +59,9 @@ handle_call({acquire, Path}, From, Config) ->
handle_call(required, _From, Config) ->
{reply, [F || {F, true} <- maps:to_list(Config#elixir_code_server.required)], Config};
handle_call({release, Path}, _From, Config) ->
{reply, ok, release(Path, Config)};
handle_call(retrieve_compiler_module, _From, Config) ->
case Config#elixir_code_server.mod_pool of
{Used, [Mod | Unused], Counter} ->
@@ -70,7 +73,7 @@ handle_call(retrieve_compiler_module, _From, Config) ->
handle_call(purge_compiler_modules, _From, Config) ->
{Used, Unused, Counter} = Config#elixir_code_server.mod_pool,
purge_used(Used),
Mods = lists:map(fun({Mod, _}) -> Mod end, Used),
Mods = [Mod || {Mod, Purgeable} <- Used, Purgeable],
ModPool = {[], Mods ++ Unused, Counter},
{reply, {ok, length(Used)}, Config#elixir_code_server{mod_pool=ModPool}};
@@ -112,7 +115,7 @@ handle_cast(purge_compiler_modules, Config) ->
%% purge them asynchronously, especially because they can
%% block the code server. Ideally we would purge them in
%% batches, but that's not supported at the moment.
Mods = lists:map(fun({Mod, _}) -> Mod end, Used),
Mods = [Mod || {Mod, Purgeable} <- Used, Purgeable],
Opts = [{monitor, [{tag, {purged, Mods}}]}],
erlang:spawn_opt(fun() -> purge_used(Used) end, Opts)
end,
@@ -140,6 +143,20 @@ terminate(_Reason, _Config) ->
code_change(_Old, Config, _Extra) ->
{ok, Config}.
release(Path, Config) ->
Current = Config#elixir_code_server.required,
case maps:find(Path, Current) of
{ok, []} ->
Released = maps:remove(Path, Current),
Config#elixir_code_server{required=Released};
{ok, [Next | Waiting]} ->
_ = gen_server:reply(Next, proceed),
Released = maps:put(Path, Waiting, Current),
Config#elixir_code_server{required=Released};
error ->
Config
end.
compiler_module(I) ->
list_to_atom("elixir_compiler_" ++ integer_to_list(I)).
+40 -18
View File
@@ -4,7 +4,7 @@
%% Elixir compiler front-end to the Erlang backend.
-module(elixir_compiler).
-export([string/3, quoted/3, bootstrap/0, file/2, compile/4]).
-export([string/3, quoted/3, bootstrap/0, file/2, compile/4, interpret/3]).
-include("elixir.hrl").
string(Contents, File, Callback) ->
@@ -20,7 +20,7 @@ quoted(Forms, File, Callback) ->
elixir_lexical:run(
Env,
fun (LexicalEnv) -> maybe_fast_compile(Forms, LexicalEnv) end,
fun (LexicalEnv) -> optimize_defmodule(Forms, LexicalEnv) end,
fun (#{lexical_tracker := Pid}) -> Callback(File, Pid) end
),
@@ -33,17 +33,37 @@ file(File, Callback) ->
{ok, Bin} = file:read_file(File),
string(elixir_utils:characters_to_list(Bin), File, Callback).
%% Evaluates the given code through the Erlang compiler.
%% It may end-up evaluating the code if it is deemed a
%% more efficient strategy depending on the code snippet.
maybe_fast_compile(Forms, E) ->
case (?key(E, module) == nil) andalso allows_fast_compilation(Forms) andalso
%% In case the forms only holds defmodules, we optimize
%% it by expanding them directly.
optimize_defmodule(Forms, E) ->
case (?key(E, module) == nil) andalso only_defmodule(Forms) andalso
(not elixir_config:is_bootstrap()) of
true -> fast_compile(Forms, E);
true -> expand_defmodule(Forms, E);
false -> compile(Forms, [], [], E)
end,
ok.
%% A version of compilation that uses eval (interpreted)
interpret(Quoted, ArgsList, #{line := Line} = E) ->
{Expanded, SE, EE} = elixir_expand:expand(Quoted, elixir_env:env_to_ex(E), E),
elixir_env:check_unused_vars(SE, EE),
{Vars, TS} = elixir_erl_var:from_env(E),
{ErlExprs, _} = elixir_erl_pass:translate(Expanded, erl_anno:new(Line), TS),
ListBinding = lists:zipwith(fun({_, Var}, Arg) -> {Var, Arg} end, Vars, ArgsList),
Binding = maps:from_list(ListBinding),
{value, Result, _} =
try
elixir:erl_eval(ErlExprs, Binding, E)
catch
Kind:Reason:Stacktrace ->
erlang:raise(Kind, Reason, Stacktrace ++ 'Elixir.Macro.Env':stacktrace(E))
end,
{Result, SE, EE}.
compile(Quoted, ArgsList, CompilerOpts, #{line := Line} = E) ->
Block = no_tail_optimize([{line, Line}], Quoted),
{Expanded, SE, EE} = elixir_expand:expand(Block, elixir_env:env_to_ex(E), E),
@@ -78,9 +98,11 @@ do_is_purgeable(<<_:4/binary, Size:32, Beam/binary>>) ->
do_is_purgeable(Rest).
dispatch(Module, Fun, Args, Purgeable) ->
Res = Module:Fun(Args),
return_compiler_module(Module, Purgeable),
Res.
try
Module:Fun(Args)
after
return_compiler_module(Module, Purgeable)
end.
code_fun(nil) -> '__FILE__';
code_fun(_) -> '__MODULE__'.
@@ -107,16 +129,16 @@ retrieve_compiler_module() ->
return_compiler_module(Module, Purgeable) ->
elixir_code_server:cast({return_compiler_module, Module, Purgeable}).
allows_fast_compilation({'__block__', _, Exprs}) ->
lists:all(fun allows_fast_compilation/1, Exprs);
allows_fast_compilation({defmodule, _, [_, [{do, _}]]}) ->
only_defmodule({'__block__', _, Exprs}) ->
lists:all(fun only_defmodule/1, Exprs);
only_defmodule({defmodule, _, [_, [{do, _}]]}) ->
true;
allows_fast_compilation(_) ->
only_defmodule(_) ->
false.
fast_compile({'__block__', _, Exprs}, E) ->
lists:foldl(fun(Expr, _) -> fast_compile(Expr, E) end, nil, Exprs);
fast_compile({defmodule, Meta, [Mod, [{do, Block}]]}, NoLineE) ->
expand_defmodule({'__block__', _, Exprs}, E) ->
lists:foldl(fun(Expr, _) -> expand_defmodule(Expr, E) end, nil, Exprs);
expand_defmodule({defmodule, Meta, [Mod, [{do, Block}]]}, NoLineE) ->
E = NoLineE#{line := ?line(Meta)},
Expanded = case Mod of
+18 -16
View File
@@ -238,15 +238,16 @@ run_with_location_change(File, #{file := File} = E, Callback) ->
run_with_location_change(File, E, Callback) ->
elixir_lexical:with_file(File, E, Callback).
def_to_clauses(_Kind, Meta, Args, [], nil, E) ->
def_to_clauses(_Kind, Meta, Args, Guards, nil, E) ->
check_args_for_function_head(Meta, Args, E),
(Guards /= []) andalso elixir_errors:module_error(Meta, E, ?MODULE, {invalid_function_head, guards}),
[];
def_to_clauses(_Kind, Meta, Args, Guards, [{do, Body}], _E) ->
[{Meta, Args, Guards, Body}];
def_to_clauses(Kind, Meta, Args, Guards, Body, E) ->
case is_list(Body) andalso lists:keyfind(do, 1, Body) of
{do, _} ->
[{Meta, Args, Guards, {'try', [{origin, Kind} | Meta], [Body]}}];
[{Meta, Args, Guards, {'try', [{definition, Kind} | Meta], [Body]}}];
false ->
elixir_errors:file_error(Meta, E, elixir_expand, {missing_option, Kind, [do]})
end.
@@ -294,17 +295,17 @@ store_definition(CheckClauses, Kind, Meta, Name, Arity, File, Module, Defaults,
%% Handling of defaults
unpack_defaults(Kind, Meta, Name, Args, S, E) ->
{Expanded, #elixir_ex{unused={_, VersionOffset}}} = expand_defaults(Args, S, E#{context := nil}, []),
unpack_expanded(Kind, Meta, Name, Expanded, VersionOffset, [], []).
{Expanded, #elixir_ex{version=Counter}} = expand_defaults(Args, S, E#{context := nil}, []),
unpack_expanded(Kind, Meta, Name, Expanded, Counter, [], []).
unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, VersionOffset, Acc, Clauses) ->
Base = match_defaults(Acc, length(Acc) + VersionOffset, []),
{Args, Invoke} = extract_defaults(List, length(Base) + VersionOffset, [], []),
unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, Counter, Acc, Clauses) ->
Base = match_defaults(Acc, length(Acc) + Counter, []),
{Args, Invoke} = extract_defaults(List, length(Base) + Counter, [], []),
Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}}, {default, true} | DefaultMeta], Base ++ Invoke}},
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [Expr | Acc], [Clause | Clauses]);
unpack_expanded(Kind, Meta, Name, [H | T], VersionOffset, Acc, Clauses) ->
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [H | Acc], Clauses);
unpack_expanded(_Kind, _Meta, _Name, [], _VersionOffset, Acc, Clauses) ->
unpack_expanded(Kind, Meta, Name, T, Counter, [Expr | Acc], [Clause | Clauses]);
unpack_expanded(Kind, Meta, Name, [H | T], Counter, Acc, Clauses) ->
unpack_expanded(Kind, Meta, Name, T, Counter, [H | Acc], Clauses);
unpack_expanded(_Kind, _Meta, _Name, [], _Counter, Acc, Clauses) ->
{lists:reverse(Acc), lists:reverse(Clauses)}.
expand_defaults([{'\\\\', Meta, [Expr, Default]} | Args], S, E, Acc) ->
@@ -374,7 +375,7 @@ check_valid_defaults(_Meta, _File, _Name, _Arity, _Kind, 0, _StoredMeta, _Stored
check_args_for_function_head(Meta, Args, E) ->
[begin
elixir_errors:module_error(Meta, E, ?MODULE, invalid_args_for_function_head)
elixir_errors:module_error(Meta, E, ?MODULE, {invalid_function_head, patterns})
end || Arg <- Args, invalid_arg(Arg)].
invalid_arg({Name, _, Kind}) when is_atom(Name), is_atom(Kind) -> false;
@@ -486,16 +487,17 @@ format_error({no_alias, Atom}) ->
format_error({invalid_def, Kind, NameAndArgs}) ->
io_lib:format("invalid syntax in ~ts ~ts", [Kind, 'Elixir.Macro':to_string(NameAndArgs)]);
format_error(invalid_args_for_function_head) ->
"patterns are not allowed in function head, only variables and default arguments (using \\\\)\n"
format_error({invalid_function_head, Prefix}) ->
atom_to_list(Prefix) ++ (
" are not allowed in function head, only variables and default arguments (using \\\\)\n"
"\n"
"If you did not intend to define a function head, make sure your function "
"definition has the proper syntax by wrapping the arguments in parentheses "
"and using the do instruction accordingly:\n\n"
"and using the do keyword accordingly:\n\n"
" def add(a, b), do: a + b\n\n"
" def add(a, b) do\n"
" a + b\n"
" end\n";
" end\n");
format_error({'__info__', Kind}) ->
io_lib:format("cannot define ~ts __info__/1 as it is automatically defined by Elixir", [Kind]);
+2 -2
View File
@@ -349,7 +349,7 @@ prune_stacktrace([{_, _, [Caller | _], _} | _], _MFA, Info, {ok, Caller}) ->
prune_stacktrace([{M, F, A, _} | _], {M, F, A}, Info, _E) ->
Info;
%% We've reached the elixir_dispatch internals, skip it with the rest
prune_stacktrace([{Mod, _, _, _} | _], _MFA, Info, _E) when Mod == elixir_dispatch; Mod == elixir_exp ->
prune_stacktrace([{Mod, _, _, _} | _], _MFA, Info, _E) when Mod == elixir_dispatch; Mod == elixir_expand ->
Info;
prune_stacktrace([H | T], MFA, Info, E) ->
[H | prune_stacktrace(T, MFA, Info, E)];
@@ -363,7 +363,7 @@ format_error({import, {conflict, Receiver}, Name, Arity}) ->
"please rename the local macro or remove the conflicting import",
[Name, Arity, elixir_aliases:inspect(Receiver), Name, Arity]);
format_error({import, {ambiguous, [Mod1, Mod2 | _]}, Name, Arity}) ->
io_lib:format("function ~ts/~B imported from both ~ts and ~ts, call is ambiguous",
io_lib:format("conflicting ~ts/~B import from modules ~ts and ~ts",
[Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]);
format_error({compile_env, Name, Arity}) ->
io_lib:format("Application.~s/~B is discouraged in the module body, use Application.compile_env/3 instead", [Name, Arity]);

Some files were not shown because too many files have changed in this diff Show More