Compare commits

...
885 Commits
Author SHA1 Message Date
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
Simon Hohl 2ab126e223 Add another example to Integer.parse/1 (#15101) 2026-02-12 15:18:54 +01:00
Alex NaserandAlex Naser f2e6d6a2fb Parallelize dep lock status checks during deps.loadpaths (#15099)
To make this safe, git.ex's lock_status no longer uses File.cd!
(which mutates global process state), instead passing the `:cd` into System.cmd.

Co-authored-by: Alex Naser <alex.naser@remote.com>
2026-02-09 12:49:43 +01:00
José Valim a5361436c4 Do not propagate parent config when building archives/escripts (#15097)
Closes #15094.
2026-02-07 18:47:13 +01:00
наб 0b1d3fcaf9 Improve Application.get_env/2 docs for dependencies
Followup-for: #15095
Signed-off-by: José Valim <jose.valim@gmail.com>
2026-02-06 20:42:12 +01:00
José Valim f7b179ac1d Ensure system_env is applied for Elixir deps, closes #15095 2026-02-05 23:41:12 +01:00
José Valim 426a4297c9 Fix wrong bdd leaf intersection optimization on equal literals, closes #15091 2026-02-03 14:30:29 +01:00
José Valim be34850da4 Remove incorrect differences in BDD 2026-02-02 23:02:47 +01:00
José Valim 3fbe977035 Validate Macro.Env struct upfront to avoid failures deep within the compiler, closes #15088 2026-02-01 20:49:59 +01:00
José Valim cebc5a76fb Better error message for when all patterns are exhausted 2026-02-01 08:45:06 +01:00
José Valim c40c140e3f Postpone deletion of modules to avoid code server bottleneck 2026-01-31 13:56:15 +01:00
José Valim 7fef1c58e4 Make compiler module purging opt-in
We run it before module verification but,
as module verification now loads many modules,
that was negatively impacting compilation times.
2026-01-31 13:56:15 +01:00
Mudassar 7048f60539 Fix spelling typos in docs and comments (#15083) 2026-01-30 08:22:19 +09:00
José Valim 4940d0f99e Reduce state tracked in guard info 2026-01-29 18:23:52 +01:00
Pierre Le Gall 7b22b8e7eb Add List.first!/1 and List.last!/1 (#15082) 2026-01-29 17:23:40 +01:00
Andrew France ba86bebd4c Fix "always evaluate" for Logger anonymous function tuples (#15081)
Previously, when the configuration directive `always_evaluate_messages`
was set to true an exception was raised when a function that returns a
tuple was given to Logger:

  ```
  Logger.info(fn -> {"", []} end)
  ```

Would result in:

  ```
    ** (Protocol.UndefinedError) protocol String.Chars not implemented for Tuple

    Got value:

        {"", []}
  ```
2026-01-28 09:35:08 +01:00
José Valim 5093bab3ab Document GenServer shutdown default 2026-01-28 08:54:07 +01:00
José Valim b381ab3ae4 Add require note to CHANGELOG, closes #15077 2026-01-27 19:57:16 +01:00
José Valim 30b59edf39 Track types across case clauses (#15080) 2026-01-27 19:49:08 +01:00
dependabot[bot] ff132444ca Bump actions/checkout from 6.0.1 to 6.0.2 (#15078)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.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-01-27 07:58:52 +01:00
José Valim d031425cbd Track guard precision 2026-01-26 18:44:47 +01:00
José Valim dc7b20fe0a Track pattern precision 2026-01-26 16:05:32 +01:00
José Valim 47d2b6d305 Collapse expressions to improve trace debugging 2026-01-26 14:16:21 +01:00
José Valim 663019d048 Clarify tasks are asynchronous on supervisor init 2026-01-24 19:12:28 +01:00
José Valim 73adf727f9 Do not leak variables from conditional vars, closes #15074 2026-01-23 21:10:10 +01:00
José Valim 22006fd77a Track variables across equality in guards 2026-01-23 15:56:49 +01:00
José Valim e87b9359c4 Streamline subpatterns for domain keys and list heads 2026-01-22 19:03:35 +01:00
José Valim b6a0097226 Properly track types in lists in pattern matching, closes #15040 2026-01-22 16:54:16 +01:00
José Valim cbe3d72666 Warn if default arguments are given in protocol definitions, closes #15071 2026-01-22 14:04:24 +01:00
José Valim 44abf19c97 Generate markdown docs for Elixir 2026-01-20 17:02:56 +01:00
dependabot[bot] a26c321e79 Bump azure/trusted-signing-action from 0.5.11 to 1.0.0 (#15072)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.11 to 1.0.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/1d365fec12862c4aa68fcac418143d73f0cea293...db7a3a6bd3912025c705162fb7475389f5b69ec6)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 1.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-01-20 09:00:13 +01:00
Jean Klingler 32082d1367 Add ready to copy example in test_ignore_filters doc for frequent use case (#15070) 2026-01-18 17:48:36 +09:00
Sofía Bordagorry 45734fe216 Fix minor typos in docs and comments (#15068) 2026-01-17 05:19:29 +09:00
José Valim ebfebede92 Fix typo in 'a implementation' 2026-01-16 12:56:53 +01:00
José Valim 8def030f38 Use type warning instead of typing violation, closes #15065 2026-01-16 08:37:04 +01:00
Jean Klingler 61d82a94f8 More fixes and tests for inlined struct opacity issues (#15063)
* Fix doc -> typedoc

* Add regression tests for dialyzer opacity

* Do not use opaque :sets.set in MapSet.t()
2026-01-16 11:50:34 +09:00
José Valim 1b0dff7e6a Do not warn on defaults from defoverridable, closes #15061 2026-01-15 10:16:35 +01:00
José Valim 0862491255 Add type checking for :erlang.map_get/2 to improve is_struct precision 2026-01-15 09:54:43 +01:00
José Valim 19cddfeea1 Use Map.to_list instead of Enum.to_list, closes #15064 2026-01-15 09:48:02 +01:00
José Valim b523a8e706 Simplify struct error message as it won't be deprecated 2026-01-15 09:48:02 +01:00
Jean Klingler 3b75c46513 Do not use opaque type for URI authority (#15062) 2026-01-15 00:29:52 +09:00
José Valim 3bb5f92fa9 Release v1.20.0-rc.1 2026-01-13 11:46:20 +01:00
dependabot[bot] cb472c10f3 Bump actions/checkout from 6.0.0 to 6.0.1 (#15059)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.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-01-13 08:53:37 +01:00
José Valim e5dc69398e Only perform eager literal intersection when maps are closed, closes #15045 2026-01-13 01:07:19 +01:00
José Valim fd6c6af81d Simplify bdd_difference code to discard differences of negations 2026-01-13 00:36:43 +01:00
José Valim a9a0969c28 Also optimize different between structs 2026-01-12 12:17:25 +01:00
José Valim dd284bbf9b Reduce travervals in map_all_but_one 2026-01-12 12:04:48 +01:00
José Valim bc1a862c93 Update CHANGELOG, closes #15058 2026-01-12 09:22:05 +01:00
José Valim ae383313f6 Deal with struct keys in map updates with atom domains
Closes #15055
2026-01-11 22:31:48 +01:00
José Valim f00cb3a9a0 Properly handle bitstrings inside guards, closes #15052 2026-01-11 16:52:34 +01:00
José Valim 9af054f9bc Add types for :erlang.split_binary/2 2026-01-10 22:13:34 +01:00
José Valim 61f55052fe Optimize tuple difference by checking for disjoint tagged tuples 2026-01-10 22:06:23 +01:00
José Valim d8322e3f76 Optimize bdd differences of disjoint literals (#15050)
We implement it for tuples as there is a very cheap disjoint check
2026-01-10 21:44:39 +01:00
José Valim f2ff3a7a86 More docs 2026-01-10 19:53:21 +01:00
José Valim 7f5e4e3bb2 Keep gradual mode in bitstrings 2026-01-10 19:20:37 +01:00
José Valim bfa154e70b Consider subtyping on bitstring segments 2026-01-10 19:12:19 +01:00
José Valim e586ed519e Optimize variable propagation by only visiting nodes once and avoiding immediate back propagation 2026-01-10 17:47:36 +01:00
José Valim 8767c5b530 Properly unpack map fields in descr before enum operations, closes #15049 2026-01-10 16:35:26 +01:00
José Valim 7c13e99412 Do not use opaque in MapSet, closes #14837 2026-01-10 16:15:46 +01:00
José Valim d8ed2b9ae4 Run license compliance on the same workflows as CI 2026-01-10 11:16:13 +01:00
Eksperimental d653d1522a CI: Move license compliance to its own file (#14982) 2026-01-10 11:15:36 +01:00
José Valim 1752058872 Do not crash on conditional variables inside multi-module apply
Closes #15044.
Closes #15046.
2026-01-10 11:05:22 +01:00
José Valim 0f9d919074 Release v1.20.0-rc.0 2026-01-09 19:38:19 +01:00
José Valim 45de6e5dc3 Add type inference for literal equality in guards (#15041) 2026-01-09 18:25:05 +01:00
Nevroz Arslan 15f2165903 Replace binary_size with byte_size in Protocol moduledoc (#15043) 2026-01-09 16:44:20 +01:00
José Valim 2a2be0acde Track alias defined through require usage, closes #14789 2026-01-09 12:34:55 +01:00
José Valim 583737d77d Update CHANGELOG 2026-01-09 09:28:44 +01:00
José Valim 205a1ecb4e Add IO.iodata_empty?/0 2026-01-09 09:20:06 +01:00
José Valim 9721759d35 Improve error message for mismatched patterns 2026-01-09 00:42:25 +01:00
Jean Klingler 6bace04dc9 Fix @doc for bitstring_type?/1 (#15039) 2026-01-08 07:32:28 +09:00
José Valim 4ee0241a5e Represent bitstring and binary as distinct types (#15038) 2026-01-07 12:00:25 +01:00
José Valim 1b234cadb1 Bump manifest version due to orelse/andalso 2026-01-06 11:26:42 +01:00
José Valim 99eae9536b Unify boolean checks into a single operation 2026-01-05 17:47:42 +01:00
José Valim 7fb01d3f47 Remove duplication and limit inferred size of tuples 2026-01-05 16:22:20 +01:00
José Valim 386c5d9d72 Infer types from length, map_size, tuple_size comparison checks 2026-01-05 16:11:21 +01:00
José Valim ad7559f6de Infer types from length, map_size, tuple_size equality checks 2026-01-05 14:33:58 +01:00
José Valim 98155171a2 Move apply responsibilities into Apply module
And allow custom implementation for whole functions.
2026-01-05 14:33:58 +01:00
Andrea Leopardi 5abc024203 Fix doc link in ExUnit.Formatter 2026-01-05 08:52:08 +01:00
José Valim ea59662fa0 Ensure refinements propagate across pins, clauses, and body 2026-01-05 00:41:46 +01:00
José Valim be0f3ae3e9 Propagate types from guards to matches 2026-01-04 19:00:26 +01:00
José Valim aa222426d0 Use conditional vars when applying across modules 2026-01-04 15:38:59 +01:00
José Valim caea8bcfa3 Move conditional vars into an official API 2026-01-04 15:38:15 +01:00
José Valim a4e7a2d19d Move variable deps and paths to context for reuse 2026-01-04 13:08:22 +01:00
José Valim ad67ac0a1a Mark arg in range as generated, closes #15037 2026-01-04 09:17:17 +01:00
Vinícius Müller 88cbabfd84 Skip :eio files in File.cp_r (#15035) 2026-01-03 17:06:04 +01:00
José Valim 0f67706cf9 Optimize or/and/orelse/andalso by unnesting traversals 2026-01-03 16:45:03 +01:00
José Valim 5b4ee56d7e Analyze conditional on :erlang.or/2 and :erlang.and/2 2026-01-03 16:16:09 +01:00
José Valim c35f651309 Analyze conditional orelse and when 2026-01-03 13:38:38 +01:00
José Valim f1bbb2cd32 Infer types from guards and do post-inference on stdlib (#15032)
We now infer types from guards although we don't
yet infer conditional types (such as on `or`).
2026-01-02 19:48:45 +01:00
José Valim 3884e7a91c Do not check the position in Regex errors, closes #15033 2026-01-01 10:54:27 +01:00
Remnant 94ba62178b Remove double quotes from printed strings in Enum.each example (#15031) 2025-12-31 07:49:54 +01:00
Matt Enlow a077a6e3fb count_until: ensure is_integer when raising argument error (#15030) 2025-12-31 07:48:56 +01:00
José Valim fff97fd3cc Refactor shared entry points for args, match, generators 2025-12-30 19:12:52 +01:00
Dave Lucia 53a567556a Raise ArgumentError for invalid Enum.count_until (#15029) 2025-12-30 18:34:13 +01:00
José Valim 56259933ea Do full initial traversal of args paths 2025-12-30 13:52:39 +01:00
José Valim 19c628ae23 Mark invididual guards of ||, &&, if, and unless as generated 2025-12-30 12:00:11 +01:00
José Valim 5f616e254f Remove TODOs 2025-12-30 11:29:19 +01:00
José Valim ee87c1c8a9 Keep precise variables for lists heads and map domain keys, closes #14915 2025-12-30 11:20:33 +01:00
José Valim 6d433580f3 Refactor variable tracking in patterns to prepare for guards (#15026) 2025-12-30 10:11:15 +01:00
Dave Lucia 3c8fe8606c Faise on --min-cycle-label <= 0 (#15027)
Provides an error message in favor of

```
mix xref graph --format cycles --min-cycle-label 0 --label compile-connected
** (FunctionClauseError) no function clause matching in Enum.count_until/3

    The following arguments were given to Enum.count_until/3:

        # 1
        [{"lib/foo.ex", nil}, {"lib/bar.ex", nil}]

        # 2
        #Function<4.83728021/1 in Mix.Tasks.Xref.cycle_filter_fn/1>

        # 3
        0

    Attempted function clauses (showing 1 out of 1):

        def count_until(enumerable, fun, limit) when is_integer(limit) and limit > 0

    (elixir 1.19.4) lib/enum.ex:783: Enum.count_until/3
    (mix 1.19.4) lib/mix/tasks/xref.ex:1247: anonymous fn/3 in Mi
```
2025-12-30 09:21:51 +01:00
Dave Lucia 8d8111af07 Convert (most) links to https (#15028) 2025-12-30 09:21:23 +01:00
José Valim fc747ffbe3 Update CI workflow to remove paths-ignore 2025-12-29 12:51:55 +01:00
dependabot[bot] e6f90bd55c Bump DavidAnson/markdownlint-cli2-action from 21.0.0 to 22.0.0 (#15017)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 21.0.0 to 22.0.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/30a0e04f1870d58f8d717450cc6134995f993c63...07035fd053f7be764496c0f8d8f9f41f98305101)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 22.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>
2025-12-29 12:49:13 +01:00
dependabot[bot] 7a01f5bab2 Bump actions/download-artifact from 6.0.0 to 7.0.0 (#15016)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/018cc2cf5baa6db3ef3c5f8a56943fffe632ef53...37930b1c2abaa49bbe596cd826c3c89aef350131)

---
updated-dependencies:
- dependency-name: actions/download-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>
2025-12-29 12:48:48 +01:00
dependabot[bot] f7904a4aa6 Bump actions/upload-artifact from 5.0.0 to 6.0.0 (#15015)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/330a01c490aca151604b8cf639adc76d48f6c5d4...b7c566a772e6b6bfb58ed0dc250532a479d7789f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 6.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>
2025-12-29 12:48:39 +01:00
dependabot[bot] 6948d08d51 Bump actions/checkout from 6.0.0 to 6.0.1 (#15005)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.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>
2025-12-29 12:48:24 +01:00
dependabot[bot] 0473fec367 Bump azure/trusted-signing-action from 0.5.10 to 0.5.11 (#15004)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.10 to 0.5.11.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/fc390cf8ed0f14e248a542af1d838388a47c7a7c...1d365fec12862c4aa68fcac418143d73f0cea293)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 0.5.11
  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>
2025-12-29 12:48:15 +01:00
наб 2cd8a57b37 Fix dead link to what was "The need for monitoring" but is now "Links and monitors" (#15025) 2025-12-26 11:30:14 +01:00
José Valim 07889d8c52 Indent duplicate types in Registry docs 2025-12-25 17:37:19 +01:00
José Valim 1445e8d021 Optimize eq intersection branches 2025-12-25 17:37:19 +01:00
Nevroz Arslan 1921765c78 Fix Kernel defstruct types docs phrasing (#15024) 2025-12-23 15:24:45 +01:00
José Valim 0e3d22fd79 bdd_bottom -> bdd_bot 2025-12-22 11:48:55 +01:00
José Valim f9ebeaa5a7 Make leaf intersection recursive 2025-12-22 11:36:24 +01:00
José Valim 6ce5044521 Optimize leaf intersections in differences 2025-12-22 11:11:17 +01:00
José Valim 5f60c39d66 Further simplify intersections 2025-12-21 22:54:37 +01:00
José Valim 9d8df88f80 Only compute literal intersection if C is not bdd bot 2025-12-20 23:45:54 +01:00
José Valim 743fa8ada5 Propagate BDD leaf nodes down the tree on intersections
LazyBDDs allowed us to represent all operations lazily
but the issue is that we want to propagate intersections
eagerly, as they allow us to cut down the number of nodes
in the tree. So whenever we have a leaf node, we propagate
it throughout the whole tree, cutting down the number of
nodes considerably and optimizing type checking.

Before:

    [profile] Finished group pass check of 1765 modules in 13506ms

After:

    [profile] Finished group pass check of 1765 modules in 2454ms
2025-12-20 18:31:14 +01:00
José Valim 995f7fc2c4 Optimize protocol consolidation
* Allow directory listings to be pre-computed
* Do not require struct modules to be loaded

The current implementation would require us to
recompute protocols whenever a struct changed,
something that is not currently available.
Furthermore, the fact we had to load modules
naturally made consolidation more expensive.
Therefore we need a lazy type resolution,
and until we have such feature, we will only
track struct names.
2025-12-19 18:40:15 +01:00
Gary Rennie 67431fc48a Fix typo in CHANGELOG.md about type checking maps (#15020) 2025-12-17 11:56:02 +01:00
José Valim ec267745e6 Fix arity of Map.replace!/3 2025-12-17 09:01:08 +01:00
Jeffrey LoandJosé Valim bb9721f27c More robust git version parsing (#15019)
When GIT_TRACE=1 is set in the env, `mix deps.get` fails with an
unexpected `FunctionClauseError` due to `git --version`'s output
including trace output from git itself:

```
λ GIT_TRACE=1 mix deps.get
* Getting ariel (git@github.com:foo/bar.git - main)
22:17:06.786357 git.c:476               trace: built-in: git init --quiet
22:17:06.798692 git.c:476               trace: built-in: git remote add origin git@github.com:foo/bar.git
22:17:06.804333 git.c:476               trace: built-in: git config remote.origin.url git@github.com:foo/bar.git
** (FunctionClauseError) no function clause matching in Mix.SCM.Git.parse_version/1

    The following arguments were given to Mix.SCM.Git.parse_version/1:

        # 1
        "22:17:06.809548 git.c:476               trace: built-in: git version\ngit version 2.50.1\n"

    Attempted function clauses (showing 1 out of 1):

        defp parse_version(<<"git version ", version::binary>>)

    (mix 1.18.4) lib/mix/scm/git.ex:426: Mix.SCM.Git.parse_version/1
    (mix 1.18.4) lib/mix/scm/git.ex:419: Mix.SCM.Git.git_version/0
    (mix 1.18.4) lib/mix/scm/git.ex:132: Mix.SCM.Git.checkout/2
    (elixir 1.18.4) lib/file.ex:1665: File.cd!/2
    (mix 1.18.4) lib/mix/dep/fetcher.ex:68: Mix.Dep.Fetcher.do_fetch/3
    (mix 1.18.4) lib/mix/dep/converger.ex:238: Mix.Dep.Converger.all/9
    (mix 1.18.4) lib/mix/dep/converger.ex:170: Mix.Dep.Converger.init_all/8
    (mix 1.18.4) lib/mix/dep/converger.ex:110: Mix.Dep.Converger.all/4
```

This patch updates `git_version` to handle trace noise by running
parse on the final output line of `git --version`.

New `deps.git_test` added to exercise the `GIT_TRACE` condition.

* Update lib/mix/test/mix/tasks/deps.git_test.exs

---------

Co-authored-by: José Valim <jose.valim@gmail.com>
2025-12-17 08:59:13 +01:00
José Valim 68207c0186 Remove alias bit from macro, closes #15018 2025-12-16 17:48:42 +01:00
José Valim 14bd4b5401 Streamline loader code and tests 2025-12-16 11:22:00 +01:00
Krishnadas PC 0f00cb0113 Warn when project app name matches a dependency (#15013) 2025-12-16 11:15:34 +01:00
José Valim e622a26420 Properly preserve function dynamic type on application 2025-12-15 16:26:43 +01:00
José Valim 72435c44a5 Add text to pure text blocks 2025-12-15 13:35:50 +01:00
José Valim 743fea7a11 Update CHANGELOG 2025-12-15 13:11:28 +01:00
José Valim 67baf686eb Properly propagate if_set on Map operations 2025-12-15 13:11:17 +01:00
José Valim 297bd84ac2 Type Map.update/4 2025-12-15 11:26:44 +01:00
José Valim e609d29bc6 Type Map.pop_lazy/3 2025-12-15 11:18:00 +01:00
José Valim 0f3fdcd84f Type Map.put_new/3 and Map.put_new_lazy/3 2025-12-15 11:03:52 +01:00
José Valim 2d327241de Type Map.replace/3 and Map.replace_lazy/3 2025-12-15 10:38:51 +01:00
José Valim f58e02adf1 Type check Map.get/2, Map.get/3, Map.get_lazy/3 2025-12-15 09:51:22 +01:00
José Valim 5c586dae68 Type Map.update!/3 2025-12-14 22:50:31 +01:00
Jean Klingler af3624c823 Mention how zip_with/2 could be use as a transpose function (#15012) 2025-12-14 19:11:03 +09:00
José Valim bd909cb658 Simplify message ordering assertions 2025-12-14 10:54:49 +01:00
José Valim aee1747c5d Type check Map.pop!/2, Map.pop/2, and Map.pop/3 2025-12-14 10:48:46 +01:00
José Valim fabbf5e9c9 Do not store not_set() on closed maps 2025-12-14 10:48:46 +01:00
Rodolfo Carvalho 0435602337 Support filtering mix deps output (#15009)
Makes it easier to report versions of specific dependencies by allowing
users (or tools) to pass dependency names as arguments to `mix deps`.

Warns when a dependency is not found, similar to `mix deps.unlock`.

Proposal: https://groups.google.com/g/elixir-lang-core/c/5tlLZ1yu4rQ/m/g7Z8fNWiBwAJ
2025-12-14 10:48:35 +01:00
José Valim b60e424204 Typecheck Map.from_struct/1 2025-12-12 13:29:37 +01:00
José Valim 70e23a407e Test inferred types for Map.new/0 and Map.equal?/2 2025-12-12 12:54:36 +01:00
José Valim a9aac5216f Type check Map.put/3 and Map.delete/2 2025-12-12 12:44:51 +01:00
José Valim d831a00063 Unify clauses by extracting location and op 2025-12-12 12:20:21 +01:00
Łukasz Samson a09fa1075a Fix parser crash on missing parentheses on expression following operator \"not in\" warning (#15011) 2025-12-12 12:14:10 +01:00
José Valim 0b3dc130cd Avoid warnings in suite that are now caught by the type system 2025-12-12 12:12:16 +01:00
José Valim 62d3e61e89 Improvements to Descr.map_update to support put operations 2025-12-12 11:54:33 +01:00
José Valim de32dff024 Convert maps to list before traversing in descr 2025-12-11 15:06:23 +01:00
José Valim a6ba0cb329 Type check Map.replace!/3 2025-12-11 13:24:25 +01:00
José Valim 18c81d66e2 Add type checking for Map.fetch/2 and improve docs 2025-12-11 12:58:22 +01:00
José Valim 7fbf48ccc7 Type check Map.fetch!/2 and :maps.take/2 2025-12-10 17:42:07 +01:00
José Valim 77ef258ee6 Update copyrights 2025-12-10 15:50:19 +01:00
José Valim ab843d0342 Add type checking for Map.from_keys/2 2025-12-10 15:39:26 +01:00
José Valim 79c28420d4 Convert list_proper? into list_of 2025-12-10 14:09:35 +01:00
José Valim 0e3465d1fc Improvements to list_proper? 2025-12-10 12:11:42 +01:00
Guillaume Duboc ee75b22a0c Enhance list type handling and add list_proper? function (#15002)
- Refactor list_bdd_to_pos_dnf to improve handling of negations and last types.
- Introduce list_proper? function to check if a list type is a proper list.
- Update list_hd and list_tl to fix wrong cases.
- Simplify list_bdd_to_pos_dnf by removing unnecessary checks.
2025-12-10 11:31:43 +01:00
José Valim 1110a9511f Fix test assertion 2025-12-10 09:39:41 +01:00
José Valim 5f608c4ad3 Handle term() in optional_static? 2025-12-10 09:21:21 +01:00
Jechol Lee c80a3b1a0e Fix unnecessary recompilation when dbg_callback is modified at runtime (#15007)
When :elixir app starts, store the dbg_callback value in dbg_callback_initial.
Mix compiler now compares against dbg_callback_initial instead of dbg_callback.

This prevents unnecessary recompilation when tools like Kino modify
dbg_callback at runtime. Previously, such runtime modifications would trigger
a full recompilation of all files using dbg/2, even though the config hadn't
actually changed.

The key insight is that dbg/2 is a compile-time macro, so runtime modifications
to dbg_callback don't affect already-compiled code. Only actual config changes
(reflected in dbg_callback_initial) should trigger recompilation.

This is a more general solution than detecting specific wrapping patterns,
as it works with any tool that modifies dbg_callback at runtime.
2025-12-10 09:21:08 +01:00
Andrea Leopardi b79b08d819 Fix spec for t:ExUnit.Callbacks.child_spec_overrides/2 (#15006) 2025-12-09 08:14:05 -05:00
José Valim 34004a2b90 Preserve gradual mode on tuple and map operations 2025-12-08 11:11:50 +01:00
José Valim 09fc63d475 Remove uneeded newline 2025-12-08 09:43:52 +01:00
José Valim e1ff7819b8 Fix deprecation warning on !left in right, closes #15001 2025-12-08 09:36:23 +01:00
José Valim 82983cf979 Remove improper_list from type specification 2025-12-08 09:36:23 +01:00
Daniel Kukula 904adda55f Calculate date_from_iso_days using neri_schneider algorithm (#14999) 2025-12-07 09:51:25 +01:00
Jonathan Toye 8fc181fb27 fix grammar in restart strategies section of tasks-and-gen-tcp (#15000) 2025-12-07 09:24:32 +01:00
Jonathan Toye d30b22f5d4 Fix minor typo in mix-and-otp docs (#14998) 2025-12-06 21:53:48 +01:00
José Valim 030660aaf7 Type Map.to_list/1, Map.keys/1, and Map.values/1 2025-12-05 13:32:01 +01:00
José Valim fc07f452b3 Improve docs on set-theoretic syntax reference 2025-12-04 12:11:06 +01:00
José Valim 4c10b43e3d Do not crash on invalid metadata in deprecated backends, closes #14995 2025-12-04 11:57:49 +01:00
José Valim 08b559fe96 Pass the caller module to unique_var to avoid confusion 2025-12-04 10:55:30 +01:00
Jean Klingler 1cadefffc7 Revert Stream transform simplification causing regression (#14993)
* Revert "Simplify transform as suspend is guaranteed to return one entry"

This reverts commit a7c008f2cd.

* Add regression test case
2025-12-04 18:05:29 +09:00
José Valim ac44e723ba Keep collecting buffer in IEx parser 2025-12-04 00:28:51 +01:00
José Valim 2ac361a4a4 Fix #iex:break as part of multi-line prompts, closes #14992 2025-12-04 00:07:26 +01:00
José Valim 509e715f60 Support fetching abstract code for modules compiled with Elixir v1.14 and earlier, closes #14987 2025-12-03 20:00:29 +01:00
José Valim a6951d0448 Document requirements without operator, closes #14988 2025-12-03 19:52:17 +01:00
phil 3c5e968a54 doc: observer example create_bucket (#14990) 2025-12-03 19:45:13 +01:00
Eksperimental cad69f790f CI: Fix continue-on-error (#14991)
If a test fails, `continue-on-error` will not be evaluated, if it's not set.

https://github.com/eksperimental-forks/elixir/actions/runs/19824449905/job/56794214695#step:9:46

    2196 doctests, 4913 tests, 1 failure (18 excluded)
    make: *** [Makefile:289: test_stdlib] Error 2
    Error: Process completed with exit code 2.
    Error: The step failed and an error occurred when attempting to determine whether to continue on error.
    Error: The template is not valid. .github/workflows/ci.yml (Line: 78, Col: 28): Unexpected value ''
2025-12-03 19:35:26 +01:00
Guillaume Duboc 1aa188f115 Add map_to_list 2025-12-03 14:17:38 +01:00
José Valim 41f9bcf621 Do not bake nil assumption into map_get/2 2025-12-03 11:29:26 +01:00
José Valim 2b63546b19 Generalize map_update so it can power delete, pop, replace, and more 2025-12-02 21:16:17 +01:00
José Valim a13f38edbe Implement map_put_key using new domain aware map_put 2025-12-02 15:57:16 +01:00
José Valim 31c9c84ebe Improve coverage for map_update 2025-12-02 12:26:43 +01:00
José Valim 284b190bb0 Clean up CI and unify coverage and docs build (#14986) 2025-12-02 12:23:59 +01:00
Eksperimental b58cdd823c CI: Minor improvements to release.yml (#14983) 2025-12-02 11:02:01 +01:00
Eksperimental e66a799b2f CI: Minor improvements to release_pre_built/action.yml (#14984) 2025-12-02 11:01:18 +01:00
Eksperimental bd4c232a28 CI: Minor improvements to POSIX compliance action (#14980) 2025-12-02 11:00:43 +01:00
Eksperimental 521b16ce21 CI: Minor improvements to Markdown action (#14979) 2025-12-02 11:00:08 +01:00
Eksperimental 3172f7487d CI: Minor improvements to Release Notifications action (#14981) 2025-12-02 10:57:28 +01:00
Daniel Kukula 6b519ae782 Optimize Calendar.ISO.date_from_iso_days (#14976) 2025-12-02 00:21:45 +01:00
Guillaume Duboc 6710d7e197 Unify :empty_list and :list domains in maps 2025-12-01 18:16:52 +01:00
Gonzalo R 7ad99b5590 Fix starter/name in Task's report callback (#14975) 2025-12-01 17:46:41 +01:00
José Valim ff2b73861d Track domain keys when creating and updating maps (using the map syntax) (#14892) 2025-12-01 17:43:35 +01:00
José Valim e2c4c07b9a Document ternary operators, closes #14959 (#14964) 2025-12-01 10:49:05 +01:00
Eksperimental 2acf5804d3 CI: Update Ubuntu to latest LTS version (#14974) 2025-11-29 18:06:06 +01:00
Eksperimental 1da7feb387 CI: Minor improvements/fixes (#14973)
- Put together name and runs-on to avoid getting outdated on update
- Fix name for Windows job with correct version
- Sort Erlang/OTP versions descendingly so they align with order under Linux
2025-11-29 17:28:01 +01:00
Michał Łępicki 31ec8cc703 Fix dialyzer warnings on EEx.eval_string (#14971) 2025-11-29 12:10:30 +01:00
Eksperimental abd73a841f Tighten up Markdownlint rule "Trailing punctuation in heading" (#14966)
Rule MD026 "Trailing punctuation in heading" is enforced,
we simply remove `!` from the list of forbidden characters.

Source: https://github.com/DavidAnson/markdownlint/blob/main/doc/md026.md
2025-11-28 17:26:54 +01:00
Jonatan Männchen 0b4d0f360d Fix Plataformatec Copyright (#14970) 2025-11-28 10:35:10 +01:00
Eksperimental 10bc090ac1 CI: Move POSIX compliance action to its own file (#14969) 2025-11-28 10:11:53 +01:00
Eksperimental 68ebb78d66 Correct SPDX-FileCopyrightText (#14968)
This file was originally created on 2020-06-26,
therefore the copyright belong to Plataformatec at the time.
2025-11-28 10:11:21 +01:00
Eksperimental 66165e6b0f Enable Markdown rule: Multiple headings with the same content (#14967)
We allow duplication for headings with different parents in rule MD024.
2025-11-28 10:10:04 +01:00
José Valim bf2298e050 Use existing Sample project 2025-11-27 15:35:50 +01:00
Wojtek Mach 75c5aa0a6b mix help app:APP: compile current Mix project (if any) (#14958)
Prior to this patch, it was impossible to get help for the current app:

    nimble_csv$ mix help app:nimble_csv
    Application nimble_csv does not exist or is not loaded

It also helps with projects with depenendencies when they haven't been
compiled yet:

    req$ mix deps.get && mix help app:finch # mix deps.compile haven't been run yet
    Application finch does not exist or is not loaded

Now it works:

    nimble_csv$ mix help app:nimble_csv
    Compiling 1 file (.ex)
    Generated nimble_csv app
    # NimbleCSV

    NimbleCSV is a small and fast parsing and dumping library.

    # NimbleCSV.RFC4180

    A CSV parser that uses comma as separator and double-quotes as escape according
    to RFC4180.

    # NimbleCSV.Spreadsheet

    A parser with spreadsheet friendly settings.
2025-11-27 15:34:20 +01:00
José Valim 19dcd95a36 Clarify @on_load will be called early on releases, closes #14960 2025-11-27 15:26:13 +01:00
dependabot[bot] da1481e433 Bump actions/checkout from 5.0.1 to 6.0.0 (#14956)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.1 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/93cb6efe18208431cddfb8368fd83d5badbf9bfd...1af3b93b6815bc44a9784bd300feb67ff0d1eeb3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.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>
2025-11-27 10:49:38 +01:00
José Valim c4c5c060d6 Clarify struct and record fields, closes #14963 2025-11-26 19:53:32 +01:00
Oskar Köök 1b215b7a36 Fix unregister_name signature for PartitionSupervisor (#14961) 2025-11-26 11:28:47 +01:00
José Valim 81ef21272d Include hex in the list of escript.build special cases (#14953) 2025-11-24 15:27:50 +01:00
José Valim 7da1b76b6b Ensure line and column are included in empty blocks, closes #14931 2025-11-22 18:29:39 -08:00
Eksperimental f52f395fcf Fix typos (#14951) 2025-11-22 10:47:35 -08:00
Eksperimental e55388d6e6 Do not crash when building docs from sources with no Git repo (#14950)
Closes https://github.com/elixir-lang/elixir/issues/14943
2025-11-22 10:47:11 -08:00
Eksperimental 912f3d546a Update .gitnore rules for generated .zip files (#14947)
`make Docs.zip` and `make Precompiled.zip` generates names with those files names.
2025-11-22 06:25:38 -08:00
Eksperimental 7eea067534 Add .tool-versions to .gitignore (#14948) 2025-11-22 04:36:10 -08:00
Eksperimental dc3986833e Update commands for building docs in CONTRIBUTING.md (#14945)
- `$DOCS_FORMAT` is no longer supported in Makefile
- By default HTML and EPUB doc formats are created
2025-11-21 16:57:41 -08:00
Eksperimental a2f21c91a4 CI: Improve trigger files on Markdown action (#14941)
This will trigger the action when the markdownlint-cli2 config or
the action yaml file are modified.
2025-11-21 16:44:48 -08:00
Eksperimental 17e244c0a5 Improve list format in Markdownfiles (#14944)
Related PR: https://github.com/elixir-lang/elixir/pull/14937
2025-11-21 16:44:26 -08:00
Eksperimental 2219d74c38 CI: Lint all Markdown files in the project (#14937)
Adds globs and ignore rules.

This allows as to lint Markdown files locally by running as simple as:

    $ markdownlint-cli2

or automatically fix issues with

   $ markdownlint-cli2 --fix
2025-11-21 06:52:23 -08:00
Łukasz Samson 0055f2fe53 Fix ex_unit formatter crash when find_diff returns nil (#14939) 2025-11-21 05:33:48 -08:00
José Valim 522aebdf74 Update markdownlint for tables 2025-11-20 11:01:44 -08:00
Eksperimental 1fa63ffe7a Refine Unicode Standard Annex #31 reference link and terminology (#14935) 2025-11-20 10:25:55 -08:00
José Valim 16862cef79 Clarify the scope of open source license (#14934) 2025-11-20 10:13:31 -08:00
Jean Klingler ae752c7bb0 Fix dialyzer warning in Code.format_string!/2 (#14928)
* Fix dialyzer warning in Code.format_string!/2

Tentative fix for https://github.com/elixir-lang/elixir/issues/14927

* Add regression test for IO.inspect/2 opts
2025-11-20 19:39:55 +09:00
Jean Klingler f9745a25cf File.cp_r reports non-existing dest dir properly (#14929) 2025-11-20 19:35:54 +09:00
José Valim 1b92f3ac87 Bump checker version 2025-11-19 10:43:50 -08:00
chops 9ec2eb41ae Fix grammar in mix run --no-start documentation (#14925) 2025-11-19 04:11:56 -08:00
José Valim 47aab3b37c Ensure parallel matches in assert propagate type information, closes #14921 2025-11-18 15:19:57 -08:00
José Valim 7ef4a3be97 Ensure logger formatter works with chardata, closes #14921 2025-11-18 14:50:46 -08:00
Guillaume Duboc d56546d81e Change function representation (#14923) 2025-11-18 14:04:21 -08:00
dependabot[bot] e56b730c8c Bump DavidAnson/markdownlint-cli2-action from 20.0.0 to 21.0.0 (#14920)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 20.0.0 to 21.0.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/992badcdf24e3b8eb7e87ff9287fe931bcb00c6e...30a0e04f1870d58f8d717450cc6134995f993c63)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 21.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>
2025-11-17 13:54:01 -08:00
dependabot[bot] 98b35eaa53 Bump actions/checkout from 5.0.0 to 5.0.1 (#14919)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...93cb6efe18208431cddfb8368fd83d5badbf9bfd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.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>
2025-11-17 13:52:53 -08:00
Austin Ziegler 5732283ada Add :formatter to ExUnit.CaptureLog options (#14914)
Mostly modified `:log_capture_on` handler for ExUnit.CaptureServer to
respect a `:formatter` option instead `Logger.default_formatter/1`.

Updates were made to documentation and tests to reflect this change.
2025-11-17 12:34:31 -08:00
José Valim 46807cfcdf Clarify calendar add always converts to microseconds 2025-11-17 08:29:48 -08:00
Michał Łępicki 7989e87716 Update subscribe spec to include error tuple (#14916)
see https://github.com/elixir-lang/elixir/commit/ef52e139381fbe87e66e48b64bd4684e3089b83d
2025-11-16 08:29:13 -08:00
José Valim ef52e13938 Improve error messages around Mix TCP failures, closes #14912 2025-11-15 10:32:35 -08:00
José Valim 8fb1ddf060 Update CHANGELOG 2025-11-15 06:50:46 -08:00
Vincent Robinson 51e25fbf4a Add Integer.ceil_div function (#14913) 2025-11-15 06:35:25 -08:00
Jean Klingler cf454cd1f1 Add Regex.import/1 (#14910) 2025-11-15 06:18:33 -08:00
José Valim f12ff1d7db Add --min-cycle-label to mix xref graph 2025-11-15 06:14:14 -08:00
José Valim c7a9d8544a Skip warning on :re.import/1 2025-11-15 05:00:58 -08:00
Jack Smith c80fc6da1a Fix grammatical error in getting started docs (#14911) 2025-11-14 12:26:14 +09:00
Jean Klingler cbb508600c Fix since in doc for E regex modifier (#14909) 2025-11-14 07:48:01 +09:00
José Valim f248994830 Share consulting logic between compile.app and release 2025-11-13 16:09:55 +01:00
Jean Klingler b45853f043 Add 'E' modifier to Regex for :export option (#14907) 2025-11-13 21:37:25 +09:00
José Valim 6f26bb0b05 Simplify format stacktrace handling 2025-11-13 13:10:44 +01:00
Łukasz Samson ee469e56e6 Fix crash when format_kind_reason is executed with TestModule (#14901)
Closes #14900.
2025-11-13 13:05:09 +01:00
Jean Klingler 0126f1f1a3 Write ascii printable charlists as such in .app file (#14906) 2025-11-13 18:42:24 +09:00
Luca Scalet edb9431df6 Add command needed for :observer.start() within iex -S mix (#14904) 2025-11-12 15:19:15 +01:00
José Valim 71a8f53521 Add :stop_unused_applications to Mix.install/2 2025-11-12 12:04:06 +01:00
José Valim 541f641a92 Allow Mix.Shell.IO to work with stderr.
This reverts "pass the shell forward on deps.partition"
as there is no guarantee custom shells can be loaded
at that stage.
2025-11-12 12:03:34 +01:00
José Valim c0933fbbac Pass the shell forward on deps.partition
Closes #14902.
2025-11-12 10:41:52 +01:00
José Valim 06619c2fcc Document how to implement Enumerable for lists, closes #14899 2025-11-10 20:09:03 +01:00
Wojtek Mach a0eb603091 mix help app:APP: Always try loading apps and support Erlang apps (#14894) 2025-11-08 12:22:34 +01:00
José Valim 0df110c9c0 &Mod.fun/arity are valid app terms, closes #14891 2025-11-08 12:08:00 +01:00
José Valim 22635e6bda Allow forcing specific compilers 2025-11-08 11:39:30 +01:00
José Valim 4a8d4f29ae Warn if non-protocol functions are defined in protocol modules
Previous approaches tried removing macros but they could always
be added back. Therefore we now check for non-protocol functions
in a before compile callback.
2025-11-04 12:31:53 +01:00
Jean Klingler 6460c642c3 Fix how UTF8 env is stored in .app file (#14890) 2025-11-04 16:20:36 +09:00
José Valim 31905ca136 Ensure relative MIX_BUILD_PATH is discarded on deps
Closes #14875.
2025-11-02 10:17:50 +01:00
José Valim 3c1514aef6 Ensure mix escript.install works with deps.partition, closes #14885 2025-11-02 09:50:43 +01:00
Ahmed Elawad 6402967c4b Remove extra indentation (#14886) 2025-11-01 16:24:08 +01:00
Eksperimental 757ab8c456 Minor improvements in deprecation table (#14881) 2025-10-30 22:06:07 +01:00
José Valim 0fac5d5f4a Remove line breaks from deprecations table 2025-10-30 21:59:45 +01:00
Gonzalo R 21450cf838 docs(logger): :process_label is not really ever included in the metadata as of now (#14880) 2025-10-30 17:47:42 +01:00
Eksperimental c18f53ed93 Update apps in Mix.Tasks.New.reserved_application_names/0 (#14876)
- Hipe was removed in OTP 24
- Added `:tftp`
2025-10-30 16:56:04 +01:00
Leslieandwolfgang 9a1c722737 Fix struct update example in guide (#14877)
Co-authored-by: wolfgang <wolfgang.yin@gmail.com>
2025-10-30 22:52:31 +09:00
Eksperimental 8a513afc60 Fix typo in test dependency (#14878) 2025-10-30 22:48:58 +09:00
José Valim 01d9c505bb Commit test changes to MIX_BUILD_PATH 2025-10-30 08:29:24 +01:00
José Valim 5ca2c15c75 Ensure MIX_BUILD_PATH is expanded 2025-10-30 08:19:05 +01:00
Jean Klingler 3a290c30c1 Document since for --no-check-cwd + fix typo (#14874) 2025-10-30 07:37:37 +09:00
José Valim 7fa1914e88 Store the lock in the manifest
This is to address an issue where a _build restored
for another version of a dependency would come up as
valid.
2025-10-29 14:35:29 +01:00
Jean Klingler aef3dcecd1 Warn on deprecated ~~~ unary op in tokenizer (#14870) 2025-10-29 19:23:31 +09:00
Raúl R Pearson 25771dd347 Remove duplicates in Keyword.get_and_update/3 (#14868)
This commit also tweaks some variable names so that get_and_update/3 and
get_and_update!/3 implementations match a bit closer.
2025-10-29 07:44:54 +01:00
José Valim e309da46cb Ensure dependencies are marked as fetched before writes to avoid races 2025-10-28 20:53:13 +01:00
José Valim e5ace9bba6 Rely on term_to_binary/1 format to protect from incomplete file writes 2025-10-28 20:26:28 +01:00
José Valim 1a2c0a24e7 Add checksum to manifest files
Closes #14866.
2025-10-28 11:59:09 +01:00
José Valim bd3d68cccc Improve reliability of compiler cache
1. Check if timestamps mismatch, instead of ordering

2. Ensure manifests are removed before .beam files on clean
2025-10-28 11:57:31 +01:00
José Valim 668fb69a47 Add --no-check-pwd to mix compile 2025-10-28 11:03:41 +01:00
José Valim bb262d8302 Raise if elixirc_paths is not a list of string paths, closes #14862 2025-10-28 08:25:27 +01:00
Jean Klingler bd3b2f6e00 Fix dialyzer warning on IO.inspect :label (#14865) 2025-10-28 16:06:08 +09:00
dependabot[bot] 58c45612dc Bump actions/download-artifact from 5.0.0 to 6.0.0 (#14864)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/634f93cb2916e3fdff6788551b99b062d0335ce0...018cc2cf5baa6db3ef3c5f8a56943fffe632ef53)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 6.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>
2025-10-28 08:05:00 +01:00
dependabot[bot] e9370bb023 Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#14863)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 5.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>
2025-10-28 08:04:35 +01:00
José Valim f175f5f3fa Ensure flags are passed down to umbrella children
Closes #14852.
Closes #14860.
2025-10-26 13:25:28 +01:00
José Valim a7258fe26d Ensure --no-optional-deps skips them even after they are compiled, closes #14856 2025-10-26 10:57:01 +01:00
José Valim a8000f211a Raise on invalid .app files (#14861)
Since regular expressions now have regexes,
they can end-up in .app files, and then apps
can no longer boot.

This changes makes it so they fail loudly.
2025-10-24 23:55:50 +02:00
Jérémy Aubé 5bcfbd38be Fix typo (#14859) 2025-10-24 20:42:02 +02:00
Eksperimental 720d77361c Fix help message for local archive/escript tasks (#14857)
Follow up to #14853.
2025-10-24 17:59:41 +02:00
Eksperimental cf7d329915 Improve DevEx when Mix commands receive no arguments (#14853)
Some mix commands fail when no argument is given.

Before this PR:

```
$ mix archive.uninstall
** (Mix) No argument was given to uninstall command

$ mix cmd
** (Mix) Expected at least one argument in mix cmd

$ mix escript.build
** (Mix) Could not generate escript, please set :main_module in your project configuration (under :escript option) to a module that implements main/1

$ mix escript.install
** (Mix) Could not generate escript, please set :main_module in your project configuration (under :escript option) to a module that implements main/1

$ mix escript.uninstall
** (Mix) No argument was given to uninstall command

$ mix eval
** (Mix) "mix eval" expects a single string to evaluate as argument

$ mix new
** (Mix) Expected PATH to be given, please use "mix new PATH"

$ mix xref
** (Mix) xref doesn't support this command. For more information run "mix help xref"
```

With this PR:

```
$ mix archive.uninstall
** (Mix) No argument was given to uninstall command.  Use "mix archive.uninstall PATH" or run "mix help archive.uninstall" for more information

$ mix cmd
** (Mix) No argument was given to mix cmd. Run "mix help cmd" for more information

$ mix escript.build
** (Mix) Could not generate escript, please set :main_module in your project configuration (under :escript option) to a module that implements main/1. Run "mix help escript.build" for more information

$ mix escript.install
** (Mix) Could not generate escript, please set :main_module in your project configuration (under :escript option) to a module that implements main/1. Run "mix help escript.build" for more information

$ mix escript.uninstall
** (Mix) No argument was given to uninstall command.  Use "mix archive.uninstall PATH" or run "mix help archive.uninstall" for more information

$ mix eval
** (Mix) "mix eval" expects a single string to evaluate as argument. Run "mix help eval" for more information

$ mix new
** (Mix) Expected PATH to be given. Use "mix new PATH" or run "mix help new" for more information

$ mix xref
** (Mix) No argument was given to xref command. Run "mix help xref" for more information
```
2025-10-24 13:23:45 +02:00
José Valim 7eb5b3ae17 Reset ansi escapes before newlines in Logger
Closes #14855
Closes #14841
2025-10-24 12:19:14 +02:00
hictamenvivit 7a1dde6034 Fix typo on task-and-gen-tcp.md (#14854) 2025-10-24 07:50:15 +02:00
José Valim 260149951e Configure search engines in latest ExDoc 2025-10-23 21:00:49 +02:00
José Valim dc6647c0e9 Pass signatures on the side instead of module map 2025-10-23 00:18:14 +02:00
José Valim d18088a3b4 Do not include signatures twice in beam files 2025-10-22 23:58:33 +02:00
José Valim 22b47d7f48 Measure and optimize writing of .beam files in the compiler
This should provide meaningful benefits when working with
containers and network mounted drives.
2025-10-22 10:47:00 +02:00
dependabot[bot] e1214a64c9 Bump azure/trusted-signing-action from 0.5.9 to 0.5.10 (#14848)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.9 to 0.5.10.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/bb15ca63eb5548cc306f4f335c5617bb414abcad...fc390cf8ed0f14e248a542af1d838388a47c7a7c)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 0.5.10
  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>
2025-10-21 12:59:34 +02:00
José Valim 5d29d61890 Simplify always true and always false bdd nodes 2025-10-21 12:51:30 +02:00
José Valim faa8d4722e Make bdd functions public to aid debugging 2025-10-21 12:51:30 +02:00
Yinameah 809b035dcc Fix typo in gen-tcp (Guide) (#14845) 2025-10-18 21:39:08 +02:00
Travis Vander Hoop 50a6f0f5e6 Update documentation for formatter's :excludes option (#14842) 2025-10-18 20:35:00 +02:00
José Valim e1772ef73a Do not spawn partitions when all dependencies are local and ok, closes #14843 2025-10-18 19:03:46 +02:00
José Valim 4e867b3775 Simplify computation of mismatched parts for error messages
Before we were doing:

    common = intersection(actual, expected)
    uncommon = difference(actual, common)

But the second clause is:

    actual and not (actual and expected)

Which is literally the same as:

    actual and not expected

But much faster as it avoids the large nesting of BDDs.

Closes #14836.
2025-10-18 18:49:58 +02:00
José Valim 3992ad4ade Improve protocol violation warnings 2025-10-18 18:31:35 +02:00
José Valim d11a84dfd6 Do not attempt to touch deleted files 2025-10-18 12:03:54 +02:00
José Valim 9cc24c9d92 Perform expensive operation once 2025-10-18 10:48:15 +02:00
José Valim 43aeded377 Do not escape dbg options, closes #14839 2025-10-17 22:15:14 +02:00
Eric Meadows-Jönsson c265744b9c Fix hex upload of Elixir build without -otp- suffix (#14840)
OTP 25 is no longer supported so we didn't upload a generic build.
Instead find the oldest version instead of hardcoding it.
2025-10-17 22:13:34 +02:00
Mike Stok 77f30d6321 Fix typo in README.md (peak → peek) (#14838)
It seems like the intended meaning here is to have a quick look.
2025-10-17 13:36:19 +02:00
José Valim d596853211 More optimizations for differences
* when a1 < a2
* when a1 == a2 and c2 == bottom and d2 == bottom
* when a1 == a2 and u2 == bottom
2025-10-16 23:49:25 +02:00
José Valim 640275b8ba Improve protocol type error to list possible root causes 2025-10-16 23:49:25 +02:00
Daniil Kulchenko 53fb96c808 Fix EEx.compile_string passing invalid options to tokenize (#14835)
EEx.compile_string/2 was passing all options to tokenize/2, but
tokenize/2 only accepts tokenize_opt (:file, :line, :column,
:indentation, :trim). This caused dialyzer to correctly flag calls
with :engine or :parser_options as type errors in Elixir 1.19+.

The fix filters options before passing to tokenize/2, keeping only
the valid tokenize_opt keys, while still passing the full options
list to EEx.Compiler.compile/3 which needs :engine, :parser_options,
and custom engine options.

Fixes #14834
2025-10-16 23:48:33 +02:00
José Valim 504c680870 Fix IEx parser fetching on mix release 2025-10-16 08:46:24 +02:00
José Valim f4aa384d1b Escape meta within existing quote extensions (#14832)
Closes #14829
Closes #14830
2025-10-14 13:07:05 +02:00
José Valim 7612567830 Add --shell to mix cmd (#14827) 2025-10-09 10:06:55 +02:00
Eksperimental b727a502d6 Correct SPDX summary for Elixir projects (#14824) 2025-10-09 08:48:34 +02:00
Eksperimental 33467dad7b Fix typos (#14825) 2025-10-09 04:51:26 +09:00
José Valim a4bbb96d7a Include a hint for defimpl type checking 2025-10-08 18:26:36 +02:00
José Valim 0f597b7327 Update parallel compiler docs, closes #14821 2025-10-07 17:54:00 +02:00
José Valim b2c1c3958b Add mix help app:APP, closes #14782 2025-10-07 17:54:00 +02:00
Eksperimental d792fe55f7 Improve Kernel.ParallelCompiler warning (#14820) 2025-10-07 17:50:03 +02:00
José Valim 06ef46ceb5 Add newline after inspection, closes #14819 2025-10-07 17:15:39 +02:00
Eksperimental da20a70810 Elixir v1.19 introduces a warning related to structs, (#14818)
hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of:

         user = some_fun()
         %User{user | name: "John Doe"}

     it is enough to write:

         %User{} = user = some_fun()
         %{user | name: "John Doe"}

Since this could be seen by new-comers to the language, offering a better user experience by avoiding abbreviations. Favoring the usage of "some_function" instead of "some_fun"
2025-10-07 15:59:08 +02:00
José Valim e07f0117bd Improve error message when escaping default values with custom rules in structs, closes #14817 2025-10-07 14:26:02 +02:00
José Valim 36b0a69d3c Ensure escaping works within struct fields, closes #14817 2025-10-07 13:07:18 +02:00
José Valim 977efeac99 Do not crash on empty test unit groups, closes #14754 2025-10-07 11:12:33 +02:00
Eksperimental e7c121609e Grammar correction: to warn of (#14816)
Use "to warn of something" instead of "to warn for something".

Some instances of "to warn on" and "to warn about" have been replaced.
2025-10-06 19:05:04 +02:00
Rafał Studnicki fecb22116c Add key-based partitioning to duplicate registries (#14654) 2025-10-06 18:58:03 +02:00
José Valim e4d0a0d252 Fix optimizations for closed map checking (#14813) 2025-10-06 15:39:26 +02:00
Eksperimental 92a28606ac Fix redundant warnings when implementing a callback of undefined behaviour (#14814) 2025-10-06 15:26:42 +02:00
Wojtek Mach e3cd399505 Fix preloading modules in mix test --slowest-modules=N (#14811) 2025-10-05 21:46:22 +02:00
José Valim 96fe37e587 Update release notes 2025-10-05 15:46:45 +02:00
José Valim 874a390881 Update CHANGELOG 2025-10-05 15:39:06 +02:00
José Valim 781d500246 Checkpoint before verification to avoid ignore modules warnings 2025-10-05 15:36:13 +02:00
José Valim aec930cd21 Use a map to track Mix compiler state 2025-10-05 15:36:12 +02:00
José Valim c6d3e48474 Fix regression on direct raise of ExUnit.AssertionError 2025-10-04 21:40:52 +02:00
José Valim 7afabc99cb Store bdd leaves in a unified and compact format (#14807) 2025-10-04 13:12:15 +02:00
José Valim 3ccc915e66 Improve docs for test patterns 2025-10-04 13:10:14 +02:00
Jean Klingler b4cab3641a Fix inaccurate hint for disabling :test_ignore_filters (#14808) 2025-10-04 13:06:23 +02:00
José Valim 20564ba6d9 Use lazy bdd for all types (#14806)
We use Lazy BDDS: ternary trees (instead of binary) where the additional node
encodes a lazy union, as in "COVARIANCE AND CONTRAVARIANCE:
A FRESH LOOK AT AN OLD ISSUE", with some additional optimisations
for intersections and differences to avoid materialising unions.
2025-10-02 19:30:35 +02:00
José Valim f6c62669cd Fix :file.pwrite return type 2025-10-01 14:44:34 +02:00
José Valim 184c7717f6 Include previous clause line on default errors/warnings, closes #14804 2025-10-01 08:43:16 +02:00
José Valim 2b57a97d0a Revert "Do not allow protocols to define structs nor exceptions, closes #14158"
Projects rely on this feature, therefore we have to revert
to avoid breaking changes.

This reverts commit 01c82022f2.

Closes #14803.
2025-10-01 08:20:11 +02:00
Jonatan Kłosko 26612e59c8 Improve user detection in lock and pubsub implementation (#14801) 2025-09-30 16:46:05 +02:00
Gary Rennie f646c89315 Add mix test examples for filtering with --only and --exclude (#14802)
These are documented in the ExUnit.Case docs, but documenting them here
makes sense too as mix is usually used to invoke the tests.
2025-09-30 16:45:31 +02:00
Jonatan Kłosko 898e61a843 Fix File.rename/2 race condition in lock implementation on windows (#14800) 2025-09-30 12:47:22 +02:00
Jean Klingler 16fb81545d Optimize for comprehensions into: MapSet (#14796) 2025-09-29 18:15:50 +09:00
José Valim 3146daf162 Optimize DNFs to avoid negations when possible 2025-09-28 19:59:53 +02:00
Guillaume Duboc 5a2a5ae96c Rewrite maps, tuples, lists as BDDs and improve performance (#14693) 2025-09-28 19:04:45 +02:00
José Valim 0685a35c63 Address regressions on 'not in' operator
Closes #14783.
2025-09-28 17:03:32 +02:00
Zach Daniel f310ed9d82 Make errors when piping expressions in IEx safer (#14795) 2025-09-28 16:18:09 +02:00
José Valim bba5542393 Do not warn on conditional requires or requires from quote 2025-09-27 16:33:05 +02:00
Jean Klingler c9b529a43c Assert scope is not match/guard when using escaped regexes (#14780) 2025-09-27 22:57:50 +09:00
Eksperimental 81cd55beef Align Regex dotall modifier to PCRE2 (#14792) 2025-09-26 09:41:55 +02:00
José Valim 2b424ca674 Deal with relative paths in mix cmd, closes #14787 (#14788) 2025-09-24 14:17:39 +02:00
Thiago Santos f089571351 Warn on unused require (#14779) 2025-09-23 09:38:06 +02:00
Zach Allaun 811767fa6c Update JSON.Encoder moduledoc (#14784) 2025-09-22 21:15:48 +02:00
José Valim 20c7d07345 Improve docs for shift vs add 2025-09-22 10:42:44 +02:00
Jean Klingler 9a8794183e Warn on boot for OTP28.0 (#14732) 2025-09-18 19:08:39 +09:00
Jean Klingler 514bc86a71 Fix Macro.escape/1 bug when :quote tuples is in the tail of a list (#14775)
Close https://github.com/elixir-lang/elixir/issues/14771
2025-09-17 07:15:16 +09:00
Jean Klingler 29e8883134 Optimize Enum.into/2 and Enum.into/3 (#14774)
* Optimize Enum.into(struct, map, fun)

* Optimize Enum.into(enum, map_set)

* Optimize Enum.into(enum, map_set, fun)
2025-09-17 04:31:07 +09:00
Jean Klingler 2ee1d0eb78 Bugfix: Macro.escape/1 properly escapes meta in :quote tuples (#14773)
Close #14771

Also internally renames the `op` field inside `elixir_quote`: `none -> escape`, `prune_metadata` -> `escape_and_prune`, `add_context -> quote`.
2025-09-16 18:36:39 +09:00
Jonatan Männchen 2e7ee76ac2 Correct builds.hex.pm Publish Condition in CI (#14772) 2025-09-15 23:13:08 +02:00
José Valim 6fccf34e6b Engines may return any AST which may return any value, closes #14770 2025-09-15 16:18:25 +02:00
José Valim 2474303f93 Do not use private APIs in EEx 2025-09-14 10:27:03 +02:00
José Valim 2e55f40713 Do not persist temporary compilation warnings, closes #14768 2025-09-14 10:26:55 +02:00
José Valim 479849475e Revert "Convert verification errors into diagnostics, closes #14768"
This reverts commit 2dc3d2713c.

This solution will still leave a corrupted state if
verification is manually aborted.
2025-09-14 09:52:55 +02:00
José Valim 7d521de63a Unify mix recompile and external resource handling
Imagine the following scenario:

1. the user changes an external resource
2. the user calls mix compile and it fails
3. the user reverts the changes to the external resource

We had already fixed this bug for `__mix_recompile__?`.
Therefore, this pull request unifies how both are handled
by touching the source file after we detect it is stale
(which is also what we do on every subsequent compilation
cycle).

Note that before we stored the result of `__mix_recompile__?`
in the checkpoint file. However, that's not needed. The checkpoint
is useful to track changes to sources that may not define any module.
But since the compilation check and external resources always
require a module, touching the file is enough.

Closes https://github.com/phoenixframework/phoenix/issues/6476.
2025-09-13 21:31:33 +02:00
José Valim 2dc3d2713c Convert verification errors into diagnostics, closes #14768 2025-09-13 20:07:35 +02:00
José Valim 59c0a38bb1 Raise if using ~r sigil in patterns or guards, closes #14767 2025-09-12 08:04:07 +02:00
José Valim 1be424db50 Expand on why we supervise, not how (#14764)
Closes #14763.
2025-09-10 22:15:42 +02:00
Jean Klingler 3ae49eb4ec Add missing :generated to Macro.escape_opts/0 type (#14761) 2025-09-10 20:13:46 +09:00
José Valim 175f54869b Update Unicode to version 17.0.0 (#14760)
This is an automated commit created by the Maintenance project
https://github.com/eksperimental/maintenance

Please read the release notes by visiting
<http://www.unicode.org/versions/Unicode17.0.0/>.
2025-09-10 09:09:44 +02:00
Jean Klingler b9387d34d8 Have mix test fail if warnings and --warnings-as-errors (#14756) 2025-09-10 07:45:02 +09:00
Nathan Long 95265ba1c2 ExUnit sets a process label for each test (#14758) 2025-09-09 21:53:07 +02:00
dependabot[bot] 0dd3985f16 Bump actions/attest-sbom from 2.4.0 to 3.0.0 (#14752)
Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 2.4.0 to 3.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/bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b...4651f806c01d8637787e274ac3bdf724ef169f34)

---
updated-dependencies:
- dependency-name: actions/attest-sbom
  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>
2025-09-09 20:05:09 +02:00
José Valim f9dbdb88c9 Accept any enumerable in Logger.metadata/1 2025-09-09 15:31:14 +02:00
Jean Klingler e2f6bbafaa Fix dialyzer opaqueness warnings on module attrs in OTP28 (#14755)
* Mark module attributes as generated in case they contain opaque terms

* Add and use Macro.escape(ast, generated: true)
2025-09-09 17:51:09 +09:00
Art Kay 469a1cc05e Optimize Access.filter to eliminate intermediate list creation (#14749) 2025-09-08 09:49:19 +02:00
Jean Klingler 4008142018 Fix infinite loop: Enum.take/2 with negative index on empty enum (#14747) 2025-09-05 21:24:39 +09:00
Jonatan Männchen 56333d451e tighten CI secret scope and move AWS config to environment vars (#14627)
* Add `environment: release` to the "publish-to-hex" job so that only
  workflows explicitly targeting the release environment can read
  sensitive values.
* Gate the job behind `if: ${{ vars.HEX_AWS_REGION }}` to avoid noisy
  failures in forks where the variable is not configured.
* Replace `${{ secrets.HEX_AWS_REGION }}` / `${{ secrets.HEX_AWS_S3_BUCKET }}`
  references with `${{ vars.* }}`.  These are not credentials, so
  environment-level *variables* are a better fit and keep them readable
  only by jobs that declare the environment.
* Remove Fastly secrets from the job-wide `env:` block and inject them
  only into the Fastly purge step, following the principle of least
  privilege.  Other steps no longer see these tokens.

Restricting secret visibility to an environment and to the exact step
that needs them reduces the blast radius of a compromised workflow run,
blocks accidental exposure in logs of unrelated steps, and stops forks
from obtaining privileged data.
2025-09-03 22:14:09 +02:00
Nevroz ArslanandNevroz Arslan 21c76b58ba Fix wording in Task docs about Ancestor (#14742)
Co-authored-by: Nevroz Arslan <nevrozarslan@gmail.com>
2025-09-03 08:55:21 +02:00
andremaker a5bdf8a881 Mention that receive/1 and ExUnit.Assertions.assert_receive/4 consumes/removes messages (#14743) 2025-09-03 08:38:32 +02:00
José Valim bedb6777c1 Revert "Bump column info on open curly in %{"
Otherwise tokens are emitted with the wrong information.

This reverts commit 6a7511f9af.
2025-09-02 11:23:24 +02:00
José Valim 6a7511f9af Bump column info on open curly in %{ 2025-09-02 10:17:30 +02:00
José Valim 78fb312013 Consistently treat \ followed by newlines as horizontal space
Closes #14713.
Closes #14714.
2025-08-31 20:04:24 +02:00
José Valim 90e1826c7e Include meta information in blocks from do-end blocks
Closes #14689.
2025-08-31 13:51:02 +02:00
José Valim 02968a46ff Add closing token metadata to a.{}, closes #14682 2025-08-31 13:08:28 +02:00
José Valim 33ee657a7f Properly handle column for 'in' in 'not in' operator
Closes #14681.
2025-08-31 13:08:28 +02:00
Jean Klingler b87a9fa35b Do not consider variables from pattern in bitstring modifier (#14738) 2025-08-31 19:20:26 +09:00
José Valim 827e65a5cb Update CHANGELOG 2025-08-31 11:17:03 +02:00
José Valim 54321de136 Update bidi/line break character checks according to UX#55 2025-08-31 10:56:33 +02:00
José Valim 6ee313a835 Only break newlines if original char is a newline 2025-08-31 10:50:57 +02:00
Lukasz Samson c5f1c64be3 Advance line when processing ? followed by <LF> and \<LF>
Closes #14715.
2025-08-31 10:14:49 +02:00
José Valim 848fc1d6df Raise if message in AssertionError is not a binary
Closes #14695.
2025-08-31 10:04:37 +02:00
Łukasz Samson 65ff52aadf Catch-all clause for unbalanced terminators (#14694) 2025-08-31 09:50:55 +02:00
Jean Klingler 032d5660f3 Shallow-validate the return of __escape__ (#14736) 2025-08-31 15:50:35 +09:00
Eksperimental dd45d9666f Use backticks around literals in documentation (#14733) 2025-08-30 19:28:10 +02:00
Eksperimental 9ae79d1385 Format numbers with underscores to improve readability (#14735) 2025-08-30 19:25:48 +02:00
Eksperimental cff61f2404 Use thin-space (U+2009) as a numeric separator in regular English language (#14734)
Related PR: https://github.com/elixir-lang/elixir/pull/14635
2025-08-30 18:09:04 +02:00
Jean Klingler 7a8dc78593 Add __escape__/1 and use it to fix Regex escaping in OTP28.1+ (#14720)
Leverages newly added :re.import/1.
https://github.com/erlang/otp/pull/9976
2025-08-30 18:15:50 +09:00
dependabot[bot] eee42e4448 Bump actions/checkout from 4.2.2 to 5.0.0 (#14710)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/11bd71901bbe5b1630ceea73d27597364c9af683...08c6903cd8c0fde910a37f88322edcfb5dd907a8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.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>
2025-08-30 10:58:44 +02:00
Dmitry Kleymenov 53be491e9f Add gen_server timeout example (#14530) 2025-08-30 10:58:25 +02:00
Eksperimental 5a8ba81b05 Add spec for DynamicSupervisor.child_spec/1 (#14729) 2025-08-29 16:41:15 +02:00
Eksperimental 06fc222e53 Minor supervisor docs improvements (#14730) 2025-08-29 09:31:11 +02:00
Zach Allaun 32a5c97d5a Docs: reorganize description of exit behavior in Process.exit/2 (#14728) 2025-08-28 17:53:45 +02:00
Eksperimental 86d5b3a293 Rephrase docs when mentioning out-of-memory errors (#14727)
Avoid using the term exception as it could be confusing for the user
and mislead him thinking it could be an Elixir `t:Exception.t/0`.
2025-08-28 16:29:34 +02:00
Eksperimental c7b2d6b6e5 Correct docs for Kernel.match?/2 (#14726) 2025-08-28 15:24:55 +02:00
Eksperimental 9f8bacdb5e Minor optimizations for Range.shift/2 (#14724) 2025-08-28 14:25:14 +02:00
Eksperimental 927f91c9fe Fix order in ExUnit results when listing pinned variables (#14723)
The pinned variables were returned in a random order (often reversed):

     test/ex_unit_pinned_variables_order_test.exs:23
     match (=) failed
     The following variables were pinned:
       var_d = "four"
       var_c = "three"
       var_b = "two"
       var_a = "one"
     code:  assert %{a: ^var_d, b: ^var_c, c: ^var_b, d: ^var_a} = build(var_a, var_b, var_c, var_d)
     left:  %{a: ^var_d, b: ^var_c, c: ^var_b, d: ^var_a}
     right: %{a: "one", b: "two", c: "three", d: "four"}
     stacktrace:
       test/ex_unit_pinned_variables_order_test.exs:29: (test)

This fix sorts them alphabetically.

This bug was introduced in 884e93391e when the pinned vars were now accumulated in a map (instead
of a list).

A repo replicating the issue can be found here:
- https://github.com/eksperimental-debug/elixir_debug/tree/ex-unit-pinned-variables-order
- https://github.com/eksperimental-debug/elixir_debug/blob/ex-unit-pinned-variables-order/ex_unit_pinned_variables_order/test/ex_unit_pinned_variables_order_test.exs
2025-08-27 08:06:14 +02:00
Eksperimental 6ea5654438 Document :description field in CompileError (#14692) 2025-08-23 17:13:04 +02:00
Jean Klingler d4ba7ee92c Inspect ill-formed structs as maps (#14718) 2025-08-23 17:10:40 +09:00
Jesse Stimpson ff9608fc22 Improve docs on iex remote shell halt behaviour (#14721) 2025-08-22 15:31:51 +02:00
Eksperimental d21a13b915 Add link to sorter functions in Enum module (#14712) 2025-08-18 20:35:55 +02:00
José Valim f0595a4799 Fix docs for Macro.compile_apply/4 2025-08-17 09:53:38 +02:00
José Valim c7a54aeac0 Improve docs on DynamicSupervisor blocking operations 2025-08-15 19:39:05 +02:00
Eksperimental 1a855ae1ba Add minor clarifications for Enum.{flap_map/2, flat_map_reduce/3} (#14711) 2025-08-14 19:44:41 +02:00
dependabot[bot] 03522aaeb2 Bump actions/download-artifact from 4.3.0 to 5.0.0 (#14708)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.3.0 to 5.0.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...634f93cb2916e3fdff6788551b99b062d0335ce0)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 5.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>
2025-08-12 09:34:43 +02:00
Eksperimental f93919f54a ExUnit: Raise explaining what failed on invalid tags (#14707) 2025-08-08 08:43:30 +02:00
Eksperimental 7d50f43217 Rearrange Examples doc section in System.cmd/3 (#14706)
Examples showed up before "Options" were explained.
2025-08-08 07:49:19 +02:00
Steve Cohen 1f786273e6 Fix filtering documentation (#14705)
The filtering documentation implied that the msg attribute of the
logger event map could be a binary, but according to the erlang types
(https://www.erlang.org/doc/apps/kernel/logger.html#t:log_event/0) it
can't be a binary.

This change updates the docs with an example that comports with the
actual typing.
2025-08-07 20:25:38 +02:00
Jean Klingler 67042e8770 Stop propagating generated on macro arguments (#14696)
* Stop propagating generated on macro arguments

* Explicitly mark assert as generated
2025-08-07 05:54:41 +09:00
Chris Hicks 04f1b7ccae Add options to mix format to allow excluding of files (#14702) 2025-08-06 10:04:54 +02:00
Łukasz Samson 75a3272036 Fix expand crash on invalid multialias root (#14698) 2025-08-06 08:49:12 +02:00
Michał Łępicki 2cfd329eea Improve Enum.min_max/1,2,3 specs and docs (#14697)
- Add back min_max/2 spec for the empty_result fallback clause,
  as a separate spec with non overlapping domain
- Merge min_max/1 and min_max/2 definitions
2025-08-05 11:07:21 +02:00
ice_cap 06ff083e48 Update structs.md (#14683) 2025-08-04 08:55:34 +02:00
Daniel Kukula cacdbf65a6 Add Enum.min_max sorter (#14690) 2025-08-03 09:02:14 +02:00
Eksperimental b5885a6f33 Add guards to functions in Application module (#14687) 2025-08-01 13:45:14 +02:00
Eksperimental 2a71415c65 Fix link to type in Application docs (#14688) 2025-08-01 06:53:01 +09:00
Jonatan Männchen 374f6e342a Update ORT Scanner (#14594) 2025-07-31 15:46:09 +02:00
Daniel Kukula 9b34ec2295 Print intermediate results of dbg for pipes (#14685) 2025-07-31 08:50:18 +02:00
Eksperimental 841f426a4c Comply with naming convention for guards and functions (#14680) 2025-07-30 12:42:25 +02:00
José Valim 2367300c45 Update naming-conventions.md 2025-07-30 11:27:14 +02:00
Eksperimental 290b5d63f8 Mention in Naming Conventions that is_ and trailing ? should not be used together (#14679) 2025-07-30 11:26:30 +02:00
José Valim b2d548af0e Remove general catch on sigil token 2025-07-28 08:28:51 +02:00
Łukasz Samson 23776d9e8f Return error on invalid unicode sequences (#14666) 2025-07-27 19:04:14 +02:00
José Valim 74df71078d Add --name-pattern option to mix test and regex support to OptionParser (#14674) 2025-07-26 19:53:15 +02:00
José Valim 8b1922150b Enhance OptionParser.ParseError with available options display (#14673)
Example output:

  Expected one of:
    --count INTEGER (alias: -c)
    --debug, --no-debug (alias: -d)
    --files STRING (alias: -f) (may be given more than once)
    --verbose, --no-verbose (alias: -v)

Prompt
======

When we raise ParseError, include all of the options we could
potentially accept, alongside their types and aliases. For example,
the switches `[foo: :string, bar: :integer]` and `aliases: [b: :bar]`,
the error message should say:

    Expected one of:
      --foo STRING
      --bar INTEGER (alias: -b)

Furthermore, for types that are :keep (which default to string), you should
add:

    --bar INTEGER (alias: -b) (may be given more than once)

And boolean ones accept no arguments, so they should be written as:

    --baz, --no-baz

Sort all of them alphabetically.
2025-07-26 18:16:49 +02:00
José Valim 8bdd277b0e Improve ExUnit docs 2025-07-26 11:06:37 +02:00
Paul Gideon Dann 4fc85f3d8f Validate type of :deps_paths option for formatter_for_file/2 (#14669) 2025-07-24 18:59:34 +02:00
Eksperimental 01b7dd32e8 Fix typos in Elixir pages (#14670) 2025-07-24 18:12:37 +02:00
Eksperimental f5b65f5d86 Fix typos in Meta-programming anti-patterns page (#14668) 2025-07-24 16:58:31 +02:00
Jean Klingler 0705e3f663 Fix typos in --include docs (#14665) 2025-07-23 17:36:16 +09:00
Jean Klingler a8c8535f8f Don't document 1.18.4 compatibility with OTP28 (#14663) 2025-07-23 17:29:58 +09:00
José Valim 494855aa26 Clarify tag includes, closes #14664 2025-07-23 09:10:34 +02:00
José Valim 4b48982da1 Remove incorrect about incorrect usage of \x in strings
Their usage in Elixir v1.0 was always semantically wrong,
which became deprecation warnings in v1.1 to avoid crashes.
Any software relying on this feature both emits warnings
and invalid strings.

Closes #14660.
2025-07-20 18:22:59 +02:00
dssecret 8344634bb6 Fix return value in logger filter docs (#14659)
Signed-off-by: dssecret <webmaster@deek.sh>
2025-07-20 09:21:07 +02:00
Łukasz Samson d4b0b0ee9b Escape quotes in DOT (#14657) 2025-07-18 19:37:14 +02:00
Jean Klingler c6ab1dbbae Fix opaqueness violation in Task.Supervisor (#14656) 2025-07-18 06:42:30 +09:00
José Valim 20cac294e9 Avoid adding lists that match negations 2025-07-16 22:09:09 +02:00
José Valim f4fdf64b61 Update checker to v2 as representation has changed 2025-07-16 16:39:20 +02:00
Guillaume Duboc 20daffb630 Remove duplicate for map_difference 2025-07-16 16:20:13 +02:00
dependabot[bot] a4f40f7622 Bump azure/trusted-signing-action from 0.5.1 to 0.5.9 (#14653)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.1 to 0.5.9.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/0d74250c661747df006298d0fb49944c10f16e03...bb15ca63eb5548cc306f4f335c5617bb414abcad)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 0.5.9
  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>
2025-07-15 12:45:34 +02:00
dependabot[bot] 79a6a57234 Bump erlef/setup-beam from 1.20.3 to 1.20.4 (#14652)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.20.3 to 1.20.4.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/8e7fdef09ffa3ea54328b5000c170a3b9b20ca96...e6d7c94229049569db56a7ad5a540c051a010af9)

---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.20.4
  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>
2025-07-15 11:47:21 +02:00
Benjamin Milde fb8f6900fa Docs updates und restructuring (#14636) 2025-07-12 22:07:34 +02:00
Vasilis Spilka d5460361b2 Add printable_limit and limit to IO.inspect doc examples (#14646) 2025-07-12 21:58:00 +02:00
Michał Łępicki 419ce18d2c Clean up unreachable clause of Types.Descr.atom_only? helper (#14647)
it's being always called with a map
2025-07-12 20:49:19 +02:00
Jean Klingler 4b09a08042 Drop :app_properties when rendering dependency in mix (#14645) 2025-07-12 21:10:26 +09:00
José Valim ce64fc364d Revamp Mix & OTP guides (#14637) 2025-07-11 15:51:18 +02:00
Gary Rennie be9352ed70 Add ETS to the Erlang Term Storage section of erlang libs (#14644)
This will ensure that it appears in the short search results on ExDoc
instead of having to navigate through to the search results.
2025-07-11 15:05:33 +02:00
Guillaume Duboc 2f2368a2d4 Domain keys in map (#14478)
- Introduced tests for union, intersection, and difference operations involving domain key types.
- Validated subtype relationships and intersection results for maps with domain keys.
- Enhanced map fetch and delete functionalities to handle domain key types.
- Ensured correct behavior of dynamic types with domain keys in various scenarios.
2025-07-11 15:03:26 +02:00
José Valim bbe1709dca Clarify function types 2025-07-11 12:56:46 +02:00
José Valim c1c8ebaa8f Tag / as an operator in fragments, closes #14643 2025-07-11 11:12:24 +02:00
José Valim 2350e83f5b Update ... to an operator in Code.Fragment 2025-07-11 11:12:24 +02:00
Noah Betzen 677e1ec8b1 Add mix test --dry-run flag (#14499) 2025-07-11 09:49:47 +02:00
Łukasz Samson 67593e1c9a Update allow_local option spec (#14642) 2025-07-10 20:48:30 +02:00
José Valim a6844bf725 Add tests for allow_locals option 2025-07-10 17:30:36 +02:00
Łukasz Samson 802ecdd4a0 Add local_for_callback option to Macro.Env.expand_import (#14620) 2025-07-10 17:15:21 +02:00
Eksperimental 36eb9daf1a Fix typo meaning stderr (#14640) 2025-07-10 12:24:58 +02:00
José Valim c710b2afb7 Remove explicit mentions to elixirc, as it isn't used in practice 2025-07-10 10:21:49 +02:00
Eksperimental 352f2723a9 Use backticks around literals in documentation (#14633) 2025-07-10 09:36:20 +02:00
Eksperimental 8bc49af49d Use thin space (U+2009) as a separator instead of _ and in regular English language (#14635) 2025-07-10 09:35:53 +02:00
José Valim e55c61ffaf Improve error message for protocols with no implementation, closes #14364 2025-07-10 09:30:34 +02:00
Eksperimental fa51593cc6 Standardize "Examples" heading section levels in docs (#14638)
* Convert "Examples" 3rd level headings to 2nd level when not under a 2nd level

* Convert 1st level "Examples" heading to 2nd level
2025-07-09 20:09:07 +02:00
Eksperimental f2804b96db Correct grammar in structural sorting order section (#14639)
It is not required by the Elixir developer, but it is not required for them to know this by heart.

The former indicates that it is the Elixir developers who are not requiring this, the latter expresses
that they do not need to know this by heart.
2025-07-09 20:08:47 +02:00
dependabot[bot] 29ee3a2ad8 Bump erlef/setup-beam from 1.20.1 to 1.20.3 (#14631)
Bumps [erlef/setup-beam](https://github.com/erlef/setup-beam) from 1.20.1 to 1.20.3.
- [Release notes](https://github.com/erlef/setup-beam/releases)
- [Commits](https://github.com/erlef/setup-beam/compare/75edbb82877ab10edeb12a99c3cf2f0909f3dc87...8e7fdef09ffa3ea54328b5000c170a3b9b20ca96)

---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.20.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>
2025-07-08 13:10:44 +02:00
Jean Klingler 0be3e70037 Prevent mix test from overriding :failures_manifest_path option (#14632)
Introduced in 99be673
2025-07-08 09:00:58 +02:00
Zach Daniel e78f105f23 use simpler words in mix-and-otp guides (#14630) 2025-07-07 18:11:06 +02:00
José Valim 1fb7ca159c Fix logger docs
Closes #14628.
Closes #14629.
2025-07-06 09:46:31 +02:00
José Valim 785ffcc7cd Add TODO 2025-07-06 09:46:31 +02:00
José Valim cfc43b37d4 Add notes to the code 2025-07-06 09:46:31 +02:00
Jean Klingler 57c254f725 Increase test coverage of IEx ls helper, handle edge case and make windows consistent (#14626) 2025-07-05 17:49:19 +09:00
José Valim efcd164e38 Fix return type of phi, closes #14621 2025-07-04 09:34:28 +02:00
José Valim b29c83a42f Apply further fn optimizations and fixes (#14619)
Closes #14598
2025-07-03 15:58:17 +02:00
José Valim d7bdea55de Add required field back to struct info
Closes #14616.
Closes #14617.
Closes #14500.
2025-07-03 12:21:39 +02:00
Guillaume Duboc 824fc3bc24 Perf optimizations and inferred intersections (#14605) 2025-07-03 11:39:54 +02:00
Łukasz Samson 3e373e8a59 Handle filesystem errors in iex helpers (#14618)
`File.cd` and `File.ls` can return any posix error code
2025-07-03 10:24:19 +02:00
Michał Łępicki 0253f614b7 Fix parallel option type in Mix.Compilers.Erlang.compile/6 spec (#14615)
compile.yecc and compile.leex tasks call it with parallel: true
2025-07-01 22:34:21 +02:00
Łukasz Samson b26ae516ac Add missing erlang compiler options (#14614)
Document options on leex and yecc compilers
2025-07-01 15:53:34 +02:00
José Valim cf8689e498 Remove specs which are pass through and from private modules 2025-07-01 10:44:03 +02:00
dependabot[bot] dc1890b879 Bump erlef/setup-beam from 1.19.0 to 1.20.1 (#14612)
---
updated-dependencies:
- dependency-name: erlef/setup-beam
  dependency-version: 1.20.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>
2025-07-01 10:20:33 +02:00
Łukasz Samson e3f7759fde Replace keyword with concrete keyword lists in specs (#14611) 2025-06-30 17:24:28 +02:00
José Valim 2fe44b747d Fix warnings on Erlang/OTP 28 2025-06-30 11:57:23 +02:00
José Valim 32ed2c38b2 Remove Regex warning until Erlang/OTP 28.1 2025-06-30 10:55:16 +02:00
José Valim ad524f528f Improve URI docs 2025-06-29 16:21:32 +02:00
José Valim 237263c446 Make sure we log all output when partition fails 2025-06-27 15:29:50 +02:00
José Valim a5d2aa88f3 Deal with undefined on :shell.whereis/0 2025-06-26 13:48:55 +02:00
José Valim b188f4d33d Do not send quoted expressions to Macro.dbg 2025-06-26 13:38:14 +02:00
José Valim 3482e4e740 Fix pry on Erlang/OTP 28 2025-06-26 13:32:51 +02:00
Art Kay 8172ff0775 Fix minor typo in the docs (#14606) 2025-06-26 07:17:16 +09:00
José Valim 274169f628 Update CHANGELOG.md 2025-06-25 20:45:16 +02:00
José Valim 08d3865fad Transform the struct update syntax into a type assertion
This transforms the struct update into a type assertion,
requiring the type system to be sure the expression has
precisely the given struct type.

The struct update syntax may still be deprecated in the
future but this will provide a safer migration path and
allow us to engage in more conversations with the community.
2025-06-25 19:30:04 +02:00
Jonatan Männchen 1753c81f9e Use Workload Identity Federation for Windows Trusted Signing (#14604) 2025-06-25 19:29:47 +02:00
Łukasz Samson 54489ef86f Fix time unit in comment (#14600) 2025-06-24 07:09:23 +02:00
Steffen Deusch 0487e4dd8a Add compilers option to Mix.install/2 (#14577) 2025-06-22 11:54:12 -07:00
José Valim 54515bf48a Distinguish source_anno from doc_anno, see #14595 2025-06-21 03:54:44 -07:00
José Valim 9d4c91be2a Ensure block_keyword_or_binary_operator is handled in surround context, closes #14590 2025-06-21 03:25:36 -07:00
Guillaume Duboc cc9a5e7682 Simplified tuple definitions by removing negations (#14596) 2025-06-20 05:20:08 -07:00
Łukasz Samson b2587e8633 Handle error result from unescape_tokens in tokenizer (#14587) 2025-06-19 12:02:02 -07:00
Łukasz Samson 6ca0ad84b7 Consistently raise UnicodeConversionError in tokenizer (#14589) 2025-06-19 12:00:43 -07:00
Christopher Keele 66753a868b Improve docs for "textual sigils" (#14591) 2025-06-19 11:10:55 -07:00
Łukasz Samson 972f9e46ed Fix invalid warning on no parens call on true (#14593) 2025-06-19 05:56:47 -07:00
Łukasz Samson f07bc7b3d5 Fix typo in elixir_erl_pass inline_pure_function decode_www_form (#14592) 2025-06-19 20:08:57 +09:00
dependabot[bot] dc4c1cf402 Bump actions/attest-sbom from 2.2.0 to 2.4.0 (#14582)
Bumps [actions/attest-sbom](https://github.com/actions/attest-sbom) from 2.2.0 to 2.4.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/115c3be05ff3974bcbd596578934b3f9ce39bf68...bd218ad0dbcb3e146bd073d1d9c6d78e08aa8a0b)

---
updated-dependencies:
- dependency-name: actions/attest-sbom
  dependency-version: 2.4.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>
2025-06-16 16:14:39 -07:00
José Valim e27cd15715 Optimize empty_difference_subtype? for dynamic parts 2025-06-14 12:13:15 -07:00
Jean Klingler d59f49aa54 Mark inlined function call result as generated (#14581) 2025-06-14 19:05:31 +09:00
Tobias Pfeiffer 7ad145082b Document Map.fetch!/2 exception in its doc (#14573) 2025-06-12 10:53:06 +02:00
José Valim fb9a9e97d8 Allow captures to be reconstructed on type system pretty printing 2025-06-11 13:51:24 +02:00
José Valim 5f2633a305 Do no start listeners if --no-deps-check is given 2025-06-11 12:08:45 +02:00
José Valim e04444c42e Warn when invalid fun typespec is used 2025-06-11 12:08:45 +02:00
Jean Klingler 7da4d6570b Remove redundant memory-per-call test (#14572) 2025-06-11 16:03:44 +09:00
José Valim 5946e8bb9d Improve error message for bad/missing clauses, closes #14569 2025-06-10 14:38:30 +02:00
Joe Yates 46b5fabc63 Fix use of prefer with '-ing' (#14568) 2025-06-10 11:17:04 +02:00
José Valim bff9f3ebbc Point out module must be required before macro usage in match/guard 2025-06-10 10:13:50 +02:00
José Valim ff4adca18a Filter @compile debug_info when explicitly set to true
Closes #14567.
2025-06-10 09:58:23 +02:00
Theodor-Alexandru Irimia 954133d64c Clarify why and how to start second session for tests (#14566) 2025-06-09 22:26:53 +02:00
José Valim 319360ce9f Fix logger function link in CHANGELOG 2025-06-09 22:26:31 +02:00
Tomasz Marek Sulima 7ef6905484 Sort by call on tprof memory tests (Erlang/OTP 28) (#14565) 2025-06-09 22:25:47 +02:00
José Valim 52495ba8bc Add tests for optional subtyping 2025-06-09 13:08:12 +02:00
José Valim 514fbbaf52 Update tables 2025-06-09 12:44:56 +02:00
Michał Łępicki 34cf44587c Fix mistake in "Untracked compile-time dependencies" anti-pattern (#14563) 2025-06-09 12:07:18 +02:00
José Valim 57ed90d5b7 Also download rebar3 automatically when compiling 2025-06-08 19:13:05 +02:00
José Valim 09dcbf7566 Introduce deprecations scheduled for Elixir v1.20 2025-06-07 21:55:06 +02:00
José Valim c001cbdd62 Start v1.20 2025-06-07 20:21:32 +02:00
438 changed files with 38640 additions and 15763 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
-40
View File
@@ -1,40 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: CI for Markdown content
on:
push:
branches:
- "main"
paths:
- "lib/**/*.md"
pull_request:
paths:
- "lib/**/*.md"
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
name: Lint Markdown content
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+61 -84
View File
@@ -1,15 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: CI
on:
push:
paths-ignore:
- "lib/**/*.md"
pull_request:
paths-ignore:
- "lib/**/*.md"
workflow_dispatch:
env:
ELIXIR_ASSERT_TIMEOUT: 2000
@@ -21,65 +19,64 @@ permissions:
jobs:
test_linux:
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- otp_version: "28.0"
- otp_version: "29.0"
deterministic: true
- otp_version: "28.0"
erlc_opts: "warnings_as_errors"
- otp_version: "28.4"
docs: true
coverage: true
- otp_version: "28.0"
otp_latest: true
erlc_opts: "warnings_as_errors"
- otp_version: "28.1"
- otp_version: "27.3"
erlc_opts: "warnings_as_errors"
- otp_version: "27.0"
erlc_opts: "warnings_as_errors"
- otp_version: "26.0"
- otp_version: master
development: true
- otp_version: maint
development: true
runs-on: ubuntu-24.04
# Earlier Erlang/OTP versions ignored compiler directives
# when using warnings_as_errors. So we only set ERLC_OPTS
# from Erlang/OTP 27+.
env:
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
ERLC_OPTS: "warnings_as_errors"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
persist-credentials: false
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: ${{ matrix.otp_version }}
- name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
- name: Compile Elixir
run: |
make compile
echo "$PWD/bin" >> $GITHUB_PATH
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make test_erlang
continue-on-error: ${{ matrix.development }}
continue-on-error: ${{ matrix.development == true }}
- name: Elixir test suite
run: make test_elixir
continue-on-error: ${{ matrix.development }}
continue-on-error: ${{ matrix.development == true }}
env:
COVER: "${{ matrix.coverage }}"
- name: "Calculate Coverage"
run: make cover | tee "$GITHUB_STEP_SUMMARY"
if: "${{ matrix.coverage }}"
- name: Build docs (ExDoc main)
if: ${{ matrix.otp_latest }}
if: ${{ matrix.docs }}
run: |
cd ..
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
@@ -88,87 +85,67 @@ jobs:
cd ../elixir/
git fetch --tags
DOCS_OPTIONS="--warnings-as-errors" make docs
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
# Recompile System without .git
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
taskset 1 make check_reproducible
- name: "Calculate Coverage"
if: ${{ matrix.coverage }}
run: make cover | tee "$GITHUB_STEP_SUMMARY"
- name: "Upload Coverage Artifact"
if: "${{ matrix.coverage }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: TestCoverage
path: cover/*
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: taskset 1 make check_reproducible
- name: Check git is not required
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
cd lib/elixir
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
test_windows:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
name: Windows Server 2022, OTP ${{ matrix.otp_version }}
runs-on: windows-2022
strategy:
matrix:
otp_version: ["26.2", "27.3", "28.0"]
runs-on: windows-2022
otp_version:
- "29.0"
- "28.1"
- "27.3"
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
persist-credentials: false
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: ${{ matrix.otp_version }}
- name: Compile Elixir
run: |
Remove-Item -Recurse -Force '.git'
make compile
- name: Build info
run: bin/elixir --version
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make test_erlang
- name: Elixir test suite
run: |
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
make test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 50
- name: Install Shellcheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Check POSIX-compliant
run: |
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
license_compliance:
name: Check Licence Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Run OSS Review Toolkit"
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
+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
+41
View File
@@ -0,0 +1,41 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: License Compliance
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
license_compliance:
name: Check License Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run OSS Review Toolkit
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
+41
View File
@@ -0,0 +1,41 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Markdown Content
on:
push:
branches:
- "main"
paths: &paths-filter
- "**/*.md"
- .github/workflows/markdown.yml
- .markdownlint-cli2.jsonc
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
lint:
name: Lint Markdown content
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
+12 -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
@@ -62,6 +63,16 @@ runs:
# Override Default Evaluator Rules
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
# Add Package Configurations
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
for FILE in .ort/package-configurations/*.yml; do
COMPONENT="$(basename "$FILE")"
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
sed -i -E \
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
done
# Set Version in SPDX & Config
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
@@ -80,7 +91,7 @@ runs:
id: ort
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
with:
image: ghcr.io/oss-review-toolkit/ort-minimal:54.0.0
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
run: >-
labels,
cache-dependencies,
+53
View File
@@ -0,0 +1,53 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: POSIX Compliance
on:
push:
paths: &paths-filter
- .github/workflows/posix_compliance.yml
- bin/elixir
- bin/elixirc
- bin/iex
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
check_posix_compliance:
name: Check POSIX compliance
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install ShellCheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Run ShellCheck on bin/ dir
run: |
shellcheck -e SC2039,2086 bin/elixir && \
echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && \
echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && \
echo "bin/iex is POSIX compliant"
+106 -80
View File
@@ -1,16 +1,19 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Release
name: Releases
on:
push:
branches:
- main
- v*.*
tags:
- v*
workflow_dispatch:
env:
ELIXIR_OPTS: "--warnings-as-errors"
LANG: C.UTF-8
@@ -20,64 +23,68 @@ permissions:
jobs:
create_draft_release:
runs-on: ubuntu-22.04
name: Create draft release
runs-on: ubuntu-24.04
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
run: |
gh release create \
--repo ${{ github.repository }} \
--title ${{ github.ref_name }} \
--repo "$GITHUB_REPOSITORY" \
--title "$GITHUB_REF_NAME" \
--notes '' \
--draft \
${{ github.ref_name }}
"$GITHUB_REF_NAME"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# zizmor: ignore[artipacked]
if: github.ref_type == 'branch'
with:
fetch-depth: 50
- name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch'
run: |
ref_name=${{ github.ref_name }}-latest
ref_name="${GITHUB_REF_NAME}-latest"
if ! gh release view $ref_name; then
if ! gh release view "$ref_name"; then
gh release create \
--latest=false \
--title $ref_name \
--notes "Automated release for latest ${{ github.ref_name }}." \
$ref_name
--title "$ref_name" \
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
"$ref_name"
fi
git tag $ref_name --force
git push origin $ref_name --force
git tag "$ref_name" --force
git push origin "$ref_name" --force
build:
name: "Build Elixir"
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
runs-on: ubuntu-24.04
strategy:
fail-fast: true
matrix:
include:
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
- otp: 28
otp_version: "28.0"
build_docs: build_docs
runs-on: ubuntu-22.04
- otp: 29
otp_version: "29.0"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 50
persist-credentials: false
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
@@ -92,64 +99,65 @@ jobs:
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Upload Windows release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.build_docs
with:
name: Docs
path: Docs.zip*
sign:
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
otp: [26, 27, 28]
otp: [27, 28, 29]
flavor: [windows, linux]
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }}
permissions:
contents: write
id-token: write
steps:
- name: "Download build"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
# AZURE_TENANT_ID and AZURE_CLIENT_ID should stay the same,
# but AZURE_CLIENT_SECRET has expiration date. When it expires go to
# App Registrations / <app> / Certificates & secrets,
# click (+) New client secret, note the "Value" (not "Secret ID")
# and update it:
#
# $ gh --repo elixir-lang/elixir secret set AZURE_CLIENT_SECRET
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: trusted-signing-elixir
certificate-profile-name: Elixir
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
@@ -173,17 +181,15 @@ jobs:
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
sbom:
name: Generate SBoM
needs: [build, sign]
runs-on: ubuntu-24.04
permissions:
@@ -199,11 +205,13 @@ jobs:
- name: Checkout project
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -218,7 +226,7 @@ jobs:
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
@@ -238,15 +246,19 @@ jobs:
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
done
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
env:
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
- name: "Assemble Release SBoM Artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "SBoM"
path: |
@@ -256,37 +268,38 @@ jobs:
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "Attestations"
path: "attestations/*.sigstore"
upload-release:
name: Upload release
needs: [create_draft_release, build, sign, sbom]
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
- name: Upload Pre-built
- name: Upload Pre-build
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.ref_type }}" == "branch" ]; then
tag=${{ github.ref_name }}-latest
if [ "$GITHUB_REF_TYPE" == "branch" ]; then
tag="${GITHUB_REF_NAME}-latest"
else
tag="${{ github.ref_name }}"
tag="$GITHUB_REF_NAME"
fi
gh release upload \
--repo ${{ github.repository }} \
--repo "$GITHUB_REPOSITORY" \
--clobber \
"$tag" \
elixir-otp-*.zip \
@@ -301,20 +314,26 @@ jobs:
bom.*
upload-builds-hex-pm:
name: Upload builds to hex.pm
runs-on: ubuntu-24.04
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
OTP_GENERIC_VERSION: "25"
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -325,10 +344,10 @@ jobs:
- name: Upload Precompiled to S3
run: |
ref_name=${{ github.ref_name }}
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
dest=${zip/elixir/${GITHUB_REF_NAME}}
surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
@@ -336,17 +355,17 @@ jobs:
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${ref_name} >> purge_keys.txt
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
fi
done
- name: Upload Docs to S3
run: |
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
unzip Docs.zip
@@ -367,7 +386,9 @@ jobs:
- name: Update builds txt
run: |
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name=${{ github.ref_name }}
ref_name="$GITHUB_REF_NAME"
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
touch builds.txt
@@ -379,7 +400,7 @@ jobs:
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
@@ -418,3 +439,8 @@ jobs:
for key in $(cat purge_keys.txt); do
purge "${key}"
done
env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Notify
name: Release Notifications
on:
release:
@@ -15,17 +15,20 @@ jobs:
notify:
runs-on: ubuntu-latest
name: Notify
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
persist-credentials: false
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: "27.3"
elixir-version: "1.18.3"
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
run: |
elixir .github/workflows/notify.exs ${{ github.ref_name }}
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
+24 -10
View File
@@ -1,45 +1,58 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: "Release pre built"
description: "Builds elixir release, ExDoc and generates docs"
name: Release Pre-build
description: "Builds Elixir release, ExDoc and generates docs"
inputs:
otp:
description: "The major OTP version"
otp_version:
description: "The exact OTP version (major.minor[.patch])"
build_docs:
description: "If docs have to be built or not"
description: "Whether docs have to be built"
runs:
using: "composite"
steps:
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
with:
otp-version: ${{ inputs.otp_version }}
version-type: strict
- name: Build Elixir Release
shell: bash
run: |
run: | # zizmor: ignore[github-env]
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip"
echo "$PWD/bin" >> $GITHUB_PATH
env:
INPUT_OTP: ${{ inputs.otp }}
- name: Install NSIS
shell: bash
run: |
sudo apt update
sudo apt install -y nsis
- name: Build Elixir Windows Installer
shell: bash
run: |
export OTP_VERSION=${{ inputs.otp_version }}
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
export OTP_VERSION="$INPUT_OTP_VERSION"
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip"
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" .
env:
INPUT_OTP: ${{ inputs.otp }}
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
run: | # zizmor: ignore[github-env]
if [ "$GITHUB_REF_NAME" = "main" ]; then
ref=main
else
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
@@ -51,6 +64,7 @@ runs:
repository: elixir-lang/ex_doc
ref: ${{ env.EX_DOC_REF }}
path: ex_doc
persist-credentials: false
- name: Build ex_doc
if: ${{ inputs.build_docs }}
shell: bash
+3 -2
View File
@@ -10,9 +10,10 @@
/lib/elixir/test/ebin/
/man/elixir.1
/man/iex.1
/Docs-v*.zip
/Precompiled-v*.zip
/Docs.zip
/Precompiled.zip
/.eunit
.elixir.plt
erl_crash.dump
/cover/
.tool-versions
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
"globs": [
"**/*.md"
],
"ignores": [
".git/**"
],
"gitignore": true,
"config": {
// Consecutive header levels (h1 -> h2 -> h3).
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content.
"MD024": {
// Duplication is allowed for headings with different parents.
"siblings_only": true
},
// Trailing punctuation in heading.
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
// the default values.
"MD026": {
"punctuation": ".,;:。,;:!"
},
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false,
// Our tables are too large to align.
"MD060": false
}
}
-45
View File
@@ -1,45 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content. That's fine.
"MD024": false,
// Some headers finish with ! because it refers to a function name
"MD026": false,
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false
}
+56 -83
View File
@@ -3,18 +3,6 @@
excludes:
paths:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "man/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
@@ -25,8 +13,64 @@ excludes:
reason: "BUILD_TOOL_OF"
comment: "Documentation"
# Unfortunately we'll have to repeat all package level excludes here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
- pattern: "lib/*/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "lib/*/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/*/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
curations:
license_findings:
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
# Unfortunately we'll have to repeat all package level license curations here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
# Test Fixtures
- path: "lib/*/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
@@ -39,13 +83,6 @@ curations:
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
@@ -54,26 +91,11 @@ curations:
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
@@ -89,57 +111,8 @@ curations:
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
packages:
- id: "SpdxDocumentFile:The Elixir Team:elixir-lang:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
- id: "SpdxDocumentFile:The Elixir Team:eex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:elixir:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:exunit:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:iex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:logger:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:mix:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
+8 -1
View File
@@ -3,6 +3,7 @@
ort:
enableRepositoryPackageCurations: true
enableRepositoryPackageConfigurations: true
scanner:
skipConcluded: false
@@ -11,4 +12,10 @@ ort:
analyzer:
allowDynamicVersions: true
enabledPackageManagers: [SpdxDocumentFile]
skipExcluded: true
reporter:
reporters:
SpdxDocument:
options:
creationInfoOrganization: The Elixir Team
documentName: "Elixir Source SPDX Document"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:eex:"
path_excludes:
- pattern: "lib/eex/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+60
View File
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:elixir:"
path_excludes:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/elixir/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:exunit:"
path_excludes:
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/ex_unit/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+8
View File
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:logger:"
path_excludes:
- pattern: "lib/logger/test/**/*"
reason: "TEST_OF"
comment: "Tests"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:mix:"
path_excludes:
- pattern: "lib/mix/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+241 -228
View File
@@ -4,13 +4,55 @@
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Changelog for Elixir v1.19
# Changelog for Elixir v1.20
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
## Type system improvements
### More type inference
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
### Type inference of guards
This release also performs inference of guards! Let's see some examples:
```elixir
def example(x, y) when is_list(x) and is_integer(y)
```
The code above correctly infers `x` is a list and `y` is an integer.
```elixir
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
```
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
```elixir
def example(x) when is_map_key(x, :foo)
```
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
```elixir
def example(x) when not is_map_key(x, :foo)
```
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
You can also have expressions that assert on the size of data structures:
```elixir
def example(x) when tuple_size(x) < 3
```
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
### Whole-body type inference
Elixir also performs inference based on the function body itself. Take the following code:
```elixir
def add_foo_and_bar(data) do
@@ -28,296 +70,267 @@ def sum_to_string(a, b) do
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
### Type checking of protocol dispatch and implementations
### Typing across clauses
This release also adds type checking when dispatching and implementing protocols.
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
```elixir
defmodule Example do
def my_code(first..last//step = range) do
"hello #{range}"
case System.get_env("SOME_VAR") do
nil -> :not_found
value -> {:ok, String.upcase(value)}
end
```
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
### Typing of atom and domain keys in maps
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
```elixir
%{123 => "hello", 456.0 => :ok}
```
will have the type:
```elixir
%{integer() => binary(), float() => :ok}
```
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
```elixir
%{integer() => integer(), root: integer()}
```
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
### Typing of map operations
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
```elixir
Map.put(map, :key, 123)
#=> returns type %{..., key: integer()}
Map.delete(map, :key)
#=> returns type %{..., key: not_set()}
```
As you can see, we track when keys are set and also when they are removed.
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
```elixir
Map.replace(map, :key, 123)
#=> returns type %{..., key: if_set(integer())}
```
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
```elixir
defmodule User do
def name(map), do: Map.fetch!(map, :name)
end
defmodule CallsUser do
def calls_name do
User.name(%{})
end
end
```
the above emits the following warnings:
The code above has a type violation, which is now caught by the type system:
```
warning: incompatible value given to string interpolation:
```text
warning: incompatible types given to User.name/1:
data
it has type:
%Range{first: term(), last: term(), step: term()}
but expected a type that implements the String.Chars protocol, it must be one of:
dynamic(
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
%Version.Requirement{}
) or atom() or binary() or float() or integer() or list(term())
```
Warnings are also emitted if you pass a data type that does not implement the `Enumerable` protocol as a generator to for-comprehensions:
```elixir
defmodule Example do
def my_code(%Date{} = date) do
for(x <- date, do: x)
end
end
```
will emit:
```
warning: incompatible value given to for-comprehension:
x <- date
it has type:
%Date{year: term(), month: term(), day: term(), calendar: term()}
but expected a type that implements the Enumerable protocol, it must be one of:
dynamic(
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
) or fun() or list(term()) or non_struct_map()
```
### Type checking and inference of anonymous functions
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
```elixir
defmodule Example do
def run do
fun = fn %{} -> :map end
fun.("hello")
end
end
```
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
```
warning: incompatible types given on function application:
fun.("hello")
User.name(%{})
given types:
binary()
%{name: not_set()}
but function has type:
but expected one of:
(dynamic(map()) -> :map)
dynamic(%{..., name: term()})
typing violation found at:
type warning found at:
│
6 │ fun.("hello")
│ ~
16 │ User.name(%{})
│ ~
│
└─ mod.exs:6:8: Example.run/0
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
```
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
### Acknowledgements
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
## Faster compile times in large projects
## Compile-time improvements
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
While Elixir has always compiled the given files in project or a dependency in parallel, the compiler would sometimes be unable to use all of the machine resources efficiently. This release addresses two common limitations, delivering performance improvements that scale with codebase size and available CPU cores.
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
### Code loading bottlenecks
* Errors during compilation may have less precise stacktraces
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
* Anonymous functions within `defmodule` can have only up to 20 arguments.
If this is an issue, you can use maps or tuples to group the data.
Note the functions themselves inside `defmodule`, such as the ones defined
inside `def` and friends, can still have up to 255 arguments
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
## v1.20.0 (2026-06-03)
The only potential regression in this approach happens if you have a module, which is used at compile time and defines an `@on_load` callback (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invokes another modules within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
@on_load :init
def init do
MyLib.AnotherModule.do_something()
end
def something_else do
...
end
end
MyLib.SomeModule.something_else()
```
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
### Parallel compilation of dependencies
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
## Improved pretty printing algorithm
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
```elixir
[
[1, 2, 3],
[1, 2, ...],
[1, ...],
[...],
...
]
```
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on actually filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
```elixir
[
[1, 2, 3],
...
]
```
The outer list is the first element, the first nested list is the second, followed by three numbers, reaching the limit. This gives developers more precise control over pretty printing.
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
## OpenChain certification
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
* Elixir releases now include a Source SBoM in CycloneDX 1.6 or later and SPDX 2.3 or later formats.
* Each release is attested along with the Source SBoM.
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
## v1.19.0-dev
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### EEx
* [EEx] Optimize compiler by flattening expr list only once
#### Elixir
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
* [Code.Fragment] Add `Code.Fragment.lines/1`
* [Enum] Provide more information on `Enum.OutOfBoundsError`
* [Inspect] Allow `optional: :all` when deriving Inspect
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
* [Kernel] Allow controlling which applications are used during inference
* [Kernel] Support `min/2` and `max/2` as guards
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
* [Protocol] Type checking of protocols dispatch and implementations
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
* [String] Add `String.count/2` to count occurrences of a pattern
* [Base] Optimize Base validation functions by using SWAR techniques
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Code] Add `:dbg_callback` option to eval functions
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [Enum] Add `Enum.min_max` sorter
* [File] Add support for `[:raw]` opts in `File.read/2`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Float] Optimize `Float.round/2` by avoiding big integers
* [Inspect] Increase inspect limit to help print deeply nested data structures
* [Inspect] Support printing Erlang records (using Erlang notation)
* [Integer] Add `Integer.ceil_div/2`
* [Integer] Add `Integer.popcount/1`
* [IO] Add `IO.iodata_empty?/1`
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
* [Kernel] Detect and warn on redundant clauses
* [Kernel] Perform type inference across applications
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Kernel] Warn on unused requires
* [List] Add `List.first!/1` and `List.last!/1`
* [Module] Purge and delete modules if `after_compile/2` callback fails
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
* [Process] Add `Process.get_label/1`
* [Registry] Switch `keys: {:duplicate, :key}` to `ordered_set` with composite keys
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
* Add Software Bill of Materials guide to the Documentation
#### ExUnit
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
* [ExUnit.Case] Add `:test_group` to the test context
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
* [ExUnit.Doctest] Add `:inspect_opts` option for doctest
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### IEx
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
* [IEx] Optimize autocompleting modules
* [IEx.Helpers] Add `source/1`
#### Mix
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
* [mix xref graph] Add support for `--format json`
* [mix xref graph] Emit a warning if `--source` is part of a cycle
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
* [mix app.tree] Support `--output` option
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
* [mix deps] Support filtering `mix deps` output
* [mix deps.tree] Support `--output` option
* [mix format] Support `--no-compile` option
* [mix help] Support printing docs for types and callbacks
* [mix source] Add `mix source MODULE` to print or open a given module/function location
* [mix test] Add `mix test --dry-run`
### 2. Bug fixes
### 2. Potential breaking changes
#### Elixir
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
* [Kernel] Disallow raw CR line ending in strings, comments, and after `?` for security reasons
* [Kernel] `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
#### Mix
* [mix cmd] Preserve argument quoting in subcommands
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
* [mix test] Preserve files with no longer filter on `mix test`
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
### 3. Soft deprecations (no warnings emitted)
### 3. Bug fixes
#### Elixir
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Allowing preserving directory permissions in `File.cp_r/3`
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
* [File] Fix `File.Stream`'s `Enumerable.count` for files without trailing newline
* [File] Warn when defining `@type record()` for Erlang/OTP 29
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
* [Kernel] Reject negative Duration in `to_timeout/1`
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
* [String] Support empty pattern list in `String.count/2`
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
#### ExUnit
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
#### IEx
* [IEx] Ensure pry works across remote nodes
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
#### Logger
* [Logger] Persist log level to app env in `Logger.configure/1`
#### Mix
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
* [mix compile] Add a build lock around protocol consolidation in umbrellas
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
* [mix test] Respect `--raise` when `mix test --warnings-as-errors` passes with warnings
### 4. Hard deprecations
#### Elixir
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}` is deprecated in favor of pattern matching on the struct when the variable is defined and then using the map update syntax `%{uri | path: "/foo/bar"}`. Thanks to the type system, pattern matching on structs can find more errors, more reliably
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
#### Logger
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
#### Mix
* [mix] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
## v1.18
## v1.19
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
+5 -5
View File
@@ -6,7 +6,7 @@
# Code of Conduct
Contact: elixir-lang-conduct@googlegroups.com
Contact: <elixir-lang-conduct@googlegroups.com>
## Why have a Code of Conduct?
@@ -51,15 +51,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
* The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
* The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
+96 -45
View File
@@ -78,6 +78,7 @@ introduced behavior, especially for bug fixes and major changes:
*fails* before your change and *passes* afterward. This makes it easier to
confirm that the fix addresses the underlying issue and helps prevent
regressions in the future.
* **New Features or Major Changes:** If you are adding a new feature or making
major changes to existing functionality, please add tests that cover the
major parts of that functionality. Aim to have the best code coverage possible.
@@ -88,7 +89,9 @@ We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes
@@ -96,15 +99,11 @@ case you are looking for some examples:
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
All pull requests must be reviewed before being merged into the repository.
If changes are necessary, the team will leave appropriate comments requesting
changes to the code. Unfortunately, we cannot guarantee a pull request will
be merged, even when modifications are requested, as the Elixir team will
re-evaluate the contribution as it changes.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
@@ -115,36 +114,63 @@ into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir.
After cloning and compiling Elixir, run:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
# and generate HTML and EPUB documents:
make docs
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
## Licensing and Compliance Requirements
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
guidelines on licensing and compliance. Below is a summary of the key points
affecting **all external contributors**:
- Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
- SPDX License Headers: With the exception of approved test fixture files,
all new or modified files in a pull request must include correct SPDX
headers. If you are creating a new file under the `Apache-2.0` license, for
instance, please use:
* Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
* SPDX License Headers: With the exception of approved test fixture files,
all new or modified files in a pull request must include correct SPDX
headers. If you are creating a new file under the `Apache-2.0` license, for
instance, please use:
```elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
```
- No Executable Binaries: Contributions must **not** include any executable
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
- Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
- Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
- Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
```
* No Executable Binaries: Contributions must **not** include any executable
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
* Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
* Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
* Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
```text
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
@@ -171,27 +197,52 @@ affecting **all external contributors**:
involved.
```
See http://developercertificate.org/ for a copy of the Developer Certificate
See <https://developercertificate.org/> for a copy of the Developer Certificate
of Origin license.
## Building documentation
## Using AI and coding agents
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
While we allow the use of AI on contributions and discussions, please be mindful
when doing so. Generally speaking, Elixir maintainers already have access to AI
(like many other developers). Therefore, if we need the feedback or help of a
coding agent, we can request so ourselves. For this reason, we often find
the point of view of the human behind the agent more valuable.
```sh
# After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
```
That said, here are examples of how one might (or might not) use AI and coding
agents in Elixir spaces:
Now go back to Elixir's root directory and run:
* When it comes to discussions, using AI to help express yourself is welcome,
but avoid directly copy and pasting AI generated content. If there is a language
barrier, use AI to translate, review, and improve your text, but do not use AI
to respond on your behalf.
```sh
make docs # to generate HTML pages
make docs DOCS_FORMAT=epub # to generate EPUB documents
```
* Do not use coding agents to tackle existing issues unless they have the
"Contributions Welcome" label.
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
* If you request a feature on the mailing list and it is accepted, you may
use coding agents to implement it, as long as it follows the AI Contributions
guidelines below.
* When automating AI usage on the Elixir codebase for performance improvements
or security fixes, pair it with a separate set of agents whose job is to argue
against and try to invalidate any proposed change. And treat their approval as
advisory: a human must still validate it before opening issues or pull requests.
If any code is written by AI, then you must follow the guidelines below.
### AI contributions
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
Certificate of Origin (DCO). The human submitter is responsible for:
* Reviewing all AI-generated code
* Ensuring compliance with licensing requirements
* Adding their own Signed-off-by tag to certify the DCO
* Taking full responsibility for the contribution
* Disclosing use of AI for comments and code contributions
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
AI in the development process. Contributions should include an Assisted-by tag in the
following format:
Assisted-by: AGENT_NAME:MODEL_VERSION
+6 -4
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
@@ -107,8 +107,10 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
fi
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
$(Q) $(GENERATE_APP) $(VERSION)
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP)
$(Q) $(GENERATE_APP) $(VERSION)
unicode: $(UNICODE)
+10 -13
View File
@@ -20,7 +20,7 @@ ensuring that Elixir remains a trusted and innovative open source project.
## 2. Scope
This policy applies to the Elixir Programming language, located at
https://github.com/elixir-lang/elixir. It covers every file, and contribution
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution
made, including documentation and any associated assets.
## 3. Licensing
@@ -29,18 +29,19 @@ All code released by the Elixir team is licensed under the
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
licenses are recognized as permissible in this project:
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
These licenses are considered acceptable for any files or code that form part of
an Elixir repository. If a contribution requires a different license, it must
either be rejected or prompt an update to this policy.
## 4. Contributing to Elixir Projects
## 4. Contributing to the Elixir repository
Any code contributed to Elixir repositories must fall under one of the accepted
Any code contributed to the Elixir repository must fall under one of the accepted
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
other license will be rejected unless this policy is formally revised to include
that license. All files except those specifically exempted (e.g., certain test
@@ -51,13 +52,9 @@ configuration and undergo review.
Contributions must not introduce executable binary files into the codebase.
Every Elixir project within the organization will have an automated GitHub
Action to enforce these rules. This mechanism aids in detecting non-compliant
licenses or files early in the review process.
## 5. Preservation of Copyright and License Information
Any third-party code incorporated into Elixir projects must retain original
Any third-party code incorporated into the Elixir repository must retain original
copyright and license headers. If no such headers exist in the source, they must
be added. This practice ensures that original authors receive proper credit and
that the licensing lineage is preserved.
@@ -165,4 +162,4 @@ necessary, by the EEF CISO. Any significant changes will be communicated to
contributors and made publicly available.
*Effective Date: 2025-02-20*
*Last Reviewed: 2025-02-20*
*Last Reviewed: 2025-11-20*
+5 -4
View File
@@ -59,7 +59,7 @@ Our *actionable item policy* has some important consequences, such as:
comment and we can always reopen the issue.
By keeping the overall issues tracker tidy and organized, the community
can easily peak at what is coming in new releases and also get involved
can easily peek at what is coming in new releases and also get involved
by commenting on existing issues and submitting pull requests. Please
remember to keep the tone positive and be kind! For more information,
see the [Code of Conduct][1].
@@ -122,9 +122,10 @@ variable `ERL_COMPILER_OPTIONS=deterministic`.
Contributions to Elixir are always welcome! Before you get started, please check
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
guidelines on how to set up your environment, run the test suite, format your
code, and submit pull requests. We also include information on our review
process, licensing requirements, and helpful tips to ensure a smooth
contribution experience.
code, and submit pull requests.
Note you must disclose the use of coding agents and AI written code in your
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
## Development links
+4 -10
View File
@@ -8,15 +8,9 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
- If this release addresses any publicly known security vulnerabilities with
assigned CVEs, add a "Security" section to `CHANGELOG.md`. For example:
```md
## Security
- Fixed CVE-2025-00000: Description of the vulnerability
```
3. Update "Compatibility and Deprecations" if a new OTP version is supported
@@ -30,11 +24,11 @@
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (before first rc)
## Creating a new vMAJOR.MINOR branch (usually before first rc)
### In the new branch
1. Comment the `CANONICAL=` in /Makefile
1. Comment out `CANONICAL := main/` in /Makefile
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
@@ -42,7 +36,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Bump /VERSION file, bin/elixir, and bin/elixir.bat
2. Start new /CHANGELOG.md
+4 -5
View File
@@ -12,16 +12,15 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.19 | Development
1.18 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
## Announcements
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
+1 -1
View File
@@ -1 +1 @@
1.19.0-dev
1.20.0
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.0-dev
ELIXIR_VERSION=1.20.0
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.19.0-dev
set ELIXIR_VERSION=1.20.0
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+23 -6
View File
@@ -118,6 +118,19 @@ defmodule EEx do
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
| {:eof, metadata}
@type tokenize_opt ::
{:file, binary()}
| {:line, line}
| {:column, column}
| {:indentation, non_neg_integer}
| {:trim, boolean()}
@type compile_opt ::
tokenize_opt
| {:engine, module()}
| {:parser_options, Code.parser_opts()}
| {atom(), term()}
@doc """
Generates a function definition from the given string.
@@ -128,6 +141,7 @@ defmodule EEx do
template.
The supported `options` are described [in the module docs](#module-options).
Additional options are passed to the underlying engine.
## Examples
@@ -220,9 +234,11 @@ defmodule EEx do
"3"
"""
@spec compile_string(String.t(), keyword) :: Macro.t()
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
case tokenize(source, options) do
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
case tokenize(source, tokenize_opts) do
{:ok, tokens} ->
EEx.Compiler.compile(tokens, source, options)
@@ -259,7 +275,7 @@ defmodule EEx do
#=> "3"
"""
@spec compile_file(Path.t(), keyword) :: Macro.t()
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
def compile_file(filename, options \\ []) when is_list(options) do
filename = IO.chardata_to_string(filename)
options = Keyword.merge([file: filename, line: 1], options)
@@ -277,7 +293,7 @@ defmodule EEx do
"foo baz"
"""
@spec eval_string(String.t(), keyword, keyword) :: String.t()
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options)
@@ -299,7 +315,7 @@ defmodule EEx do
#=> "foo baz"
"""
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename)
@@ -339,7 +355,7 @@ defmodule EEx do
Note new tokens may be added in the future.
"""
@doc since: "1.14.0"
@spec tokenize([char()] | String.t(), opts :: keyword) ::
@spec tokenize([char()] | String.t(), [tokenize_opt]) ::
{:ok, [token()]} | {:error, String.t(), metadata()}
def tokenize(contents, opts \\ []) do
EEx.Compiler.tokenize(contents, opts)
@@ -348,6 +364,7 @@ defmodule EEx do
### Helpers
defp do_eval(compiled, bindings, options) do
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
{result, _} = Code.eval_quoted(compiled, bindings, options)
result
end
+27 -14
View File
@@ -96,7 +96,7 @@ defmodule EEx.Compiler do
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
"please remove \"#{marker}\""
:elixir_errors.erl_warn({line, column}, state.file, message)
IO.warn(message, file: state.file, line: line, column: column)
~c""
else
marker
@@ -303,7 +303,7 @@ defmodule EEx.Compiler do
file: file,
source: source,
line: line,
quoted: [],
quoted: %{},
parser_options: [indentation: indentation] ++ parser_options,
indentation: indentation
}
@@ -347,7 +347,7 @@ defmodule EEx.Compiler do
state.parser_options
expr = Code.string_to_quoted!(chars, options)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
buffer = handle_expr(buffer, mark, expr, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -366,17 +366,17 @@ defmodule EEx.Compiler do
rest,
state.engine.handle_begin(buffer),
[{contents, start_line, start_column} | scope],
%{state | quoted: [], line: line}
%{state | quoted: %{}, line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
buffer = handle_expr(buffer, mark, contents, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -410,7 +410,7 @@ defmodule EEx.Compiler do
) do
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
options = [file: state.file, line: line, column: column] ++ state.parser_options
tuples = Code.string_to_quoted!(wrapped, options)
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options)
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
end
@@ -426,7 +426,7 @@ defmodule EEx.Compiler do
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
message = "expected a closing '<% end %>' for block expression in EEx"
expr_meta = non_whitespace_meta(content, line, column, state)
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state)
syntax_error!(message, expr_meta, state)
end
@@ -443,10 +443,10 @@ defmodule EEx.Compiler do
defp wrap_expr(current, line, buffer, chars, state) do
new_lines = List.duplicate(?\n, line - state.line)
key = length(state.quoted)
placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
count = current ++ placeholder ++ new_lines ++ chars
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
key = map_size(state.quoted)
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"]
count = [current, placeholder, new_lines, chars]
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))}
{count, new_state}
end
@@ -479,8 +479,7 @@ defmodule EEx.Compiler do
# Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
{^key, value} = List.keyfind(quoted, key, 0)
value
Map.fetch!(quoted, key)
end
defp insert_quoted({left, line, right}, quoted) do
@@ -513,6 +512,20 @@ defmodule EEx.Compiler do
column: meta.column
end
defp handle_expr(buffer, mark, expr, meta, state) do
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
rescue
e in EEx.SyntaxError ->
reraise %{
e
| file: e.file || state.file,
line: e.line || meta.line,
column: e.column || meta.column,
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
},
__STACKTRACE__
end
defp code_snippet(source, indentation, meta) do
line_start = max(meta.line - 3, 1)
line_end = meta.line
+4
View File
@@ -17,6 +17,10 @@ defmodule EEx.Engine do
@doc """
Called at the beginning of every template.
It receives the options during compilation, including the
ones managed by EEx, such as `:line` and `:file`, as well
as custom engine options.
It must return the initial state.
"""
@callback init(opts :: keyword) :: state
+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)
+6 -11
View File
@@ -873,11 +873,6 @@ defmodule Access do
...> end)
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.filter(5)])
** (FunctionClauseError) no function clause matching in Access.filter/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
@@ -891,7 +886,7 @@ defmodule Access do
end
defp filter(:get, data, func, next) when is_list(data) do
data |> Enum.filter(func) |> Enum.map(next)
for elem <- data, func.(elem), do: next.(elem)
end
defp filter(:get_and_update, data, func, next) when is_list(data) do
@@ -1096,6 +1091,11 @@ defmodule Access do
defp normalize_range(range, _list), do: range
defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
when index > last do
{:lists.reverse(gets), :lists.reverse(updates, rest)}
end
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
if index in range do
case next.(head) do
@@ -1154,11 +1154,6 @@ defmodule Access do
...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
+19 -13
View File
@@ -507,7 +507,7 @@ defmodule Application do
of all loaded applications. Returns `nil` if
the module is not listed in any application spec.
"""
@spec get_application(atom) :: atom | nil
@spec get_application(module) :: app | nil
def get_application(module) when is_atom(module) do
case :application.get_application(module) do
{:ok, app} -> app
@@ -680,13 +680,13 @@ defmodule Application do
## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications.
Since Mix configurations are commonly used to configure applications, we will use
this as a point of illustration.
Since Mix configurations are commonly used to configure applications (including
your dependencies), we will use this as a point of illustration.
Consider a new application `:my_app`. `:my_app` contains a database engine which
supports a pool of databases. The database engine needs to know the configuration for
each of those databases, and that configuration is supplied by key-value pairs in
environment of `:my_app`.
environment of `:my_app`. For example, your `config/runtime.exs` file might have:
config :my_app, Databases.RepoOne,
# A database configuration
@@ -696,7 +696,7 @@ defmodule Application do
config :my_app, Databases.RepoTwo,
# Another database configuration (for the same OTP app)
ip: "localhost",
port: 20717
port: 20_717
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
@@ -714,6 +714,11 @@ defmodule Application do
config = Application.get_env(:my_app, Databases.RepoOne)
config[:ip]
The sample `config/runtime.exs` above could be used both for `:my_app` to
configure itself but also to allow any application that depends on `:my_app`
to configure how it works. However, one should keep in mind the caveats described
in the `Application` module documentation: the application environment is global
state which should be avoided if possible.
"""
@spec get_env(app, key, value) :: value
def get_env(app, key, default \\ nil) when is_atom(app) do
@@ -814,7 +819,7 @@ defmodule Application do
stick after the application is loaded and also on application reload.
"""
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
def put_env(app, key, value, opts \\ []) when is_atom(app) do
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do
maybe_warn_on_app_env_key(app, key)
:application.set_env(app, key, value, opts)
end
@@ -856,7 +861,7 @@ defmodule Application do
It receives the same options as `put_env/4`. Returns `:ok`.
"""
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
def delete_env(app, key, opts \\ []) when is_atom(app) do
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do
maybe_warn_on_app_env_key(app, key)
:application.unset_env(app, key, opts)
end
@@ -903,13 +908,13 @@ defmodule Application do
@doc """
Ensures the given `app` or `apps` and their child applications are started.
The second argument is either the `t:restart_type/1` (for consistency with
The second argument is either the `t:restart_type/0` (for consistency with
`start/2`) or a keyword list.
## Options
* `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
`:permanent`, or `:transient`. See `t:restart_type/0` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`).
@@ -921,11 +926,11 @@ defmodule Application do
{:ok, [app]} | {:error, term}
def ensure_all_started(app_or_apps, type_or_opts \\ [])
def ensure_all_started(app, type) when is_atom(type) do
ensure_all_started(app, type: type)
def ensure_all_started(app_or_apps, type) when is_atom(type) do
ensure_all_started(app_or_apps, type: type)
end
def ensure_all_started(app, opts) when is_atom(app) do
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do
ensure_all_started([app], opts)
end
@@ -1056,7 +1061,8 @@ defmodule Application do
Returns a list with information about the applications which are currently running.
"""
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
def started_applications(timeout \\ 5000) do
def started_applications(timeout \\ 5000)
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
:application.which_applications(timeout)
end
+278 -54
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)
+17 -1
View File
@@ -162,6 +162,22 @@ defmodule Calendar do
"""
@type time_zone_database :: module()
@typedoc """
Options for formatting dates and times with `strftime/3`.
"""
@type strftime_opts :: [
preferred_datetime: String.t(),
preferred_date: String.t(),
preferred_time: String.t(),
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_month_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_day_of_week_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
]
@doc """
Returns how many days there are in the given month of the given year.
"""
@@ -617,7 +633,7 @@ defmodule Calendar do
"""
@doc since: "1.11.0"
@spec strftime(map(), String.t(), keyword()) :: String.t()
@spec strftime(map(), String.t(), strftime_opts()) :: String.t()
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
parse(
+13 -8
View File
@@ -53,7 +53,7 @@ defmodule Date do
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
14716
iex> Date.add(~D[1970-01-01], 14716)
iex> Date.add(~D[1970-01-01], 14_716)
~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
@@ -321,7 +321,7 @@ defmodule Date do
@doc """
Converts the given date to a string according to its calendar.
### Examples
## Examples
iex> Date.to_string(~D[2000-02-28])
"2000-02-28"
@@ -399,7 +399,7 @@ defmodule Date do
or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will raise an `ArgumentError`.
### Examples
## Examples
iex> Date.to_iso8601(~D[2000-02-28])
"2000-02-28"
@@ -633,7 +633,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
@@ -667,7 +667,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
@@ -691,10 +691,15 @@ defmodule Date do
@doc """
Adds the number of days to the given `date`.
The days are counted as Gregorian days. The date is returned in the same
calendar as it was given in.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
>
> `add/2` always considers a day to be measured according to the
> `Calendar.ISO`.
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
The days are counted as Gregorian days, independent of the underlying
calendar. The date is returned in the same calendar as it was given in.
## Examples
+17 -8
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
@@ -95,7 +104,7 @@ defmodule Date.Range do
[date_from_iso_days(current, calendar)]
end
defp slice(current, step, remaining, calendar) do
defp slice(current, step, remaining, calendar) when remaining > 1 do
[
date_from_iso_days(current, calendar)
| slice(current + step, step, remaining - 1, calendar)
@@ -218,11 +227,11 @@ defmodule Date.Range do
defimpl Inspect do
import Kernel, except: [inspect: 2]
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
end
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
end
+44 -23
View File
@@ -1046,7 +1046,7 @@ defmodule DateTime do
its abbreviation, which means information is lost when converting to such
format.
### Examples
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1390,7 +1390,7 @@ defmodule DateTime do
custom (but relatively common) representation which appends the time
zone abbreviation and full name to the datetime.
### Examples
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1611,32 +1611,45 @@ defmodule DateTime do
@doc """
Adds a specified amount of time to a `DateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always considers the unit to be computed according
to the `Calendar.ISO`.
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
This function relies on a contiguous representation of time,
ignoring the wall time and timezone changes. For example, if you add
one day when there are summer time/daylight saving time changes,
it will also change the time forward or backward by one hour,
so the elapsed time is precisely 24 hours. Similarly, adding just
a few seconds to a datetime just before "spring forward" can cause
wall time to increase by more than an hour.
ignoring timezone changes. For example, if you add one day when there
are summer time/daylight saving time changes, it will also change the
time forward or backward by one hour, so the elapsed time is precisely
24 hours. Similarly, adding just a few seconds to a datetime just before
"spring forward" can cause wall time to increase by more than an hour.
While this means this function is precise in terms of elapsed time,
its result may be misleading in certain use cases. For example, if a
its result may be confusing in certain use cases. For example, if a
user requests a meeting to happen every day at 15:00 and you use this
function to compute all future meetings by adding day after day, this
function may change the meeting time to 14:00 or 16:00 if there are
changes to the current timezone. Computing of recurring datetimes is
not currently supported in Elixir's standard library but it is available
by third-party libraries.
changes to the current timezone.
### Examples
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can add to
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## Examples
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
@@ -1664,8 +1677,6 @@ defmodule DateTime do
iex> result.microsecond
{21000, 3}
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
"""
@doc since: "1.8.0"
@spec add(
@@ -1739,7 +1750,7 @@ defmodule DateTime do
to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime.
On the other hand, time zones that observe "Daylight Saving Time"
Consequently, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours
from the resulting datetime:
@@ -1751,12 +1762,22 @@ defmodule DateTime do
DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
Although the first example shows a difference of 2 hours when
comparing the wall clocks of the given datetime with the returned one,
due to the "spring forward" time jump, the actual elapsed time is
still exactly of 1 hour.
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## ISO calendar considerations
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
@@ -1922,7 +1943,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -1969,7 +1990,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
+19 -2
View File
@@ -161,6 +161,22 @@ defmodule Duration do
"""
@type duration :: t | [unit_pair]
@typedoc """
Options for `Duration.to_string/2`.
"""
@type to_string_opts :: [
units: [
year: String.t(),
month: String.t(),
week: String.t(),
day: String.t(),
hour: String.t(),
minute: String.t(),
second: String.t()
],
separator: String.t()
]
@microseconds_per_second 1_000_000
@doc """
@@ -216,7 +232,7 @@ defmodule Duration do
@doc """
Adds units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
Respects the highest microsecond precision of the two.
## Examples
@@ -246,7 +262,7 @@ defmodule Duration do
@doc """
Subtracts units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
Respects the highest microsecond precision of the two.
## Examples
@@ -436,6 +452,7 @@ defmodule Duration do
"""
@doc since: "1.18.0"
@spec to_string(t, to_string_opts) :: String.t()
def to_string(%Duration{} = duration, opts \\ []) do
units = Keyword.get(opts, :units, [])
separator = Keyword.get(opts, :separator, " ")
+84 -154
View File
@@ -182,7 +182,7 @@ defmodule Calendar.ISO do
@type day_of_year :: 1..366
@type quarter_of_year :: 1..4
@type year_of_era :: {1..10000, era}
@type year_of_era :: {1..10_000, era}
@seconds_per_minute 60
@seconds_per_hour 60 * 60
@@ -196,14 +196,28 @@ defmodule Calendar.ISO do
@ext_date_sep ?-
@ext_time_sep ?:
@days_per_nonleap_year 365
@days_per_leap_year 366
# The ISO epoch starts, in this implementation,
# with ~D[0000-01-01]. Era "1" starts
# on ~D[0001-01-01] which is 366 days later.
@iso_epoch 366
# Constants for date calculations using 400-year era cycles.
# The algorithm uses a March-based year where March 1 is day 0.
# Reference: Neri C, Schneider L. "Euclidean Affine Functions and
# their Application to Calendar Algorithms". Softw Pract Exper. 2022.
@days_per_year 365
@years_per_era 400
@days_per_era @years_per_era * @days_per_year + 97
@days_per_4_years 4 * @days_per_year
@days_per_100_years 100 * @days_per_year + 24
@march_1_offset 31 + 29
@unix_epoch_days 719_528
# Month calculation constants: in a March-based year, each 5-month
# cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
@days_per_5_months 153
@months_per_cycle 5
[match_basic_date, match_ext_date, guard_date, read_date] =
quote do
[
@@ -652,12 +666,12 @@ defmodule Calendar.ISO do
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
{{year, month, day}, {hour, minute, second, _}} =
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
{0, day_fraction} ->
{{year, month, day}, time_from_day_fraction(day_fraction)}
{extra_days, day_fraction} ->
base_days = date_to_iso_days(year, month, day)
base_days = valid_date_to_iso_days(year, month, day)
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
end
@@ -784,13 +798,13 @@ defmodule Calendar.ISO do
## Examples
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86_400}})
{0, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86_400}})
{2000, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43_200, 86_400}})
{2000, 1, 1, 12, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86400000000}})
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86_400_000_000}})
{-1, 1, 1, 0, 0, 0, {0, 6}}
"""
@@ -878,28 +892,52 @@ defmodule Calendar.ISO do
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_iso_days(0, 1, 1) do
0
end
def date_to_iso_days(1970, 1, 1) do
719_528
end
def date_to_iso_days(year, month, day) do
ensure_day_in_month!(year, month, day)
valid_date_to_iso_days(year, month, day)
end
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
1
defp valid_date_to_iso_days(0, 1, 1), do: 0
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
defp valid_date_to_iso_days(year, month, day) do
y = if month <= 2, do: year - 1, else: year
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
year_of_era = y - era * @years_per_era
month_prime = if month > 2, do: month - 3, else: month + 9
day_of_year = div(@days_per_5_months * month_prime + 2, @months_per_cycle) + day - 1
day_of_era =
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year
era * @days_per_era + day_of_era + @march_1_offset
end
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days(days) do
{year, day_of_year} = days_to_year(days)
extra_day = if leap_year?(year), do: 1, else: 0
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
{year, month, day_in_month + 1}
z = days - @march_1_offset
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era)
day_of_era = z - era * @days_per_era
year_of_era =
div(
day_of_era - div(day_of_era, @days_per_4_years) + div(day_of_era, @days_per_100_years) -
div(day_of_era, @days_per_era - 1),
@days_per_year
)
day_of_year =
day_of_era -
(@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100))
month_prime = div(@months_per_cycle * day_of_year + 2, @days_per_5_months)
day = day_of_year - div(@days_per_5_months * month_prime + 2, @months_per_cycle) + 1
month = if month_prime < 10, do: month_prime + 3, else: month_prime - 9
year = year_of_era + era * @years_per_era
year = if month <= 2, do: year + 1, else: year
{year, month, day}
end
defp div_rem(int1, int2) do
@@ -913,6 +951,9 @@ defmodule Calendar.ISO do
end
end
defp floor_div_positive_divisor(int1, int2) when int1 >= 0, do: div(int1, int2)
defp floor_div_positive_divisor(int1, int2), do: -div(-int1 - 1, int2) - 1
@doc """
Returns how many days there are in the given year-month.
@@ -1133,7 +1174,7 @@ defmodule Calendar.ISO do
"""
@doc since: "1.8.0"
@spec year_of_era(year) :: {1..10000, era}
@spec year_of_era(year) :: {1..10_000, era}
def year_of_era(year) when is_year_CE(year), do: {year, 1}
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
@@ -1159,7 +1200,7 @@ defmodule Calendar.ISO do
"""
@doc since: "1.13.0"
@impl true
@spec year_of_era(year, month, day) :: {1..10000, era}
@spec year_of_era(year, month, day) :: {1..10_000, era}
def year_of_era(year, _month, _day), do: year_of_era(year)
@doc """
@@ -1704,11 +1745,11 @@ defmodule Calendar.ISO do
## Examples
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86400000000}})
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86_400_000_000}})
{0, {0, 86400000000}}
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {43200000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {43_200_000_000, 86_400_000_000}})
{730485, {0, 86400000000}}
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {46800000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {46_800_000_000, 86_400_000_000}})
{730485, {0, 86400000000}}
"""
@@ -1724,11 +1765,11 @@ defmodule Calendar.ISO do
## Examples
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86400000000}})
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86_400_000_000}})
{0, {86399999999, 86400000000}}
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {43200000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {43_200_000_000, 86_400_000_000}})
{730485, {86399999999, 86400000000}}
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {46800000000, 86400000000}})
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {46_800_000_000, 86_400_000_000}})
{730485, {86399999999, 86400000000}}
"""
@@ -1837,7 +1878,7 @@ defmodule Calendar.ISO do
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
valid_date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
@@ -1848,7 +1889,7 @@ defmodule Calendar.ISO do
months_in_year = 12
total_months = year * months_in_year + month + months - 1
new_year = Integer.floor_div(total_months, months_in_year)
new_year = floor_div_positive_divisor(total_months, months_in_year)
new_month =
case rem(total_months, months_in_year) + 1 do
@@ -1888,7 +1929,7 @@ defmodule Calendar.ISO do
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
ppd = System.convert_time_unit(86400, :second, unit)
ppd = System.convert_time_unit(86_400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end
@@ -1937,7 +1978,7 @@ defmodule Calendar.ISO do
}) do
[
month: year * 12 + month,
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
second: week * 7 * 86_400 + day * 86_400 + hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
@@ -1971,7 +2012,7 @@ defmodule Calendar.ISO do
if total in @unix_range_microseconds do
microseconds = Integer.mod(total, @microseconds_per_second)
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
precision = precision_for_unit(unit)
{date, time} = iso_seconds_to_datetime(seconds)
{:ok, date, time, {microseconds, precision}}
@@ -2093,9 +2134,12 @@ defmodule Calendar.ISO do
end
end
# Note that this function does not add the extra leap day for a leap year.
# If you want to add that leap day when appropriate,
# add the result of leap_day_offset/2 to the result of days_before_month/1.
defp leap_day_offset(_year, month) when month < 3, do: 0
defp leap_day_offset(year, _month) do
if leap_year?(year), do: 1, else: 0
end
defp days_before_month(1), do: 0
defp days_before_month(2), do: 31
defp days_before_month(3), do: 59
@@ -2109,120 +2153,6 @@ defmodule Calendar.ISO do
defp days_before_month(11), do: 304
defp days_before_month(12), do: 334
defp leap_day_offset(_year, month) when month < 3, do: 0
defp leap_day_offset(year, _month) do
if leap_year?(year), do: 1, else: 0
end
defp days_to_year(days) when days < 0 do
year_estimate = -div(-days, @days_per_nonleap_year) - 1
{year, days_before_year} =
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
leap_year_pad = if leap_year?(year), do: 1, else: 0
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
end
defp days_to_year(days) do
year_estimate = div(days, @days_per_nonleap_year)
{year, days_before_year} =
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
{year, days - days_before_year}
end
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
end
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
end
defp days_to_year(year, _days1, days2) do
{year, days2}
end
defp days_to_end_of_epoch(year) when year < 0 do
previous_year = year + 1
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
previous_year * @days_per_nonleap_year
end
defp days_in_previous_years(0), do: 0
# A concise version of the algorithm would use floor_div instead of div.
# However, floor_div would check the operands on every operation.
# We optimize this by providing a positive and negative version of each algorithm.
defp days_in_previous_years(year) when year > 0 do
previous_year = year - 1
div(previous_year, 4) - div(previous_year, 100) +
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
defp days_in_previous_years(year) when year < 0 do
previous_year = year - 1
div(year, 4) - div(year, 100) +
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
# Note that 0 is the first day of the month.
defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
{1, day_of_year}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
{2, day_of_year - 31}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
{3, day_of_year - (59 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
{4, day_of_year - (90 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
{5, day_of_year - (120 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
{6, day_of_year - (151 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
{7, day_of_year - (181 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
{8, day_of_year - (212 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
{9, day_of_year - (243 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
{10, day_of_year - (273 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
{11, day_of_year - (304 + extra_day)}
end
defp year_day_to_year_date(extra_day, day_of_year) do
{12, day_of_year - (334 + extra_day)}
end
defp iso_seconds_to_datetime(seconds) do
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
+52 -41
View File
@@ -391,13 +391,20 @@ defmodule NaiveDateTime do
@doc """
Adds a specified amount of time to a `NaiveDateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
## Examples
@@ -447,8 +454,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(dt, 21, :second)
~N[2000-02-29 23:00:28]
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
"""
@doc since: "1.4.0"
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
@@ -710,16 +715,18 @@ defmodule NaiveDateTime do
"""
@spec to_date(Calendar.naive_datetime()) :: Date.t()
def to_date(%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
}) do
def to_date(
%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
} = _naive_datetime
) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@@ -736,16 +743,18 @@ defmodule NaiveDateTime do
"""
@spec to_time(Calendar.naive_datetime()) :: Time.t()
def to_time(%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
def to_time(
%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = _naive_datetime
) do
%Time{
hour: hour,
minute: minute,
@@ -761,7 +770,7 @@ defmodule NaiveDateTime do
For readability, this function follows the RFC3339 suggestion of removing
the "T" separator between the date and time components.
### Examples
## Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
"2000-02-28 23:00:13"
@@ -908,7 +917,7 @@ defmodule NaiveDateTime do
Only supports converting naive datetimes which are in the ISO calendar,
attempting to convert naive datetimes from other calendars will raise.
### Examples
## Examples
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
"2000-02-28T23:00:13"
@@ -1141,16 +1150,18 @@ defmodule NaiveDateTime do
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}) do
def to_gregorian_seconds(
%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
} = _naive_datetime
) do
{days, day_fraction} =
calendar.naive_datetime_to_iso_days(
year,
@@ -1261,7 +1272,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1327,7 +1338,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
+14 -11
View File
@@ -225,7 +225,7 @@ defmodule Time do
@doc """
Converts the given `time` to a string.
### Examples
## Examples
iex> Time.to_string(~T[23:00:00])
"23:00:00"
@@ -334,7 +334,7 @@ defmodule Time do
format, for human readability. It also supports the "basic" format through
passing the `:basic` option.
### Examples
## Examples
iex> Time.to_iso8601(~T[23:00:13])
"23:00:13"
@@ -505,13 +505,18 @@ defmodule Time do
@doc """
Adds the `amount_to_add` of `unit`s to the given `time`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` always considers the unit to be computed according to
> the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
`t:System.time_unit/0` for convenience but ultimately they are
all converted to microseconds. Negative values will move backwards
in time and the default precision is `:second`.
Note the result value represents the time of day, meaning that it is cyclic,
for instance, it will never go over 24 hours for the ISO calendar.
@@ -549,8 +554,6 @@ defmodule Time do
iex> result.microsecond
{21000, 3}
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -781,7 +784,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
@@ -837,7 +840,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
+173 -73
View File
@@ -248,6 +248,64 @@ defmodule Code do
"""
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for code formatting functions.
"""
@type format_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:line_length, pos_integer()}
| {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()}
| {:migrate, boolean()}
| {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()}
| {:migrate_unless, boolean()}
| {atom(), term()}
@typedoc """
Options for `quoted_to_algebra/2`.
"""
@type quoted_to_algebra_opt ::
{:line, pos_integer() | nil}
| {:escape, boolean()}
| {:locals_without_parens, keyword()}
| {:comments, [term()]}
@typedoc """
Options for parsing functions that convert strings to quoted expressions.
"""
@type parser_opts :: [
file: binary(),
line: pos_integer(),
column: pos_integer(),
indentation: non_neg_integer(),
columns: boolean(),
unescape: boolean(),
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
emit_warnings: boolean()
]
@typedoc """
Options for evaluation environment, accepted by `env_for_eval/1`.
"""
@type env_eval_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:module, module()}
@typedoc """
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
and `eval_quoted_with_env/4`.
"""
@type eval_opt ::
{:prune_binding, boolean()}
| {:dbg_callback, {module(), atom(), list()}}
@boolean_compiler_options [
:docs,
:debug_info,
@@ -260,7 +318,12 @@ defmodule Code do
@available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
[
:on_undefined_variable,
:infer_signatures,
:no_warn_undefined,
:module_definition
]
@doc """
Lists all required files.
@@ -516,9 +579,11 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`. Additionally, you may
also pass an environment as second argument, so the evaluation happens
within that environment.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
as third argument, so the evaluation happens within that environment.
## Return
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -548,11 +613,11 @@ defmodule Code do
iex> Enum.sort(binding)
[a: 3, b: 2]
For convenience, you can pass `__ENV__/0` as the `opts` argument and
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
all imports, requires and aliases defined in the current environment
will be automatically carried over:
iex> require Integer
iex> require Integer, warn: false
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
iex> result
3
@@ -560,21 +625,28 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_string(string, binding \\ [], opts_or_env \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do
validated_eval_string(string, binding, env)
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
end
def eval_string(string, binding, opts) when is_list(opts) do
validated_eval_string(string, binding, opts)
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
end
defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
defp validate_binding(binding) when is_list(binding), do: binding
defp validate_binding(binding) do
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
end
defp validated_eval_string(string, binding, env, opts) do
%{line: line, file: file} = env
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
{value, binding}
end
@@ -615,7 +687,8 @@ defmodule Code do
"""
@doc since: "1.15.0"
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
@spec with_diagnostics([log: boolean()], (-> result)) ::
{result, [diagnostic(:warning | :error)]}
when result: term()
def with_diagnostics(opts \\ [], fun) do
value = :erlang.get(:elixir_code_diagnostics)
@@ -648,7 +721,7 @@ defmodule Code do
Defaults to `true`.
"""
@doc since: "1.15.0"
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
@spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok
def print_diagnostic(diagnostic, opts \\ []) do
read_snippet? = Keyword.get(opts, :snippet, true)
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
@@ -672,7 +745,7 @@ defmodule Code do
* `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting
the document. Defaults to 98. This value indicates when an expression
the document. Defaults to `98`. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information
@@ -1035,9 +1108,9 @@ defmodule Code do
address the deprecation warnings.
"""
@doc since: "1.6.0"
@spec format_string!(binary, keyword) :: iodata
@spec format_string!(binary, [format_opt]) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
line_length = Keyword.get(opts, :line_length, 98)
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
to_quoted_opts =
[
@@ -1060,7 +1133,7 @@ defmodule Code do
available options.
"""
@doc since: "1.6.0"
@spec format_file!(binary, keyword) :: iodata
@spec format_file!(binary, [format_opt]) :: iodata
def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
string = File.read!(file)
formatted = format_string!(string, [file: file, line: 1] ++ opts)
@@ -1076,7 +1149,8 @@ defmodule Code do
returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of arguments and return types.
The options are described under `env_for_eval/1`.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`.
## Examples
@@ -1098,11 +1172,20 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
end
def eval_quoted(quoted, binding, opts) when is_list(opts) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
end
defp eval_quoted(quoted, binding, env, opts) do
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
{value, binding}
end
@@ -1129,8 +1212,10 @@ defmodule Code do
* `:line` - the line on which the script starts
* `:module` - the module to run the environment on
"""
@doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """
@@ -1150,9 +1235,13 @@ defmodule Code do
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
used in the evaluated code. It must be a `{module, function, args}`
tuple, see `dbg/2` for more details.
"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
@@ -1171,14 +1260,14 @@ defmodule Code do
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:column` - (since v1.11.0) the starting column of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:indentation` - (since v1.19.0) the indentation for the string being parsed.
This is useful when the code parsed is embedded within another document.
Defaults to 0.
Defaults to `0`.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1230,7 +1319,7 @@ defmodule Code do
and keyword lists.
The encoder function will receive the atom name (as a binary) and a
keyword list with the current file, line and column. It must return
keyword list with the current line and column. It must return
`{:ok, token :: term} | {:error, reason :: binary}`.
The encoder function is supposed to create an atom from the given
@@ -1263,20 +1352,13 @@ defmodule Code do
{:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
"""
@spec string_to_quoted(List.Chars.t(), keyword) ::
@spec string_to_quoted(List.Chars.t(), parser_opts) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def string_to_quoted(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
{:ok, tokens} ->
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, _error_msg} = error ->
error
end
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
end
@doc """
@@ -1290,7 +1372,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information.
"""
@spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
@spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t()
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1341,7 +1423,7 @@ defmodule Code do
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
@spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) ::
{:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
charlist = to_charlist(string)
@@ -1352,8 +1434,7 @@ defmodule Code do
Process.put(:code_formatter_comments, [])
opts = [preserve_comments: &preserve_comments/5] ++ opts
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, opts) do
with {:ok, forms} <- :elixir.string_to_quoted(charlist, line, column, file, opts) do
comments = Enum.reverse(Process.get(:code_formatter_comments))
{:ok, forms, comments}
end
@@ -1371,7 +1452,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information.
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
@spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())}
def string_to_quoted_with_comments!(string, opts \\ []) do
charlist = to_charlist(string)
@@ -1456,6 +1537,9 @@ defmodule Code do
## Options
This function accepts all options supported by `format_string!/2` for controlling
code formatting, plus these additional options:
* `:comments` - the list of comments associated with the quoted expression.
Defaults to `[]`. It is recommended that both `:token_metadata` and
`:literal_encoder` options are given to `string_to_quoted_with_comments/2`
@@ -1466,17 +1550,13 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`.
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
The arity may be the atom `:*`, which implies all arities of
that name. The formatter already includes a list of functions
and this option augments this list.
* `:syntax_colors` - a keyword list of colors the output is colorized.
See `Inspect.Opts` for more information.
See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
"""
@doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
Inspect.Algebra.t()
def quoted_to_algebra(quoted, opts \\ []) do
quoted
|> Code.Normalizer.normalize(opts)
@@ -1655,9 +1735,6 @@ defmodule Code do
Available options are:
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled
module. This option can also be overridden per module using the `@compile`
directive. Defaults to `true`.
@@ -1673,6 +1750,9 @@ defmodule Code do
via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files.
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added.
Defaults to `false`.
@@ -1684,24 +1764,37 @@ defmodule Code do
should be using during type inference. When `false`, it disables module-local
signature inference used when type checking remote calls to the compiled
module. Type checking will be executed regardless of the value of this option.
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
Mix projects will set this option to your dependencies list in dev/prod, and
it will disable this option during test (as there is typically no need to infer
signature for test files). Outside of Mix projects, it defaults to `[:elixir]`.
When setting this option, we recommend running `mix clean` so the current module
may be compiled from scratch. `mix test` automatically disables this option via
the `:test_elixirc_options` project configuration, as there is typically no need
to infer signatures for test files.
* `:module_definition` (since v1.20.0) - stores if the module definition should
be `:compiled` (the default) or `:interpreted`. Note this does not affect the
`.beam` file written to disk, only how the contents inside `defmodule` are
executed. Using the `:interpreted` mode may offer better compilation times for
large projects, especially on machines with high core count, however, it comes
with some downsides:
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* Errors during compilation may have less precise stacktraces
* Anonymous functions within `defmodule` can have only up to 20 arguments.
If this is an issue, you can use maps or tuples to group the data.
Note the functions themselves inside `defmodule`, such as the ones defined
inside `def` and friends, can still have up to 255 arguments
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
tuples that will not emit warnings that the module or function does not exist
at compilation time. Pass atom `:all` to skip warning for all undefined
functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
compilation. See the module docs for more information. Defaults to `[]`.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
When `:raise` (the default), undefined variables will trigger a compilation
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
* `:parser_options` (since v1.10.0) - a keyword list of options to be given
to the parser when compiling files. It accepts the same options as
@@ -1712,14 +1805,12 @@ defmodule Code do
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
latter is used for other purposes beyond compilation.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
When `:raise` (the default), undefined variables will trigger a compilation
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
compilation. See the module docs for more information. Defaults to `[]`.
It always returns `:ok`. Raises an error for invalid options.
@@ -1759,6 +1850,15 @@ defmodule Code do
:ok
end
def put_compiler_option(:module_definition, value) do
if value not in [:interpreted, :compiled] do
raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}"
end
:elixir_config.put(:module_definition, value)
:ok
end
def put_compiler_option(:infer_signatures, value) do
value =
cond do
+1
View File
@@ -158,6 +158,7 @@ defmodule Code.Formatter do
@doc """
Converts the quoted expression into an algebra document.
"""
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
def to_algebra(quoted, opts \\ []) do
comments = Keyword.get(opts, :comments, [])
+74 -13
View File
@@ -11,6 +11,26 @@ defmodule Code.Fragment do
@type position :: {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for cursor context functions.
Currently, these options are not used but reserved for future extensibility.
"""
@type cursor_opts :: []
@typedoc """
Options for converting code fragments to quoted expressions.
"""
@type container_cursor_to_quoted_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
columns: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
trailing_fragment: String.t()
]
@doc ~S"""
Returns the list of lines in the given string, preserving their line endings.
@@ -172,7 +192,7 @@ defmodule Code.Fragment do
references, and more.
"""
@doc since: "1.13.0"
@spec cursor_context(List.Chars.t(), keyword()) ::
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:block_keyword_or_binary_operator, charlist}
@@ -282,7 +302,8 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
{_, _} -> {:none, 0}
{{:sigil, _}, _} -> {:none, 0}
{_, _} -> {{:operator, ~c"/"}, 1}
end
end
@@ -315,7 +336,7 @@ defmodule Code.Fragment do
end
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
@@ -662,7 +683,7 @@ defmodule Code.Fragment do
of examples and their return values.
"""
@doc since: "1.13.0"
@spec surround_context(List.Chars.t(), position(), keyword()) ::
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
%{begin: position, end: position, context: context} | :none
when context:
{:alias, charlist}
@@ -771,6 +792,12 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, offset} ->
build_surround({:local_or_var, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
build_surround({:keyword, acc}, reversed, line, offset)
{{:module_attribute, ~c""}, offset} ->
build_surround({:operator, ~c"@"}, reversed, line, offset)
@@ -1187,10 +1214,10 @@ defmodule Code.Fragment do
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:column` - the starting column of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1207,14 +1234,43 @@ defmodule Code.Fragment do
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
(see "Tracking sigils" section below)
## Tracking sigils
The `:preserve_sigils` option can be used to track cursor positions inside
a sigil.
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
as the second argument:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
In case the sigil is completed and has zero or more modifiers, the cursor will
be nested in the list, with all previous delimiters specified:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
If the cursor is after the sigil, then it is discarded as everything else:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
{:ok, {:__cursor__, [line: 1], []}}
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1234,7 +1290,10 @@ defmodule Code.Fragment do
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
@@ -1242,7 +1301,7 @@ defmodule Code.Fragment do
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
@@ -1261,10 +1320,12 @@ defmodule Code.Fragment do
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
:elixir.tokens_to_quoted(tokens, file, opts)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
{:error, :elixir_tokenizer.format_error(info)}
end
end
@@ -1305,7 +1366,7 @@ defmodule Code.Fragment do
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], 0), do: []
defp drop_tokens([], _counter), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
+17 -9
View File
@@ -14,6 +14,7 @@ defmodule Code.Normalizer do
Wraps literals in the quoted expression to conform to the AST format expected
by the formatter.
"""
@spec normalize(Macro.t(), keyword()) :: Macro.t()
def normalize(quoted, opts \\ []) do
line = Keyword.get(opts, :line, nil)
escape = Keyword.get(opts, :escape, true)
@@ -67,7 +68,7 @@ defmodule Code.Normalizer do
# Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state)
normalize_bitstring(quoted, state, false)
end
# Atoms with interpolations
@@ -88,13 +89,7 @@ defmodule Code.Normalizer do
normalize_literal(:utf8, [], state)
end
string =
if state.escape do
normalize_bitstring(string, state, true)
else
normalize_bitstring(string, state)
end
string = normalize_bitstring(string, state, state.escape)
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end
@@ -117,6 +112,7 @@ defmodule Code.Normalizer do
end
end)
parts = maybe_add_trailing_newline(call_meta, parts, state)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else
normalize_call(quoted, state)
@@ -404,7 +400,8 @@ defmodule Code.Normalizer do
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
parts = maybe_add_trailing_newline(meta, parts, state)
meta = patch_meta_line(meta, state.parent_meta)
parts =
@@ -426,6 +423,17 @@ defmodule Code.Normalizer do
{:<<>>, meta, parts}
end
defp maybe_add_trailing_newline(meta, parts, state) do
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
last = List.last(parts),
true <- is_binary(last) and not String.ends_with?(last, "\n") do
[_last | rest] = Enum.reverse(parts)
Enum.reverse([last <> "\n" | rest])
else
_ -> parts
end
end
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
Enum.map(parts, fn
{:"::", interpolation_meta,
+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
+20 -8
View File
@@ -98,6 +98,12 @@ defmodule Config do
(assembled with `mix release`).
"""
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config}
@imports_key {__MODULE__, :imports}
@@ -306,7 +312,7 @@ defmodule Config do
end
@doc false
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
env = Keyword.get(opts, :env)
target = Keyword.get(opts, :target)
@@ -371,21 +377,27 @@ defmodule Config do
end
end
defp validate!(config, file) do
Enum.all?(config, fn
defp validate!(config, file) when is_list(config) do
Enum.each(config, fn
{app, value} when is_atom(app) ->
if Keyword.keyword?(value) do
true
else
if not Keyword.keyword?(value) do
raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}"
end
_ ->
false
other ->
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
end)
config
end
defp validate!(config, file) do
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got: #{inspect(config)}"
end
end
+11
View File
@@ -111,6 +111,16 @@ defmodule Config.Provider do
"""
@type config_path :: {:system, binary(), binary()} | binary()
@typedoc """
Options for `init/3`.
"""
@type init_opts :: [
extra_config: config(),
prune_runtime_sys_config_after_boot: boolean(),
reboot_system_after_config: boolean(),
validate_compile_env: [{atom(), [atom()], term()}]
]
@doc """
Invoked when initializing a config provider.
@@ -196,6 +206,7 @@ defmodule Config.Provider do
@reboot_mode_key :config_provider_reboot_mode
@doc false
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
validate_config_path!(config_path)
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
+9 -3
View File
@@ -46,6 +46,12 @@ defmodule Config.Reader do
@behaviour Config.Provider
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@impl true
def init(opts) when is_list(opts) do
{path, opts} = Keyword.pop!(opts, :path)
@@ -68,7 +74,7 @@ defmodule Config.Reader do
Accepts the same options as `read!/2`.
"""
@doc since: "1.11.0"
@spec eval!(Path.t(), binary, keyword) :: keyword
@spec eval!(Path.t(), binary, config_opts) :: keyword
def eval!(file, contents, opts \\ [])
when is_binary(file) and is_binary(contents) and is_list(opts) do
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
@@ -90,7 +96,7 @@ defmodule Config.Reader do
"""
@doc since: "1.9.0"
@spec read!(Path.t(), keyword) :: keyword
@spec read!(Path.t(), config_opts) :: keyword
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
@@ -104,7 +110,7 @@ defmodule Config.Reader do
option cannot be disabled in `read_imports!/2`.
"""
@doc since: "1.9.0"
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
if opts[:imports] == :disabled do
raise ArgumentError, ":imports must be a list of paths"
+21 -68
View File
@@ -16,7 +16,7 @@ defmodule DynamicSupervisor do
## Examples
A dynamic supervisor is started with no children and often a name:
A dynamic supervisor is started with no children and often with a name:
children = [
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
@@ -137,67 +137,6 @@ defmodule DynamicSupervisor do
A supervisor is bound to the same name registration rules as a `GenServer`.
Read more about these rules in the documentation for `GenServer`.
## Migrating from Supervisor's :simple_one_for_one
In case you were using the deprecated `:simple_one_for_one` strategy from
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
few steps.
Imagine the given "old" code:
defmodule MySupervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
Supervisor.start_child(__MODULE__, [foo, bar, baz])
end
@impl true
def init(init_arg) do
children = [
# Or the deprecated: worker(MyWorker, [init_arg])
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
]
Supervisor.init(children, strategy: :simple_one_for_one)
end
end
It can be upgraded to the DynamicSupervisor like this:
defmodule MySupervisor do
use DynamicSupervisor
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# If MyWorker is not using the new child specs, we need to pass a map:
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
extra_arguments: [init_arg]
)
end
end
The difference is that the `DynamicSupervisor` expects the child specification
at the moment `start_child/2` is called, and no longer on the init callback.
If there are any initial arguments given on initialization, such as `[initial_arg]`,
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
"""
@behaviour GenServer
@@ -233,9 +172,10 @@ defmodule DynamicSupervisor do
@typedoc """
Return values of `start_child` functions.
Unlike `Supervisor`, this module ignores the child spec ids, so
`{:error, {:already_started, pid}}` is not returned for child specs given with the same id.
`{:error, {:already_started, pid}}` is returned however if a duplicate name is used when using
Unlike `Supervisor`, this module ignores the child spec ids,
so `{:error, {:already_started, pid}}` is not returned for child specs
given with the same id. `{:error, {:already_started, pid}}` is returned
however if a duplicate name is used when using
[name registration](`m:GenServer#module-name-registration`).
"""
@type on_start_child ::
@@ -266,6 +206,7 @@ defmodule DynamicSupervisor do
See `Supervisor` for more information about child specifications.
"""
@doc since: "1.6.1"
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
def child_spec(options) when is_list(options) do
id =
case Keyword.get(options, :name, DynamicSupervisor) do
@@ -415,6 +356,10 @@ defmodule DynamicSupervisor do
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
used when using [name registration](`m:GenServer#module-name-registration`).
This function will block the `DynamicSupervisor` until the child initializes.
When starting too many processes dynamically, you may want to use a
`PartitionSupervisor` to split the work across multiple processes.
If the child process start function returns `{:ok, child}` or `{:ok, child,
info}`, then child specification and PID are added to the supervisor and
this function returns the same value.
@@ -518,6 +463,14 @@ defmodule DynamicSupervisor do
@doc """
Terminates the given child identified by `pid`.
This function will block the `DynamicSupervisor` until the child
terminates, which may take an arbitrary amount of time if the child
is trapping exits and implements its own terminate callback.
For this reason, it is often better to ask the child process
itself to terminate, often by declaring in its child spec it has
a restart strategy of `:transient` (or `:temporary`) and then
sending it a message to stop with reason `:shutdown`.
If successful, this function returns `:ok`. If there is no process with
the given PID, this function returns `{:error, :not_found}`.
"""
@@ -528,11 +481,11 @@ defmodule DynamicSupervisor do
end
@doc """
Returns a list with information about all children.
Returns a list with information about all children of the given supervisor.
Note that calling this function when supervising a large number
of children under low memory conditions can cause an out of memory
exception.
of children under low memory conditions can bring the system down due to an
out of memory error.
This function returns a list of tuples containing:
+180 -61
View File
@@ -39,6 +39,20 @@ defprotocol Enumerable do
`reduce/3` function. All other functions exist as optimizations paths
for data structures that can implement certain properties in better
than linear time.
## Default implementation for lists
Sometimes you may want to implement this protocol for a list contained
in struct. This can be done by delegating to the `Enumerable.List` module
in the `reduce/3` implementation and providing a straight-forward
implementation for the remaining ones:
defimpl Enumerable, for: CustomStruct do
def count(struct), do: {:ok, length(struct.items)}
def member?(struct, value), do: {:ok, value in struct.items}
def slice(struct), do: {:error, __MODULE__}
def reduce(struct, acc, fun), do: Enumerable.List.reduce(struct.items, acc, fun)
end
"""
@typedoc """
@@ -766,6 +780,10 @@ defmodule Enum do
end
end
def count_until(_enumerable, limit) when is_integer(limit) do
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
end
@doc """
Counts the elements in the enumerable for which `fun` returns a truthy value, stopping at `limit`.
@@ -787,6 +805,10 @@ defmodule Enum do
end
end
def count_until(_enumerable, _fun, limit) when is_integer(limit) do
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
end
@doc """
Enumerates the `enumerable`, returning a list where all consecutive
duplicate elements are collapsed to a single element.
@@ -807,7 +829,7 @@ defmodule Enum do
"""
@spec dedup(t) :: list
def dedup(enumerable) when is_list(enumerable) do
dedup_list(enumerable, []) |> :lists.reverse()
dedup_list(enumerable)
end
def dedup(enumerable) do
@@ -951,8 +973,8 @@ defmodule Enum do
## Examples
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
"some"
"example"
some
example
#=> :ok
"""
@@ -1214,7 +1236,7 @@ defmodule Enum do
end
@doc """
Maps the given `fun` over `enumerable` and flattens the result.
Maps the given `fun` over `enumerable` and flattens the result only one level deep.
This function returns a new enumerable built by appending the result of invoking `fun`
on each element of `enumerable` together; conceptually, this is similar to a
@@ -1231,7 +1253,7 @@ defmodule Enum do
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
[[:a], [:b], [:c]]
This is frequently used to to transform and filter in one pass, returning empty
This is frequently used to transform and filter in one pass, returning empty
lists to exclude results:
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
@@ -1262,13 +1284,16 @@ defmodule Enum do
defp flat_reverse([], acc), do: acc
@doc """
Maps and reduces an `enumerable`, flattening the given results (only one level deep).
Maps and reduces an `enumerable`, flattening the results only one level deep.
It expects an accumulator and a function that receives each enumerable
element, and must return a tuple containing a new enumerable (often a list)
with the new accumulator or a tuple with `:halt` as first element and
the accumulator as second.
Returns a 2-element tuple where the first element is the results flattened one level deep and
the second element is the last accumulator.
## Examples
iex> enumerable = 1..100
@@ -1493,6 +1518,14 @@ defmodule Enum do
to_list(enumerable)
end
def into(enumerable, collectable) when is_struct(collectable, MapSet) do
if MapSet.size(collectable) == 0 do
MapSet.new(enumerable)
else
MapSet.new(enumerable) |> MapSet.union(collectable)
end
end
def into(%_{} = enumerable, collectable) do
into_protocol(enumerable, collectable)
end
@@ -1569,8 +1602,12 @@ defmodule Enum do
map(enumerable, transform)
end
def into(%_{} = enumerable, collectable, transform) do
into_protocol(enumerable, collectable, transform)
def into(enumerable, collectable, transform) when is_struct(collectable, MapSet) do
if MapSet.size(collectable) == 0 do
MapSet.new(enumerable, transform)
else
MapSet.new(enumerable, transform) |> MapSet.union(collectable)
end
end
def into(enumerable, %_{} = collectable, transform) do
@@ -1842,7 +1879,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` according
to Erlang's term ordering.
By default, the comparison is done with the `>=` sorter function.
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
If multiple elements are considered maximal, the first one that
was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true
@@ -1909,7 +1946,7 @@ defmodule Enum do
Returns the maximal element in the `enumerable` as calculated
by the given `fun`.
By default, the comparison is done with the `>=` sorter function.
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
If multiple elements are considered maximal, the first one that
was found is returned. If you want the last element considered
maximal to be returned, the sorter function should not return true
@@ -1986,11 +2023,16 @@ defmodule Enum do
operators work by using this function.
"""
@spec member?(t, element) :: boolean
def member?(enumerable, element) when is_list(enumerable) do
def member?(enumerable, element) do
__in__(element, enumerable)
end
@doc false
def __in__(element, enumerable) when is_list(enumerable) do
:lists.member(element, enumerable)
end
def member?(enumerable, element) do
def __in__(element, enumerable) do
case Enumerable.member?(enumerable, element) do
{:ok, element} when is_boolean(element) ->
element
@@ -2022,7 +2064,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` according
to Erlang's term ordering.
By default, the comparison is done with the `<=` sorter function.
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
If multiple elements are considered minimal, the first one that
was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true
@@ -2089,7 +2131,7 @@ defmodule Enum do
Returns the minimal element in the `enumerable` as calculated
by the given `fun`.
By default, the comparison is done with the `<=` sorter function.
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
If multiple elements are considered minimal, the first one that
was found is returned. If you want the last element considered
minimal to be returned, the sorter function should not return true
@@ -2143,28 +2185,60 @@ defmodule Enum do
@doc """
Returns a tuple with the minimal and the maximal elements in the
enumerable according to Erlang's term ordering.
enumerable.
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
Calls the provided `empty_fallback` function and returns its value if
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
By default, the comparison is done with the [`<`](`</2`) sorter function,
as the function must not return true for equal elements.
## Examples
iex> Enum.min_max([2, 3, 1])
{1, 3}
iex> Enum.min_max(["foo", "bar", "baz"])
{"bar", "foo"}
iex> Enum.min_max([], fn -> {nil, nil} end)
{nil, nil}
"""
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
when empty_result: any
def min_max(enumerable, empty_fallback \\ fn -> raise Enum.EmptyError end)
The fact this function uses Erlang's term ordering means that the
comparison is structural and not semantic. Therefore, if you want
to compare structs, most structs provide a "compare" function, such as
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
sorting function, Elixir will automatically use the `compare/2` function
of said module:
def min_max(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
iex> dates = [
...> ~D[2019-01-01],
...> ~D[2020-01-01],
...> ~D[2018-01-01]
...> ]
iex> Enum.min_max(dates, Date)
{~D[2018-01-01], ~D[2020-01-01]}
You can also pass a custom sorting function:
iex> Enum.min_max([2, 3, 1], &>/2)
{3, 1}
Finally, if you don't want to raise on empty enumerables, you can pass
the empty fallback:
iex> Enum.min_max([], fn -> nil end)
nil
"""
@spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
{element, element} | empty_result
when empty_result: any
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
def min_max(first..last//step = range, empty_fallback)
when is_function(empty_fallback, 0) do
case Range.size(range) do
0 ->
empty_fallback.()
@@ -2175,11 +2249,39 @@ defmodule Enum do
end
end
def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
def min_max(enumerable, empty_fallback)
when is_function(empty_fallback, 0) do
min_max(enumerable, &</2, empty_fallback)
end
def min_max(enumerable, sorter) when is_atom(sorter) do
min_max(enumerable, min_max_sort_fun(sorter))
end
def min_max(enumerable, sorter) when is_function(sorter, 2) do
min_max(enumerable, sorter, fn -> raise Enum.EmptyError end)
end
def min_max(enumerable, sorter, empty_fallback)
when is_atom(sorter) and is_function(empty_fallback, 0) do
min_max(enumerable, min_max_sort_fun(sorter), empty_fallback)
end
def min_max(enumerable, sorter, empty_fallback)
when is_function(sorter, 2) and is_function(empty_fallback, 0) do
first_fun = &[&1 | &1]
reduce_fun = fn entry, [min | max] ->
[Kernel.min(min, entry) | Kernel.max(max, entry)]
reduce_fun = fn entry, [min | max] = acc ->
cond do
sorter.(entry, min) ->
[entry | max]
sorter.(max, entry) ->
[min | entry]
true ->
acc
end
end
case reduce_by(enumerable, first_fun, reduce_fun) do
@@ -2200,8 +2302,8 @@ defmodule Enum do
Returns a tuple with the minimal and the maximal elements in the
enumerable as calculated by the given function.
If multiple elements are considered maximal or minimal, the first one
that was found is returned.
By default, the comparison is done with the [`<`](`</2`) sorter function,
as the function must not return `true` for equal elements.
## Examples
@@ -2259,7 +2361,7 @@ defmodule Enum do
def min_max_by(enumerable, fun, sorter, empty_fallback)
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback)
end
def min_max_by(enumerable, fun, sorter, empty_fallback)
@@ -2290,7 +2392,7 @@ defmodule Enum do
end
end
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
@doc """
Splits the `enumerable` in two lists according to the given function `fun`.
@@ -3611,9 +3713,14 @@ defmodule Enum do
end
def take(enumerable, amount) when is_integer(amount) and amount < 0 do
{count, fun} = slice_count_and_fun(enumerable, 1)
first = Kernel.max(amount + count, 0)
fun.(first, count - first, 1)
case slice_count_and_fun(enumerable, 1) do
{0, _fun} ->
[]
{count, fun} ->
first = Kernel.max(amount + count, 0)
fun.(first, count - first, 1)
end
end
@doc """
@@ -4089,6 +4196,11 @@ defmodule Enum do
iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end)
[4, 6]
`zip_with/2` can be used to transpose lists of lists:
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
[[1, 3], [2, 4]]
"""
@doc since: "1.12.0"
@spec zip_with(t, ([term] -> term)) :: [term]
@@ -4400,19 +4512,9 @@ defmodule Enum do
# dedup
defp dedup_list([value | tail], acc) do
acc =
case acc do
[^value | _] -> acc
_ -> [value | acc]
end
dedup_list(tail, acc)
end
defp dedup_list([], acc) do
acc
end
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
defp dedup_list([]), do: []
## drop
@@ -5006,8 +5108,7 @@ end
defimpl Enumerable, for: List do
def count(list), do: {:ok, length(list)}
def member?([], _value), do: {:ok, false}
def member?(_list, _value), do: {:error, __MODULE__}
def member?(list, value), do: {:ok, :lists.member(value, list)}
def slice([]), do: {:ok, 0, fn _, _, _ -> [] end}
def slice(_list), do: {:error, __MODULE__}
@@ -5062,7 +5163,16 @@ defimpl Enumerable, for: Range do
end
# TODO: Remove me on v2.0
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
reduce =
quote generated: true do
reduce(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(acc),
var!(fun)
)
end
def unquote(reduce) do
step = if first <= last, do: 1, else: -1
reduce(Map.put(range, :step, step), acc, fun)
end
@@ -5085,12 +5195,12 @@ defimpl Enumerable, for: Range do
{:done, acc}
end
def member?(first..last//step, value) when is_integer(value) do
if step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
else
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
end
def member?(first..last//step, value) when is_integer(value) and step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
end
def member?(first..last//step, value) when is_integer(value) and step < 0 do
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
end
# TODO: Remove me on v2.0
@@ -5109,15 +5219,24 @@ defimpl Enumerable, for: Range do
end
def slice(first.._//step = range) do
{:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
{:ok, Range.size(range), &slice(first + &1 * step, step * &3, &2)}
end
# TODO: Remove me on v2.0
def slice(%{__struct__: Range, first: first, last: last} = range) do
slice =
quote generated: true do
slice(%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range))
end
def unquote(slice) do
step = if first <= last, do: 1, else: -1
slice(Map.put(range, :step, step))
end
defp slice(_current, _step, 0), do: []
defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)]
defp slice(current, _step, 1), do: [current]
defp slice(current, step, remaining) when remaining > 1 do
[current | slice(current + step, step, remaining - 1)]
end
end
+22 -40
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
@@ -188,13 +188,12 @@ defmodule Exception do
term
|> inspect(pretty: true)
|> String.split("\n")
|> Enum.map(fn
|> Enum.map_intersperse("\n", fn
"" -> ""
line -> " " <> line
end)
|> Enum.join("\n")
message <> "\n\n" <> inspected
IO.iodata_to_binary([message, "\n\n", inspected, "\n"])
end
@doc """
@@ -288,10 +287,10 @@ defmodule Exception do
end
end
defp is_map_node?({:is_map, _, [_]}), do: true
defp is_map_node?(_), do: false
defp is_map_key_node?({:is_map_key, _, [_, _]}), do: true
defp is_map_key_node?(_), do: false
defp map_node?({:is_map, _, [_]}), do: true
defp map_node?(_), do: false
defp map_key_node?({:is_map_key, _, [_, _]}), do: true
defp map_key_node?(_), do: false
defp struct_validation_node?(
{:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]}
@@ -305,16 +304,16 @@ defmodule Exception do
defp struct_validation_node?(_), do: false
defp is_struct_macro?(
defp struct_macro?(
{:and, _,
[
{:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [arg, _]}}]},
%{node: node_3 = {_, _, [{_, _, [_, arg]}]}}
]}
),
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
defp is_struct_macro?(
defp struct_macro?(
{:and, _,
[
{:and, _,
@@ -329,12 +328,12 @@ defmodule Exception do
%{node: node_3 = {_, _, [{_, _, [_, arg]}, _]}}
]}
),
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
defp is_struct_macro?(_), do: false
defp struct_macro?(_), do: false
defp translate_guard(guard) do
if is_struct_macro?(guard) do
if struct_macro?(guard) do
undo_is_struct_guard(guard)
else
guard
@@ -1389,6 +1388,7 @@ defmodule CompileError do
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
the error occurred in code that did not come from a file
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred
* `:description` (`t:String.t/0`) - a description of the compile error
This is mostly raised by Elixir tooling when compiling and evaluating code.
"""
@@ -1457,20 +1457,6 @@ defmodule BadFunctionError do
end
end
defmodule BadStructError do
@moduledoc deprecated:
"This exception is deprecated alongside the struct update syntax that raises it"
defexception [:struct, :term]
@impl true
def message(exception) do
Exception.__format_message_with_term__(
"expected a struct named #{inspect(exception.struct)}, got:",
exception.term
)
end
end
defmodule BadMapError do
@moduledoc """
An exception raised when a map is expected, but something else was given.
@@ -1912,7 +1898,7 @@ defmodule UndefinedFunctionError do
end
defp format_fa({_dist, fun, arity}) do
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
end
defp exports_for(module) do
@@ -1944,8 +1930,8 @@ defmodule FunctionClauseError do
For example:
iex> URI.parse(:wrong_argument)
** (FunctionClauseError) no function clause matching in URI.parse/1
iex> List.duplicate(:ok, -3)
** (FunctionClauseError) no function clause matching in List.duplicate/2
The following fields of this exception are public and can be accessed freely:
@@ -2244,7 +2230,7 @@ defmodule KeyError do
case suggestions do
[] -> []
suggestions -> ["\n\nDid you mean:\n\n" | format_suggestions(suggestions)]
suggestions -> ["\nDid you mean:\n\n" | format_suggestions(suggestions)]
end
end
@@ -2253,7 +2239,7 @@ defmodule KeyError do
|> Enum.sort(&(elem(&1, 0) >= elem(&2, 0)))
|> Enum.take(@max_suggestions)
|> Enum.sort(&(elem(&1, 1) <= elem(&2, 1)))
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
end
end
@@ -2605,10 +2591,6 @@ defmodule ErlangError do
%BadFunctionError{term: term}
end
def normalize({:badstruct, struct, term}, _stacktrace) do
%BadStructError{struct: struct, term: term}
end
def normalize({:badmatch, term}, _stacktrace) do
%MatchError{term: term}
end
+126 -28
View File
@@ -317,7 +317,7 @@ defmodule File do
directories of `path`
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory
* `:eperm` - missed required permisions
* `:eperm` - missed required permissions
## Examples
@@ -400,6 +400,13 @@ defmodule File do
You can use `:file.format_error/1` to get a descriptive string of the error.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.read("hello.txt")
@@ -408,15 +415,24 @@ defmodule File do
File.read("non_existing.txt")
#=> {:error, :enoent}
"""
@spec read(Path.t()) :: {:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
def read(path) do
:file.read_file(IO.chardata_to_string(path))
@spec read(Path.t(), [exists_option]) ::
{:ok, binary} | {:error, posix | :badarg | :terminated | :system_limit}
when exists_option: :raw
def read(path, opts \\ []) do
:file.read_file(IO.chardata_to_string(path), opts)
end
@doc """
Returns a binary with the contents of the given filename,
or raises a `File.Error` exception if an error occurs.
## Options (since v1.20)
The supported options are:
* `:raw` - a single atom to bypass the file server and only check
for the file locally
## Examples
File.read!("hello.txt")
@@ -425,9 +441,9 @@ defmodule File do
File.read!("non_existing.txt")
** (File.Error) could not read file "non_existing.txt": no such file or directory
"""
@spec read!(Path.t()) :: binary
def read!(path) do
case read(path) do
@spec read!(Path.t(), [exists_option]) :: binary when exists_option: :raw
def read!(path, opts \\ []) do
case read(path, opts) do
{:ok, binary} ->
binary
@@ -694,7 +710,7 @@ defmodule File do
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
{:error, :enoent}
File.touch("/tmp/a.txt", 1544519753)
File.touch("/tmp/a.txt", 1_544_519_753)
#=> :ok
"""
@@ -706,7 +722,7 @@ defmodule File do
path = IO.chardata_to_string(path)
with {:error, :enoent} <- :elixir_utils.change_universal_time(path, time),
:ok <- write(path, "", [:append]),
:ok <- write(path, "", [:raw, :append]),
do: :elixir_utils.change_universal_time(path, time)
end
@@ -714,7 +730,7 @@ defmodule File do
path = IO.chardata_to_string(path)
with {:error, :enoent} <- :elixir_utils.change_posix_time(path, time),
:ok <- write(path, "", [:append]),
:ok <- write(path, "", [:raw, :append]),
do: :elixir_utils.change_posix_time(path, time)
end
@@ -733,7 +749,7 @@ defmodule File do
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
File.touch!("/tmp/a.txt", 1544519753)
File.touch!("/tmp/a.txt", 1_544_519_753)
"""
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
@@ -1087,7 +1103,7 @@ defmodule File do
@doc ~S"""
Copies the contents in `source` to `destination` recursively, maintaining the
source directory structure and modes.
source directory structure and regular file modes.
If `source` is a file or a symbolic link to it, `destination` must be a path
to an existent file, a symbolic link to one, or a path to a non-existent file.
@@ -1098,7 +1114,9 @@ defmodule File do
If the source is a file, it copies `source` to `destination`. If the `source`
is a directory, it copies the contents inside source into the `destination` directory.
If a file already exists in the destination, it invokes the optional `on_conflict`
For regular files, their respective file modes are preserved in the destination.
Directory modes are preserved only when `:preserve_directory_permissions` is `true`.
If a file already exists in the destination, it invokes the optional `:on_conflict`
callback given as an option. See "Options" for more information.
This function may fail while copying files, in such cases, it will leave the
@@ -1114,6 +1132,14 @@ defmodule File do
explicitly disallow this behavior. If `source` is a `file` and `destination`
is a directory, `{:error, :eisdir}` will be returned.
Special files such as device files, sockets, and named pipes are not copied.
Typical error reasons are:
* `:enoent` - `source` does not exist
* `:eisdir` - `source` is a file and `destination` is a directory
* `:einval` - `destination` is the same as or a subdirectory of `source`
## Options
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
@@ -1127,6 +1153,11 @@ defmodule File do
dereferenced and have their contents copied instead when set to `true`. If the dereferenced
files do not exist, than the operation fails. The default is `false`.
* `:preserve_directory_permissions` - (since v1.20.0) when `true`, the permissions of
source directories are copied to the destination directories after their contents are
written. This is useful when source directories are read-only or have restricted
permissions that must be preserved. The default is `false`.
## Examples
# Copies file "a.txt" to "b.txt"
@@ -1144,11 +1175,16 @@ defmodule File do
#=> {:ok, ["z.txt", "y.txt", "x.txt]}
File.cp_r("non_existing.txt", "copy.txt")
#=> {:error, :enoent}
#=> {:error, :enoent, "non_existing.txt"}
# Copying into a subdirectory of source is not allowed
File.cp_r("src", "src/dest")
#=> {:error, :einval, "src/dest"}
"""
@spec cp_r(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean()
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) ::
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
@@ -1170,6 +1206,7 @@ defmodule File do
def cp_r(source, destination, options) when is_list(options) do
on_conflict = Keyword.get(options, :on_conflict, fn _, _ -> true end)
dereference? = Keyword.get(options, :dereference_symlinks, false)
preserve_directory_permissions? = Keyword.get(options, :preserve_directory_permissions, false)
source =
source
@@ -1181,9 +1218,25 @@ defmodule File do
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
case do_cp_r(source, destination, on_conflict, dereference?, []) do
{:error, _, _} = error -> error
res -> {:ok, res}
source_parts = source |> Path.expand() |> Path.split()
dest_parts = destination |> Path.expand() |> Path.split()
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
{:error, :einval, destination}
else
dereference = if dereference?, do: MapSet.new(), else: nil
case do_cp_r(
source,
destination,
on_conflict,
dereference,
preserve_directory_permissions?,
[]
) do
{:error, _, _} = error -> error
res -> {:ok, res}
end
end
end
@@ -1204,7 +1257,8 @@ defmodule File do
"""
@spec cp_r!(Path.t(), Path.t(),
on_conflict: on_conflict_callback,
dereference_symlinks: boolean()
dereference_symlinks: boolean(),
preserve_directory_permissions: boolean()
) :: [binary]
def cp_r!(source, destination, options \\ []) do
case cp_r(source, destination, options) do
@@ -1221,15 +1275,28 @@ defmodule File do
end
end
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do
{:ok, :regular} ->
do_cp_file(src, dest, on_conflict, acc)
case do_cp_file(src, dest, on_conflict, acc) do
# we don't have a way to make a distinction between a non-existing src
# or dest being a non-existing dir in the case of :enoent,
# but we already know that src exists here.
{:error, :enoent, _} -> {:error, :enoent, dest}
other -> other
end
{:ok, :symlink} ->
case :file.read_link(src) do
{:ok, link} when dereference? ->
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
{:ok, link} when dereference != nil ->
resolved = Path.expand(link, Path.dirname(src))
if MapSet.member?(dereference, resolved) do
{:error, :eloop, src}
else
dereference = MapSet.put(dereference, resolved)
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
end
{:ok, link} ->
do_cp_link(link, src, dest, on_conflict, acc)
@@ -1243,9 +1310,35 @@ defmodule File do
{:ok, files} ->
case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] ->
Enum.reduce(files, [dest | acc], fn x, acc ->
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
files
|> Enum.reduce_while([dest | acc], fn x, acc ->
case do_cp_r(
Path.join(src, x),
Path.join(dest, x),
on_conflict,
dereference,
preserve_dir_perms?,
acc
) do
{:error, _, _} = error -> {:halt, error}
acc -> {:cont, acc}
end
end)
|> case do
{:error, _, _} = error ->
error
files when preserve_dir_perms? ->
# Change the directory after writing files in case
# it was originally read only
case copy_file_mode(src, dest) do
:ok -> files
{:error, reason} -> {:error, reason, src}
end
files ->
files
end
{:error, reason} ->
{:error, reason, dest}
@@ -1256,7 +1349,7 @@ defmodule File do
end
{:ok, _} ->
{:error, :eio, src}
acc
{:error, reason} ->
{:error, reason, src}
@@ -1264,7 +1357,7 @@ defmodule File do
end
# If we reach this clause, there was an error while processing a file.
defp do_cp_r(_, _, _, _, acc) do
defp do_cp_r(_, _, _, _, _, acc) do
acc
end
@@ -2159,7 +2252,12 @@ defmodule File do
def stream!(path, line_or_bytes, modes)
def stream!(path, modes, line_or_bytes) when is_list(modes) do
# TODO: Deprecate this on Elixir v1.20
# TODO: Remove me on Elixir 2.0
IO.warn(
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
"invoke File.stream!(path, line_or_bytes, modes) instead"
)
stream!(path, line_or_bytes, modes)
end
+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
+137 -121
View File
@@ -42,7 +42,7 @@ defmodule Float do
To learn more about floating-point arithmetic visit:
* [0.30000000000000004.com](http://0.30000000000000004.com/)
* [0.30000000000000004.com](https://0.30000000000000004.com/)
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
"""
@@ -186,10 +186,9 @@ defmodule Float do
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
# rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, true = _e?, acc) do
# :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example,
# "1.0e400". Because of this, we rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, _, acc) do
acc
|> add_dot(dot?)
|> :lists.reverse()
@@ -200,16 +199,6 @@ defmodule Float do
float -> {float, rest}
end
defp parse_unsigned(rest, dot?, false = _e?, acc) do
float =
acc
|> add_dot(dot?)
|> :lists.reverse()
|> :erlang.list_to_float()
{float, rest}
end
defp add_dot(acc, true), do: acc
defp add_dot(acc, false), do: [?0, ?. | acc]
@@ -355,15 +344,12 @@ defmodule Float do
"""
@spec round(float, precision_range) :: float
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
def round(float, precision \\ 0)
def round(float, 0) when float == 0.0, do: float
def round(float, 0) when is_float(float) do
case float |> :erlang.round() |> :erlang.float() do
case :erlang.round(float) * 1.0 do
zero when zero == 0.0 and float < 0.0 -> -0.0
rounded -> rounded
end
@@ -377,140 +363,170 @@ defmodule Float do
raise ArgumentError, invalid_precision_message(precision)
end
# Decimal-place rounding via exact rational scaling. This is the bignum
# core used by reference implementations like David M. Gay's "Correctly
# Rounded Binary-Decimal and Decimal-Binary Conversions" (cited in the
# @doc above), Python's round(), and Java's BigDecimal.setScale.
#
# 1. Decompose float exactly: |float| = mantissa / 2^shift.
# 2. Scale exactly: |float| * 10^precision = mantissa * 10^precision / 2^shift.
# Because precision is bounded to 0..15, the product fits in ~103 bits
# (53-bit mantissa + ~50-bit power of ten) and BEAM bignums handle it
# directly without approximation.
# 3. Round the exact rational to an integer per the requested mode
# (half_up / floor / ceil) using quotient and remainder.
# 4. Emit the float closest to rounded_int / 10^precision:
# - fast path: when rounded_int < 2^53, both operands are exactly
# representable as floats and IEEE division is correctly rounded.
# - slow path: bignum alignment + manual mantissa extraction with
# round-to-nearest-even for the trailing bit.
#
# The integer-rounding decision (step 3) and the binary-emission decision
# (step 4) are deliberately independent: step 3 picks the exact rational
# the user asked for, step 4 picks the closest float to that rational.
# Conflating them is the classic source of double-rounding bugs.
#
# Faster algorithms exist (Cox 2026's table-based uscale; Ryū / Schubfach
# for round-trip printing) but target different problems or assume
# fixed-width machine arithmetic that BEAM doesn't expose efficiently.
# At precision <= 15, the exact path is small, easy to audit, and fast
# enough that a more complex algorithm has not been justified by benchmarks.
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
defp round(float, precision, rounding) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count} = decompose(significant, 1)
count = count - exp + 1023
defp round(float, precision, mode) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
cond do
# Precision beyond 15 digits
count >= 104 ->
case rounding do
:ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
:ceil when sign === 1 -> minus_zero()
:half_up when sign === 1 -> minus_zero()
_ -> 0.0
end
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump
# to 10^-precision; everything else rounds to signed zero).
exp == 0 ->
tiny_round(sign, precision, mode)
# We are asking more precision than we have
count <= precision ->
# |float| >= 2^52 — has no fractional bits, return unchanged.
exp - 1075 >= 0 ->
float
true ->
# Difference in precision between float and asked precision
# We subtract 1 because we need to calculate the remainder too
diff = count - precision - 1
# Get up to latest so we calculate the remainder
power_of_10 = power_of_10(diff)
# Convert the numerand to decimal base
num = num * power_of_5(count)
# Move to the given precision - 1
num = div(num, power_of_10)
div = div(num, 10)
num = rounding(rounding, sign, num, div)
# Convert back to float without loss
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
den = power_of_10(precision)
boundary = den <<< 52
cond do
num == 0 and sign == 1 ->
minus_zero()
num == 0 ->
0.0
num >= boundary ->
{den, exp} = scale_down(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
true ->
{num, exp} = scale_up(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
end
mantissa = @power_of_2_to_52 ||| mantissa
shift = 1075 - exp
do_round(sign, mantissa, shift, precision, mode)
end
end
# TODO remove once we require Erlang/OTP 27+
# This function tricks the compiler to avoid this bug in previous versions:
# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
defp minus_zero, do: -0.0
defp decompose(significant, initial) do
decompose(significant, 1, 0, initial)
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign.
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do
tiny_round(sign, precision, mode)
end
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
end
defp do_round(sign, mantissa, shift, precision, mode) do
power = power_of_10(precision)
product = mantissa * power
half = 1 <<< (shift - 1)
quotient = product >>> shift
remainder = product - (quotient <<< shift)
rounded_int = round_step(mode, sign, quotient, remainder, half)
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
decompose(bits, count + 1, last_count, acc)
end
cond do
rounded_int == 0 ->
signed_zero(sign)
defp decompose(<<>>, _count, last_count, acc) do
{acc, last_count}
end
rounded_int < @power_of_2_to_52 <<< 1 ->
# Both rounded_int and power fit in 53 bits, so IEEE float division
# is correctly rounded.
result = rounded_int / power
if sign == 1, do: -result, else: result
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
defp scale_down(num, den, exp) do
new_den = den <<< 1
if num < new_den do
{den >>> 52, exp}
else
scale_down(num, new_den, exp + 1)
true ->
bignum_to_float(sign, rounded_int, power)
end
end
defp decimal_to_float(sign, num, den, exp) do
quo = div(num, den)
rem = num - quo * den
defp round_step(:half_up, _sign, quotient, remainder, half) do
if remainder >= half, do: quotient + 1, else: quotient
end
tmp =
case den >>> 1 do
den when rem > den -> quo + 1
den when rem < den -> quo
_ when (quo &&& 1) === 1 -> quo + 1
_ -> quo
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient
defp round_step(:ceil, 1, quotient, _remainder, _half), do: quotient
defp signed_zero(0), do: 0.0
defp signed_zero(1), do: -0.0
# Result of rounding a non-zero float whose |float * 10^precision| < 0.5.
# ceil(+) → +10^-precision, floor(-) → -10^-precision, others → signed 0.
defp tiny_round(0, precision, :ceil), do: 1.0 / power_of_10(precision)
defp tiny_round(1, precision, :floor), do: -1.0 / power_of_10(precision)
defp tiny_round(sign, _precision, _mode), do: signed_zero(sign)
# Slow path: emit float closest to `sign * rounded_int / power` when
# rounded_int >= 2^53. The binary emission step is always IEEE
# round-to-nearest-even, regardless of the integer-rounding mode.
defp bignum_to_float(sign, rounded_int, power) do
shift_adjust = bit_length(rounded_int) - bit_length(power) - 53
{numerator, denominator, exp} = align(rounded_int, power, shift_adjust)
quotient = div(numerator, denominator)
remainder = numerator - quotient * denominator
half = denominator >>> 1
mantissa =
cond do
remainder > half -> quotient + 1
remainder < half -> quotient
(quotient &&& 1) === 1 -> quotient + 1
true -> quotient
end
tmp = tmp - @power_of_2_to_52
<<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
tmp
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient
# or when rounding carries. Rebalance into the canonical [2^52, 2^53)
# range so the 52-bit packing below doesn't silently truncate.
{mantissa, exp} =
if mantissa == @power_of_2_to_52 <<< 1,
do: {@power_of_2_to_52, exp + 1},
else: {mantissa, exp}
<<result::float>> = <<sign::1, exp + 1023::11, mantissa - @power_of_2_to_52::52>>
result
end
defp rounding(:floor, 1, _num, div), do: div + 1
defp rounding(:ceil, 0, _num, div), do: div + 1
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53)
# and the resulting float = numerator/denominator * 2^(exp-52).
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
new_power = power <<< shift_adjust
defp rounding(:half_up, _sign, num, div) do
case rem(num, 10) do
rem when rem < 5 -> div
rem when rem >= 5 -> div + 1
if rounded_int < new_power <<< 53,
do: {rounded_int, new_power, 52 + shift_adjust},
else: {rounded_int, new_power <<< 1, 53 + shift_adjust}
end
defp align(rounded_int, power, shift_adjust) do
shifted = rounded_int <<< -shift_adjust
cond do
shifted >= power <<< 53 -> {shifted, power <<< 1, 53 + shift_adjust}
shifted >= power <<< 52 -> {shifted, power, 52 + shift_adjust}
true -> {shifted <<< 1, power, 51 + shift_adjust}
end
end
defp rounding(_, _, _, div), do: div
defp bit_length(0), do: 0
defp bit_length(integer) when integer > 0, do: bit_length(integer, 0)
defp bit_length(integer, acc) when integer >= 1 <<< 64, do: bit_length(integer >>> 64, acc + 64)
defp bit_length(integer, acc) when integer >= 1 <<< 16, do: bit_length(integer >>> 16, acc + 16)
defp bit_length(integer, acc) when integer >= 1 <<< 4, do: bit_length(integer >>> 4, acc + 4)
defp bit_length(integer, acc) when integer >= 1, do: bit_length(integer >>> 1, acc + 1)
defp bit_length(_integer, acc), do: acc
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_10(unquote(x)), do: unquote(acc)
Enum.reduce(0..15, 1, fn exponent, acc ->
defp power_of_10(unquote(exponent)), do: unquote(acc)
acc * 10
end)
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_5(unquote(x)), do: unquote(acc)
acc * 5
end)
@doc """
Returns a pair of integers whose ratio is exactly equal
to the original float and with a positive denominator.
+48 -7
View File
@@ -210,13 +210,14 @@ defmodule GenServer do
* [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the
child should be restarted, defaults to `:permanent`
* [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to
shut down the child, either immediately or by giving it time to shut down
shut down the child, either immediately or by giving it time to shut down,
defaults to `5_000`
For example:
use GenServer, restart: :transient, shutdown: 10_000
See the ["Child specification"](`m:Supervisor#module-child_spec-1-function`) section in the `Supervisor` module for more
See the ["Child specification"](`m:Supervisor#module-child-specification`) section in the `Supervisor` module for more
detailed information. The `@doc` annotation immediately preceding
`use GenServer` will be attached to the generated `child_spec/1` function.
@@ -231,6 +232,8 @@ defmodule GenServer do
a name on start via the `:name` option. Registered names are also
automatically cleaned up on termination. The supported values are:
* `nil` (default) - the GenServer is not registered with a name.
* an atom - the GenServer is registered locally (to the current node)
with the given name using `Process.register/2`.
@@ -349,6 +352,41 @@ defmodule GenServer do
message arriving, `handle_info/2` is called with `:timeout` as the first
argument.
For example:
defmodule Counter do
use GenServer
@timeout to_timeout(second: 5)
@impl true
def init(count) do
{:ok, count, @timeout}
end
@impl true
def handle_call(:increment, _from, count) do
new_count = count + 1
{:reply, new_count, new_count, @timeout}
end
@impl true
def handle_info(:timeout, count) do
{:stop, :normal, count}
end
end
A `Counter` server will exit with `:normal` if there are no messages in 5 seconds
after the initialization or after the last `:increment` call:
{:ok, counter_pid} = GenServer.start(Counter, 50)
GenServer.call(counter_pid, :increment)
#=> 51
# After 5 seconds
Process.alive?(counter_pid)
#=> false
## When (not) to use a GenServer
So far, we have learned that a `GenServer` can be used as a supervised process
@@ -488,7 +526,7 @@ defmodule GenServer do
* [GenServer - Elixir's Getting Started Guide](genservers.md)
* [`:gen_server` module documentation](`:gen_server`)
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
* [Clients and Servers - Learn You Some Erlang for Great Good!](https://learnyousomeerlang.com/clients-and-servers)
"""
@@ -529,9 +567,12 @@ defmodule GenServer do
`Supervisor`. Likely this approach involves calling `Supervisor.restart_child/2`
after a delay to attempt a restart.
Returning `{:stop, reason}` will cause `start_link/3` to return
`{:error, reason}` and the process to exit with reason `reason` without
entering the loop or calling `c:terminate/2`.
Returning `{:error, reason}` will cause `start_link/3` to return
`{:error, reason}`.
Returning `{:stop, reason}` will the process to exit with reason `reason`,
without entering the loop or calling `c:terminate/2`. `start_link/3` will
return `{:error, reason}`, but only if the caller is trapping exits.
"""
@callback init(init_arg :: term) ::
{:ok, state}
@@ -822,7 +863,7 @@ defmodule GenServer do
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
@typedoc "The GenServer name"
@type name :: atom | {:global, term} | {:via, module, term}
@type name :: nil | atom | {:global, term} | {:via, module, term}
@typedoc "Options used by the `start*` functions"
@type options :: [option]
+19 -29
View File
@@ -275,8 +275,6 @@ defprotocol Inspect do
end
defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
@@ -569,6 +567,7 @@ defimpl Inspect, for: Regex do
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
@@ -662,35 +661,18 @@ end
defimpl Inspect, for: Any do
def inspect(%module{} = struct, opts) do
try do
module.__info__(:struct)
rescue
_ -> Inspect.Map.inspect_as_map(struct, opts)
else
info ->
if valid_struct?(info, struct) do
info =
for %{field: field} = map <- info,
field != :__exception__,
do: map
info =
for %{field: field} = map <- module.__info__(:struct),
field != :__exception__,
do: map
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
else
Inspect.Map.inspect_as_map(struct, opts)
end
end
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
end
defp valid_struct?(info, struct), do: valid_struct?(info, struct, map_size(struct) - 1)
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
do: valid_struct?(info, struct, count - 1)
defp valid_struct?([], _struct, 0),
do: true
defp valid_struct?(_fields, _struct, _count),
do: false
# A temporary clause to deal with native records until they are officially supported
def inspect(native_record, _opts) do
:io_lib.format("~p", [native_record]) |> IO.iodata_to_binary()
end
def inspect_as_struct(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
@@ -719,7 +701,15 @@ defimpl Inspect, for: Range do
end
# TODO: Remove me on v2.0
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
inspect =
quote generated: true do
inspect(
%{__struct__: Range, first: var!(first), last: var!(last)} = var!(range),
var!(opts)
)
end
def unquote(inspect) do
step = if first <= last, do: 1, else: -1
inspect(Map.put(range, :step, step), opts)
end
+59 -7
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,
@@ -115,11 +115,28 @@ defmodule Inspect.Opts do
width: non_neg_integer | :infinity
}
@typedoc """
Options for building an `Inspect.Opts` struct with `new/1`.
"""
@type new_opt ::
{:base, :decimal | :binary | :hex | :octal}
| {:binaries, :infer | :as_binaries | :as_strings}
| {:charlists, :infer | :as_lists | :as_charlists}
| {:custom_options, keyword}
| {:inspect_fun, (any, t -> Inspect.Algebra.t())}
| {:limit, non_neg_integer | :infinity}
| {:pretty, boolean}
| {:printable_limit, non_neg_integer | :infinity}
| {:safe, boolean}
| {:structs, boolean}
| {:syntax_colors, [{color_key, IO.ANSI.ansidata()}]}
| {:width, non_neg_integer | :infinity}
@doc """
Builds an `Inspect.Opts` struct.
"""
@doc since: "1.13.0"
@spec new(keyword()) :: t
@spec new([new_opt()]) :: t
def new(opts) do
struct(%Inspect.Opts{inspect_fun: default_inspect_fun()}, opts)
end
@@ -324,6 +341,14 @@ defmodule Inspect.Algebra do
quote do: {:doc_color, unquote(doc), unquote(color)}
end
@typedoc """
Options for container documents.
"""
@type container_opts :: [
separator: String.t(),
break: :strict | :flex | :maybe
]
@docs [
:doc_break,
:doc_collapse,
@@ -371,7 +396,7 @@ defmodule Inspect.Algebra do
def to_doc_with_opts(term, opts)
def to_doc_with_opts(%_{} = struct, %Inspect.Opts{inspect_fun: fun} = opts) do
if opts.structs do
if opts.structs and valid_struct?(struct) do
try do
fun.(struct, opts)
rescue
@@ -428,6 +453,26 @@ defmodule Inspect.Algebra do
fun.(arg, opts) |> pack_opts(opts)
end
defp valid_struct?(%module{} = struct) do
try do
module.__info__(:struct)
rescue
_ -> false
else
info ->
valid_struct?(info, struct, map_size(struct) - 1)
end
end
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
do: valid_struct?(info, struct, count - 1)
defp valid_struct?([], _struct, 0),
do: true
defp valid_struct?(_fields, _struct, _count),
do: false
defp pack_opts({_doc, %Inspect.Opts{}} = doc_opts, _opts), do: doc_opts
defp pack_opts(doc, opts), do: {doc, opts}
@@ -440,7 +485,14 @@ defmodule Inspect.Algebra do
updated options from inspection.
"""
@doc since: "1.6.0"
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
@spec container_doc(
t,
[term],
t,
Inspect.Opts.t(),
(term, Inspect.Opts.t() -> t),
container_opts()
) ::
t
def container_doc(left, collection, right, inspect_opts, fun, opts \\ []) do
container_doc_with_opts(left, collection, right, inspect_opts, fun, opts) |> elem(0)
@@ -496,7 +548,7 @@ defmodule Inspect.Algebra do
t,
Inspect.Opts.t(),
(term, Inspect.Opts.t() -> t),
keyword()
container_opts()
) ::
{t, Inspect.Opts.t()}
def container_doc_with_opts(left, collection, right, inspect_opts, fun, opts \\ [])
+83 -5
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.
@@ -172,6 +211,35 @@ defmodule Integer do
end
end
@doc """
Performs a ceiled integer division.
Raises an `ArithmeticError` exception if one of the arguments is not an
integer, or when the `divisor` is `0`.
This function performs a *ceiled* integer division, which means that
the result will always be rounded towards positive infinity.
## Examples
iex> Integer.ceil_div(5, 2)
3
iex> Integer.ceil_div(6, -4)
-1
iex> Integer.ceil_div(-99, 2)
-49
"""
@doc since: "1.20.0"
@spec ceil_div(integer, neg_integer | pos_integer) :: integer
def ceil_div(dividend, divisor) do
if not :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
div(dividend, divisor) + 1
else
div(dividend, divisor)
end
end
@doc """
Returns the ordered digits for the given `integer`.
@@ -229,8 +297,9 @@ defmodule Integer do
defp undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | _], base, _)
when is_integer(digit) and (digit >= base or digit <= -base),
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit)
@@ -241,7 +310,7 @@ defmodule Integer do
An optional `base` to the corresponding integer can be provided.
If `base` is not given, 10 will be used.
If successful, returns a tuple in the form of `{integer, remainder_of_binary}`.
If successful, returns a tuple in the form of `{integer, remaining_string}`.
Otherwise `:error`.
Raises an error if `base` is less than 2 or more than 36.
@@ -260,6 +329,9 @@ defmodule Integer do
iex> Integer.parse("three")
:error
iex> Integer.parse("404 not found")
{404, " not found"}
iex> Integer.parse("34", 10)
{34, ""}
@@ -460,8 +532,12 @@ defmodule Integer do
iex> Integer.extended_gcd(10, 0)
{10, 1, 0}
iex> Integer.extended_gcd(-10, 0)
{10, -1, 0}
iex> Integer.extended_gcd(0, 10)
{10, 0, 1}
iex> Integer.extended_gcd(0, -10)
{10, 0, -1}
iex> Integer.extended_gcd(0, 0)
{0, 0, 0}
@@ -469,8 +545,10 @@ defmodule Integer do
@doc since: "1.12.0"
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
def extended_gcd(0, 0), do: {0, 0, 0}
def extended_gcd(0, b), do: {b, 0, 1}
def extended_gcd(a, 0), do: {a, 1, 0}
def extended_gcd(0, b) when b > 0, do: {b, 0, 1}
def extended_gcd(0, b) when b < 0, do: {-b, 0, -1}
def extended_gcd(a, 0) when a > 0, do: {a, 1, 0}
def extended_gcd(a, 0) when a < 0, do: {-a, -1, 0}
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
extended_gcd(integer2, integer1, 0, 1, 1, 0)
+61 -14
View File
@@ -128,6 +128,22 @@ defmodule IO do
@type nodata :: {:error, term} | :eof
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
@typedoc """
Stacktrace information as keyword options for `warn/2`.
At least `:file` is required. Other options are optional and used
to provide more precise location information.
"""
@type warn_stacktrace_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
module: module(),
function: {atom(), arity()}
]
defguardp is_device(term) when is_atom(term) or is_pid(term)
defguardp is_iodata(data) when is_list(data) or is_binary(data)
@@ -346,7 +362,10 @@ defmodule IO do
#=> my_app.ex:4: MyApp.main/1
"""
@spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
@spec warn(
chardata | String.Chars.t(),
Exception.stacktrace() | warn_stacktrace_opts() | Macro.Env.t()
) ::
:ok
def warn(message, stacktrace_info)
@@ -448,13 +467,15 @@ defmodule IO do
## Examples
The following code:
IO.inspect(<<0, 1, 2>>, width: 40)
Prints:
<<0, 1, 2>>
We can use the `:label` option to decorate the output:
You can use the `:label` option to decorate the output:
IO.inspect(1..100, label: "a wonderful range")
@@ -462,21 +483,23 @@ defmodule IO do
a wonderful range: 1..100
The `:label` option is especially useful with pipelines:
Inspect truncates large inputs by default. The `:printable_limit` controls
the limit for strings and other string-like constructs (such as charlists):
[1, 2, 3]
|> IO.inspect(label: "before")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after")
|> Enum.sum()
"abc"
|> String.duplicate(9001)
|> IO.inspect(printable_limit: :infinity)
Prints:
For containers such as lists, maps, and tuples, the number of entries
is managed by the `:limit` option:
before: [1, 2, 3]
after: [2, 4, 6]
1..100
|> Enum.map(& {&1, &1})
|> Enum.into(%{})
|> IO.inspect(limit: :infinity)
"""
@spec inspect(item, keyword) :: item when item: var
@spec inspect(item, inspect_opts) :: item when item: var
def inspect(item, opts \\ []) do
inspect(:stdio, item, opts)
end
@@ -486,9 +509,10 @@ defmodule IO do
See `inspect/2` for a full list of options.
"""
@spec inspect(device, item, keyword) :: item when item: var
@spec inspect(device, item, inspect_opts) :: item when item: var
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
{label, opts} = Keyword.pop(opts, :label)
label = if label, do: [to_chardata(label), ": "], else: []
opts = Inspect.Opts.new(opts)
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
chardata = Inspect.Algebra.format(doc, opts.width)
@@ -772,6 +796,29 @@ defmodule IO do
:erlang.iolist_size(iodata)
end
@doc """
Checks if an IO data (the length is zero).
For more information about IO data, see the ["IO data"](#module-io-data)
section in the module documentation.
## Examples
iex> IO.iodata_empty?([])
true
iex> IO.iodata_empty?([""])
true
iex> IO.iodata_empty?([1, 2 | <<3, 4>>])
false
"""
@doc since: "1.20.0"
@spec iodata_empty?(iodata) :: boolean
def iodata_empty?(""), do: true
def iodata_empty?([]), do: true
def iodata_empty?([head | tail]), do: iodata_empty?(head) and iodata_empty?(tail)
def iodata_empty?(_), do: false
@doc false
def each_stream(device, line_or_codepoints) do
case read(device, line_or_codepoints) do
+18 -4
View File
@@ -5,6 +5,20 @@
defmodule IO.ANSI.Docs do
@moduledoc false
@type print_opts :: [
enabled: boolean(),
doc_bold: [IO.ANSI.ansicode()],
doc_code: [IO.ANSI.ansicode()],
doc_headings: [IO.ANSI.ansicode()],
doc_metadata: [IO.ANSI.ansicode()],
doc_quote: [IO.ANSI.ansicode()],
doc_inline_code: [IO.ANSI.ansicode()],
doc_table_heading: [IO.ANSI.ansicode()],
doc_title: [IO.ANSI.ansicode()],
doc_underline: [IO.ANSI.ansicode()],
width: pos_integer()
]
@bullet_text_unicode "• "
@bullet_text_ascii "* "
@bullets [?*, ?-, ?+]
@@ -30,7 +44,7 @@ defmodule IO.ANSI.Docs do
Values for the color settings are strings with
comma-separated ANSI values.
"""
@spec default_options() :: keyword
@spec default_options() :: print_opts
def default_options do
[
enabled: true,
@@ -52,7 +66,7 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_headings([String.t()], keyword) :: :ok
@spec print_headings([String.t()], print_opts) :: :ok
def print_headings(headings, options \\ []) do
# It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
# breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
@@ -77,7 +91,7 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_metadata(map, keyword) :: :ok
@spec print_metadata(map, print_opts) :: :ok
def print_metadata(metadata, options \\ []) when is_map(metadata) do
options = Keyword.merge(default_options(), options)
print_each_metadata(metadata, options) && IO.write("\n")
@@ -115,7 +129,7 @@ defmodule IO.ANSI.Docs do
It takes a set of `options` defined in `default_options/0`.
"""
@spec print(term(), String.t(), keyword) :: :ok
@spec print(term(), String.t(), print_opts) :: :ok
def print(doc, format, options \\ [])
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
+39 -18
View File
@@ -8,24 +8,29 @@ defprotocol JSON.Encoder do
If you have a struct, you can derive the implementation of this protocol
by specifying which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [....]}
@derive {JSON.Encoder, only: [...]}
defstruct ...
It is also possible to encode all fields or skip some fields via the
`:except` option:
Additionally, you can exclude specific fields using the `:except` option or
encode all fields by omitting both options entirely, but these should be used
with caution:
@derive {JSON.Encoder, except: [...]}
defstruct ...
@derive JSON.Encoder
defstruct ...
> #### Leaking Private Information {: .error}
>
> The `:except` approach should be used carefully to avoid
> accidentally leaking private information when new fields are added.
> Prefer using `:only` to avoid accidentally leaking private information when
> new fields are added. Other approaches should be used with auction.
Finally, if you don't own the struct you want to encode to JSON,
you may use `Protocol.derive/3` placed outside of any module:
You can also use `Protocol.derive/3` if you don't own the struct that you want
to encode to JSON:
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct, except: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct)
"""
@@ -62,7 +67,7 @@ defprotocol JSON.Encoder do
{io, _prefix} =
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
key = IO.iodata_to_binary([prefix, :json.encode_binary(Atom.to_string(field)), ?:])
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
end)
@@ -125,25 +130,25 @@ end
defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do
:elixir_json.encode_binary(value)
:json.encode_binary(value)
end
end
defimpl JSON.Encoder, for: List do
def encode(value, encoder) do
:elixir_json.encode_list(value, encoder)
:json.encode_list(value, encoder)
end
end
defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do
:elixir_json.encode_integer(value)
:json.encode_integer(value)
end
end
defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do
:elixir_json.encode_float(value)
:json.encode_float(value)
end
end
@@ -328,6 +333,22 @@ defmodule JSON do
| {:invalid_byte, non_neg_integer(), byte()}
| {:unexpected_sequence, non_neg_integer(), binary()}
@typedoc """
Decoders for customizing JSON decoding behavior.
"""
@type decoders :: [
array_start: (term() -> term()),
array_push: (term(), term() -> term()),
array_finish: (term(), term() -> {term(), term()}),
object_start: (term() -> term()),
object_push: (term(), term(), term() -> term()),
object_finish: (term(), term() -> {term(), term()}),
float: (String.t() -> term()),
integer: (String.t() -> term()),
string: (String.t() -> term()),
null: term()
]
@doc ~S"""
Decodes the given JSON.
@@ -381,13 +402,13 @@ defmodule JSON do
For streaming decoding, see Erlang's [`:json`](`:json`) module.
"""
@spec decode(binary(), term(), keyword()) ::
@spec decode(binary(), term(), decoders()) ::
{term(), term(), binary()} | {:error, decode_error_reason()}
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil)
try do
:elixir_json.decode(binary, acc, Map.new(decoders))
:json.decode(binary, acc, Map.new(decoders))
catch
:error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}}
@@ -507,16 +528,16 @@ defmodule JSON do
end
def protocol_encode(value, _encoder) when is_binary(value),
do: :elixir_json.encode_binary(value)
do: :json.encode_binary(value)
def protocol_encode(value, _encoder) when is_integer(value),
do: :elixir_json.encode_integer(value)
do: :json.encode_integer(value)
def protocol_encode(value, _encoder) when is_float(value),
do: :elixir_json.encode_float(value)
do: :json.encode_float(value)
def protocol_encode(value, encoder) when is_list(value),
do: :elixir_json.encode_list(value, encoder)
do: :json.encode_list(value, encoder)
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
do: JSON.Encoder.Map.encode(value, encoder)
+141 -125
View File
@@ -231,7 +231,7 @@ defmodule Kernel do
Finally, note there is an overall structural sorting order, called
"Term Ordering", defined below. This order is provided for reference
purposes, it is not required by Elixir developers to know it by heart.
purposes, it is not required for Elixir developers to know it by heart.
### Term ordering
@@ -1999,6 +1999,12 @@ defmodule Kernel do
{:case, extra ++ meta, args}
end
defp x_is_false_or_nil do
quote generated: true do
:erlang.orelse(:erlang."=:="(x, false), :erlang."=:="(x, nil))
end
end
@doc """
Strictly boolean "or" operator.
@@ -2063,15 +2069,20 @@ defmodule Kernel do
end
defp build_boolean_check(operator, check, true_clause, false_clause) do
annotate_case(
[optimize_boolean: true, type_check: :expr],
bools =
quote do
case unquote(check) do
false -> unquote(false_clause)
true -> unquote(true_clause)
other -> :erlang.error({:badbool, unquote(operator), other})
end
false -> unquote(false_clause)
true -> unquote(true_clause)
end
error =
quote generated: true do
other -> :erlang.error({:badbool, unquote(operator), other})
end
annotate_case(
[optimize_boolean: true, type_check: {:case, operator}],
{:case, [], [check, [do: bools ++ error]]}
)
end
@@ -2098,10 +2109,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :"!!"}],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
x when unquote(x_is_false_or_nil()) -> false
_ -> true
end
end
@@ -2112,10 +2123,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :!}],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
x when unquote(x_is_false_or_nil()) -> true
_ -> false
end
end
@@ -2456,7 +2467,7 @@ defmodule Kernel do
See the "Deriving" section of the documentation of the `Inspect`
protocol for more information.
"""
@spec inspect(Inspect.t(), keyword) :: String.t()
@spec inspect(Inspect.t(), [Inspect.Opts.new_opt()]) :: String.t()
def inspect(term, opts \\ []) when is_list(opts) do
opts = Inspect.Opts.new(opts)
@@ -2741,7 +2752,7 @@ defmodule Kernel do
nil ->
quote do
case unquote(term) do
%_{__exception__: true} -> true
%_{__exception__: _} -> true
_ -> false
end
end
@@ -2753,8 +2764,7 @@ defmodule Kernel do
quote do
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -2781,7 +2791,7 @@ defmodule Kernel do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name, __exception__: true} -> true
%{__struct__: ^name, __exception__: _} -> true
_ -> false
end
@@ -2799,8 +2809,7 @@ defmodule Kernel do
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -2815,7 +2824,7 @@ defmodule Kernel do
This is most commonly used in pipelines, using the `|>/2` operator, allowing you
to pipe a value to a function outside of its first argument.
### Examples
## Examples
iex> 1 |> then(fn x -> x * 2 end)
2
@@ -3524,8 +3533,7 @@ defmodule Kernel do
end
@doc """
A convenience macro that checks if the right side (an expression) matches the
left side (a pattern).
A convenience macro that checks if the result of `expression` matches `pattern`.
## Examples
@@ -3603,7 +3611,7 @@ defmodule Kernel do
#=> (MatchError) no match of right hand side value: %{x: 1, y: 2}
"""
defmacro match?(pattern, expr) do
defmacro match?(pattern, expression) do
success =
quote do
unquote(pattern) -> true
@@ -3614,7 +3622,7 @@ defmodule Kernel do
_ -> false
end
{:case, [], [expr, [do: success ++ failure]]}
{:case, [], [expression, [do: success ++ failure]]}
end
@doc """
@@ -3814,19 +3822,6 @@ defmodule Kernel do
{_, doc} when doc_attr? ->
do_at_escape(name, doc)
%{__struct__: Regex, source: source, opts: opts} = regex ->
# TODO: Remove this in Elixir v2.0
IO.warn(
"storing and reading regexes from module attributes is deprecated, " <>
"inline the regex inside the function definition instead",
env
)
case :erlang.system_info(:otp_release) < [?2, ?8] do
true -> do_at_escape(name, regex)
false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
end
value ->
do_at_escape(name, value)
end
@@ -3872,7 +3867,9 @@ defmodule Kernel do
defp do_at_escape(name, value) do
try do
:elixir_quote.escape(value, :none, false)
# mark module attrs as shallow-generated since the ast for their representation
# might contain opaque terms
Macro.escape(value, generated: true)
rescue
ex in [ArgumentError] ->
raise ArgumentError,
@@ -4055,10 +4052,10 @@ defmodule Kernel do
defp build_if(condition, do: do_clause, else: else_clause) do
annotate_case(
[optimize_boolean: true, type_check: :expr],
[optimize_boolean: true, type_check: {:case, :if}],
quote do
case unquote(condition) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
_ -> unquote(do_clause)
end
end
@@ -4106,9 +4103,15 @@ defmodule Kernel do
end
defp build_unless(condition, do: do_clause, else: else_clause) do
quote do
if(unquote(condition), do: unquote(else_clause), else: unquote(do_clause))
end
annotate_case(
[optimize_boolean: true, type_check: {:case, :unless}],
quote do
case unquote(condition) do
x when unquote(x_is_false_or_nil()) -> unquote(do_clause)
_ -> unquote(else_clause)
end
end
)
end
defp build_unless(_condition, _arguments) do
@@ -4376,10 +4379,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
annotate_case(
[type_check: :expr],
[type_check: {:case, :&&}],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x when unquote(x_is_false_or_nil()) ->
x
_ ->
@@ -4419,10 +4422,10 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "||")
annotate_case(
[type_check: :expr],
[type_check: {:case, :||}],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x when unquote(x_is_false_or_nil()) ->
unquote(right)
x ->
@@ -4700,17 +4703,13 @@ defmodule Kernel do
false
[] ->
quote do
_ = unquote(left)
false
end
# inlined as false in erlang pass
quote(do: :lists.member(unquote(left), []))
[head | tail] = list ->
# We only expand lists in the body if they are relatively
# short and it is made only of literal expressions.
case not in_body? or small_literal_list?(right) do
true -> in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
false -> quote(do: :lists.member(unquote(left), unquote(right)))
case in_body? do
false -> in_list(left, head, tail, expand, list)
true -> quote(do: :lists.member(unquote(left), unquote(right)))
end
%{} = right ->
@@ -4722,7 +4721,7 @@ defmodule Kernel do
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step)))
_ when in_body? ->
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
quote(do: Elixir.Enum.__in__(unquote(left), unquote(right)))
_ ->
raise_on_invalid_args_in_2(right)
@@ -4757,12 +4756,6 @@ defmodule Kernel do
end
end
defp small_literal_list?(list) when is_list(list) and length(list) <= 32 do
:lists.all(fn x -> is_binary(x) or is_atom(x) or is_number(x) end, list)
end
defp small_literal_list?(_list), do: false
defp in_range(left, first, last, step) when is_integer(step) do
in_range_literal(left, first, last, step)
end
@@ -4770,8 +4763,8 @@ defmodule Kernel do
defp in_range(left, first, last, step) do
quoted =
quote do
:erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
:erlang.is_integer(unquote(last)) and
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and
unquote(generated_is_integer(last)) and
((:erlang.>(unquote(step), 0) and
unquote(increasing_compare(left, first, last))) or
(:erlang.<(unquote(step), 0) and
@@ -4787,9 +4780,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step > 0 do
quoted =
quote do
:erlang.andalso(
:erlang.is_integer(unquote(left)),
quote generated: true do
Kernel.and(
unquote(generated_is_integer(left)),
unquote(increasing_compare(left, first, last))
)
end
@@ -4799,9 +4792,9 @@ defmodule Kernel do
defp in_range_literal(left, first, last, step) when step < 0 do
quoted =
quote do
:erlang.andalso(
:erlang.is_integer(unquote(left)),
quote generated: true do
Kernel.and(
unquote(generated_is_integer(left)),
unquote(decreasing_compare(left, first, last))
)
end
@@ -4815,36 +4808,28 @@ defmodule Kernel do
defp in_range_step(quoted, left, first, step) do
quote do
:erlang.andalso(
Kernel.and(
unquote(quoted),
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
)
end
end
defp in_list(left, head, tail, expand, right, in_body?) do
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
:lists.foldl(&quote(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
defp in_list(left, head, tail, expand, right) do
[head | tail] = :lists.map(&comp(left, &1, expand, right), [head | tail])
:lists.foldl(&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)))
[tail_head | tail] ->
quote do
:erlang.orelse(
Kernel.or(
:erlang."=:="(unquote(left), unquote(head)),
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
)
end
tail when in_body? ->
quote do
:erlang.orelse(
:erlang."=:="(unquote(left), unquote(head)),
:lists.member(unquote(left), unquote(tail))
unquote(in_list(left, tail_head, tail, expand, right))
)
end
@@ -4853,13 +4838,17 @@ defmodule Kernel do
end
end
defp comp(left, right, _expand, _right, _in_body?) do
defp comp(left, right, _expand, _right) do
quote(do: :erlang."=:="(unquote(left), unquote(right)))
end
defp generated_is_integer(arg) do
quote generated: true, do: :erlang.is_integer(unquote(arg))
end
defp increasing_compare(var, first, last) do
quote do
:erlang.andalso(
Kernel.and(
:erlang.>=(unquote(var), unquote(first)),
:erlang."=<"(unquote(var), unquote(last))
)
@@ -4868,7 +4857,7 @@ defmodule Kernel do
defp decreasing_compare(var, first, last) do
quote do
:erlang.andalso(
Kernel.and(
:erlang."=<"(unquote(var), unquote(first)),
:erlang.>=(unquote(var), unquote(last))
)
@@ -5197,7 +5186,7 @@ defmodule Kernel do
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
%{} ->
:elixir_quote.escape(block, :none, false)
:elixir_quote.escape(block, :escape, false)
end
versioned_vars = env.versioned_vars
@@ -5225,6 +5214,11 @@ defmodule Kernel do
end
end
defmacro defmodule(alias, [{:do, _block}, {atom, _} | _]) when is_atom(atom) do
raise ArgumentError,
"unexpected reserved word at the top-level of the \"defmodule #{Macro.to_string(alias)}\" do-block: #{atom}"
end
defp module_meta({_, meta, _}), do: meta
defp module_meta(_), do: []
@@ -5477,7 +5471,7 @@ defmodule Kernel do
store =
case unquoted_expr or unquoted_call do
true ->
:elixir_quote.escape({call, expr}, :none, true)
:elixir_quote.escape({call, expr}, :escape, true)
false ->
key = :erlang.unique_integer()
@@ -5605,8 +5599,8 @@ defmodule Kernel do
## Types
It is recommended to define types for structs. By convention, such a type
is called `t`. To define a struct inside a type, the struct literal syntax
is used:
is called `t`. To define a type for a struct, the struct literal syntax is
used:
defmodule User do
defstruct name: "John", age: 25
@@ -5877,11 +5871,15 @@ defmodule Kernel do
end
@doc """
Defines a macro suitable for use in guard expressions.
Defines a custom guard with the given name.
It raises at compile time if the `guard` uses expressions that aren't
allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a macro that can be used both inside or outside guards.
Once defined, custom guards can be invoked within regular code or in
guards. The module that contains the custom guard must be required before usage.
Custom guards are defined by providing a valid guard expression to
the right-hand side of `when`. `defguard` will then expand and validate
the expressions as guards. `defguard` will raise at compile time if the
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
@@ -5889,31 +5887,30 @@ defmodule Kernel do
## Example
For example, to define a guard similar to `Integer.is_even/1`, you can write:
defmodule Integer.Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end
defmodule Collatz do
@moduledoc "Tools for working with the Collatz sequence."
import Integer.Guards
which can then be used as:
@doc "Determines the number of steps `n` takes to reach `1`."
# If this function never converges, please let me know what `n` you used.
def converge(n) when n > 0, do: step(n, 0)
require Integer.Guards
Integer.Guards.is_even(3)
#=> false
defp step(1, step_count) do
step_count
end
## Implementation details
defp step(n, step_count) when is_even(n) do
step(div(n, 2), step_count + 1)
end
Behind the scenes, `defguard` will generate a macro which can be used
inside and outside of guards, preserving their respective semantics.
defp step(n, step_count) do
step(3 * n + 1, step_count + 1)
end
end
When invoked inside a guard, it behaves as if the right-hand side of
`when` is injected as part of the guard, replacing the custom guard
arguments by the expressions given as inputs.
When invoked outside of a guard, it preserves regular function calling
semantics with one caveat: all arguments are evaluated before invocation,
except arguments which are unused, which are then never evaluated.
"""
@doc since: "1.6.0"
@spec defguard(Macro.t()) :: Macro.t()
@@ -6328,7 +6325,15 @@ defmodule Kernel do
"""
@doc since: "1.14.0"
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
# The compiling process may override the callback by putting it in
# the process dictionary.
dbg_callback =
case :erlang.get({:elixir, :dbg_callback}) do
:undefined -> Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
value -> value
end
{mod, fun, args} = dbg_callback
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
end
@@ -6389,7 +6394,7 @@ defmodule Kernel do
With a timeout:
iex> to_timeout(5400000)
iex> to_timeout(5_400_000)
5400000
iex> to_timeout(:infinity)
:infinity
@@ -6417,12 +6422,20 @@ defmodule Kernel do
{microsecond, _precision} = duration.microsecond
millisecond = :erlang.convert_time_unit(microsecond, :microsecond, :millisecond)
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
total =
duration.week * unquote(week_in_ms) +
duration.day * unquote(day_in_ms) +
duration.hour * unquote(hour_in_ms) +
duration.minute * 60_000 +
duration.second * 1000 +
millisecond
if total < 0 do
raise ArgumentError,
"duration must be positive, got: #{inspect(duration)}"
end
total
end
end
@@ -6630,11 +6643,13 @@ defmodule Kernel do
defmacro sigil_r(term, modifiers)
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
binary = :elixir_interpolation.unescape_string(binary, &regex_unescape_map/1)
compile_regex(binary, options)
end
defmacro sigil_r({:<<>>, meta, pieces}, options) do
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
tuple = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
compile_regex(tuple, options)
end
@@ -6653,13 +6668,14 @@ defmodule Kernel do
end
defp compile_regex(binary_or_tuple, options) do
# TODO: Remove this when we require Erlang/OTP 28+
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
bin_opts = :binary.list_to_bin(options)
case is_binary(binary_or_tuple) do
true ->
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
Macro.escape(Regex.compile!(binary_or_tuple, bin_opts))
false ->
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(bin_opts)))
end
end
+36
View File
@@ -21,6 +21,15 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :references, @timeout)
end
@doc """
Invoked during module expansion to annotate a require
must be warned if unused.
"""
def warn_require(pid, meta, module, alias) do
:gen_server.cast(pid, {:warn_require, module, meta, alias})
module
end
@doc """
Invoked during module expansion to annotate an alias
must be warned if unused.
@@ -57,6 +66,11 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:add_export, module})
end
@doc false
def add_require(pid, module, meta) when is_atom(module) do
:gen_server.cast(pid, {:add_require, module, meta})
end
@doc false
def add_import(pid, module, fas, meta, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
@@ -119,12 +133,18 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :unused_aliases, @timeout)
end
@doc false
def collect_unused_requires(pid) do
:gen_server.call(pid, :unused_requires, @timeout)
end
# Callbacks
def init(:ok) do
state = %{
aliases: %{},
imports: %{},
requires: %{},
references: %{},
exports: %{},
cache: %{},
@@ -150,6 +170,18 @@ defmodule Kernel.LexicalTracker do
{:reply, Enum.sort(imports), state}
end
def handle_call(:unused_requires, _from, state) do
%{references: references, aliases: aliases} = state
unused_requires =
for {module, {meta, alias}} <- state.requires,
Map.get(references, module) != :compile do
{module, meta, alias, Map.get(aliases, alias) == :used}
end
{:reply, Enum.sort(unused_requires), state}
end
def handle_call(:references, _from, state) do
{compile, runtime} = partition(Map.to_list(state.references), [], [])
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
@@ -245,6 +277,10 @@ defmodule Kernel.LexicalTracker do
{:noreply, put_in(state.imports[module][@warn_key], true)}
end
def handle_cast({:warn_require, module, meta, alias}, state) do
{:noreply, put_in(state.requires[module], {meta, alias})}
end
@doc false
def handle_info(_msg, state) do
{:noreply, state}
+141 -39
View File
@@ -16,11 +16,43 @@ defmodule Kernel.ParallelCompiler do
@type warning() :: {file :: Path.t(), Code.position(), message :: String.t()}
@type error() :: {file :: Path.t(), Code.position(), message :: String.t()}
@typedoc """
Options for parallel compilation functions.
"""
@type compile_opts :: [
after_compile: (-> term()),
each_file: (Path.t() -> term()),
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(),
verification: boolean(),
profile: :time,
dest: Path.t(),
beam_timestamp: term(),
return_diagnostics: boolean(),
max_concurrency: pos_integer(),
purge_compiler_modules: boolean()
]
@typedoc """
Options for requiring files in parallel.
"""
@type require_opts :: [
each_file: (Path.t() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
max_concurrency: pos_integer(),
return_diagnostics: boolean()
]
@doc """
Starts a task for parallel compilation.
"""
# TODO: Deprecate this on Elixir v1.20.
@doc deprecated: "Use `pmap/2` instead"
# TODO: Remove me on Elixir 2.0
@deprecated "Use `pmap/2` instead"
def async(fun) when is_function(fun, 0) do
{ref, task} = inner_async(fun)
send(task.pid, ref)
@@ -114,10 +146,9 @@ defmodule Kernel.ParallelCompiler do
the current file stops being compiled until the dependency is
resolved.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -169,6 +200,9 @@ defmodule Kernel.ParallelCompiler do
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
and group pass checker
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
after compilation (see `Code.purge_compiler_modules/0`)
* `:dest` - the destination directory for the BEAM files. When using `compile/2`,
this information is only used to properly annotate the BEAM files before
they are loaded into memory. If you want a file to actually be written to
@@ -177,15 +211,16 @@ defmodule Kernel.ParallelCompiler do
* `:beam_timestamp` - the modification timestamp to give all BEAM files
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
Defaults to the number of schedulers online, or at least `2`.
"""
@doc since: "1.6.0"
@spec compile([Path.t()], keyword()) ::
@spec compile([Path.t()], compile_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def compile(files, options \\ []) when is_list(options) do
@@ -198,7 +233,7 @@ defmodule Kernel.ParallelCompiler do
See `compile/2` for more information.
"""
@doc since: "1.6.0"
@spec compile_to_path([Path.t()], Path.t(), keyword()) ::
@spec compile_to_path([Path.t()], Path.t(), compile_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
@@ -211,10 +246,9 @@ defmodule Kernel.ParallelCompiler do
Opposite to compile, dependencies are not attempted to be
automatically solved between files.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -231,11 +265,15 @@ defmodule Kernel.ParallelCompiler do
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
Defaults to the number of schedulers online, or at least `2`.
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
"""
@doc since: "1.6.0"
@spec require([Path.t()], keyword()) ::
@spec require([Path.t()], require_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def require(files, options \\ []) when is_list(options) do
@@ -286,7 +324,10 @@ defmodule Kernel.ParallelCompiler do
if Keyword.get(options, :return_diagnostics, false) do
{status, modules_or_errors, info}
else
IO.warn("you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler")
IO.warn(
"you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions"
)
to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)
modules_or_errors =
@@ -298,7 +339,14 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(schedulers, checker, files, output, options) do
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = Process.send_after(self(), :threshold_check, threshold)
timer_ref = :erlang.send_after(threshold, self(), :threshold_check)
purge_compiler_modules =
if Keyword.get(options, :purge_compiler_modules, false) do
fn -> :elixir_code_server.cast(:purge_compiler_modules) end
else
fn -> :ok end
end
{outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
@@ -315,7 +363,8 @@ defmodule Kernel.ParallelCompiler do
long_compilation_threshold: threshold,
schedulers: schedulers,
checker: checker,
verification?: Keyword.get(options, :verification, true)
verification?: Keyword.get(options, :verification, true),
purge_compiler_modules: purge_compiler_modules
})
Process.cancel_timer(state.timer_ref)
@@ -342,29 +391,75 @@ defmodule Kernel.ParallelCompiler do
end
end
defp write_module_binaries(result, {:compile, path}, timestamp) do
File.mkdir_p!(path)
Code.prepend_path(path)
defp write_module_binaries(result, {:compile, path}, state) when map_size(result) > 0 do
profile(state, "writing modules to disk", fn ->
File.mkdir_p!(path)
Code.prepend_path(path)
timestamp = state.beam_timestamp
for {{:module, module}, {binary, _}} when is_binary(binary) <- result do
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
File.write!(full_path, binary)
if timestamp, do: File.touch!(full_path, timestamp)
module
end
# We fan-out the writes as that improves performance
# when writing hundreds of beam files. This is cheap as
# we only transfer atoms and binaries across processes.
pool_size = min(map_size(result), state.schedulers)
pool_list =
for _ <- 1..pool_size do
spawn_link(fn -> write_loop(path, timestamp) end)
end
pool_tuple = List.to_tuple(pool_list)
{modules, _} =
Enum.flat_map_reduce(result, 0, fn
{{:module, module}, {binary, _}}, scheduler when is_binary(binary) ->
send(elem(pool_tuple, scheduler), {:write, module, binary})
{[module], rem(scheduler + 1, pool_size)}
_, scheduler ->
{[], scheduler}
end)
pool_refs =
for pid <- pool_list do
ref = Process.monitor(pid)
send(pid, :done)
ref
end
for ref <- pool_refs do
receive do
{:DOWN, ^ref, _, _, _} -> :ok
end
end
modules
end)
end
defp write_module_binaries(result, _output, _timestamp) do
defp write_module_binaries(result, _output, _state) do
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
end
defp write_loop(path, timestamp) do
receive do
{:write, module, binary} ->
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
File.write!(full_path, binary, [:raw])
if timestamp, do: File.touch!(full_path, timestamp)
write_loop(path, timestamp)
:done ->
:ok
end
end
## Verification
defp verify_modules(result, compile_warnings, dependent_modules, state) do
modules = write_module_binaries(result, state.output, state.beam_timestamp)
_ = state.after_compile.()
modules = write_module_binaries(result, state.output, state)
profile(state, "after compile callback", state.after_compile)
runtime_warnings =
{runtime_warnings, errors} =
if state.verification? do
profile(
state,
@@ -375,11 +470,19 @@ defmodule Kernel.ParallelCompiler do
fn -> Module.ParallelChecker.verify(state.checker, dependent_modules) end
)
else
[]
{[], []}
end
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
{{:ok, modules, info}, state}
case errors do
[] ->
{{:ok, modules, info}, state}
_ ->
IO.puts(:stderr, "== Type checking failed with errors ==")
{{:error, errors, info}, state}
end
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
@@ -486,11 +589,11 @@ defmodule Kernel.ParallelCompiler do
case cycle_return do
{:runtime, dependent_modules, extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
state.purge_compiler_modules.()
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
{:compile, [], extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
state.purge_compiler_modules.()
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more, extra_warnings} ->
@@ -737,7 +840,7 @@ defmodule Kernel.ParallelCompiler do
end
end
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
timer_ref = :erlang.send_after(state.long_compilation_threshold, self(), :threshold_check)
state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
@@ -795,8 +898,7 @@ defmodule Kernel.ParallelCompiler do
end
defp return_error(warnings, errors, state, fun) do
# Also prune compiler modules in case of errors
:elixir_code_server.cast(:purge_compiler_modules)
state.purge_compiler_modules.()
errors =
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
+22 -34
View File
@@ -1593,9 +1593,9 @@ defmodule Kernel.SpecialForms do
Let's give it a try on IEx:
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
iex> opts = %{"width" => 10, "height" => 15}
iex> with {:ok, width} <- Map.fetch(opts, "width"),
...> {:ok, height} <- Map.fetch(opts, "height") do
...> {:ok, width * height}
...> end
{:ok, 150}
@@ -1603,21 +1603,13 @@ defmodule Kernel.SpecialForms do
If all clauses match, the `do` block is executed, returning its result.
Otherwise the chain is aborted and the non-matched value is returned:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
iex> opts = %{"width" => 10}
iex> with {:ok, width} <- Map.fetch(opts, "width"),
...> {:ok, height} <- Map.fetch(opts, "height") do
...> {:ok, width * height}
...> end
:error
Guards can be used in patterns as well:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...> {:ok, to_string(role)}
...> end
{:ok, "admin"}
As in `for/1`, variables bound inside `with/1` won't be accessible
outside of `with/1`.
@@ -1661,22 +1653,18 @@ defmodule Kernel.SpecialForms do
An `else` option can be given to modify what is being returned from
`with` in the case of a failed match:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
with {:ok, content} <- File.read(path),
:ok <- File.write(path, [content, "!"]) do
:ok
else
{:error, reason} ->
Logger.error("could not append ! to \#{path} with reason: \#{reason}")
:error
end
The `else` block works like a `case`: it can have multiple clauses,
and the first match will be used. Variables bound inside `with` (such as
`width` in this example) are not available in the `else` block.
and the first match will be used. Variables bound inside `with`
(such as `content` in this example) are not available in the `else` block.
If an `else` block is used and there are no matching clauses, a `WithClauseError`
exception is raised.
@@ -1987,13 +1975,13 @@ defmodule Kernel.SpecialForms do
While it is not possible to match against multiple patterns in a single
clause, it's possible to match against multiple values by using guards:
iex> case :two do
...> value when value in [:one, :two] ->
iex> case 2 do
...> value when value in [1, 2] ->
...> "#{value} has been matched"
...> :three ->
...> "three has been matched"
...> 3 ->
...> "3 has been matched"
...> end
"two has been matched"
"2 has been matched"
"""
defmacro case(condition, clauses), do: error!([condition, clauses])
@@ -2355,7 +2343,7 @@ defmodule Kernel.SpecialForms do
defmacro try(args), do: error!([args])
@doc """
Checks if there is a message matching any of the given clauses in the current
Consumes the first message matching any of the given clauses in the current
process mailbox.
If there is no matching message, the current process waits until a matching
+24 -6
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
)
@@ -877,7 +887,15 @@ defmodule Kernel.Typespec do
defp typespec({:fun, meta, args}, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, location(meta), :fun, args}, state}
if args != [] do
IO.warn(
"fun/#{length(args)} is not valid in typespecs. Either specify fun() or use (... -> return) instead",
caller
)
end
{{:type, location(meta), :fun, []}, state}
end
defp typespec({:..., _meta, _args}, _vars, caller, _state) do
+8 -7
View File
@@ -126,10 +126,11 @@ defmodule Kernel.Utils do
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
try do
:elixir_quote.escape(val, :none, false)
:elixir_quote.escape(val, {:struct, module}, false)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
raise ArgumentError,
"invalid default value for struct field #{key}, " <> Exception.message(e)
else
_ -> {key, val}
end
@@ -171,7 +172,7 @@ defmodule Kernel.Utils do
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :none, false)
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
body =
case bootstrapped? do
@@ -217,7 +218,7 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
mapper = fn {key, val} ->
%{field: key, default: val}
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
@@ -265,7 +266,7 @@ defmodule Kernel.Utils do
module.exception([])
end
def raise(%_{__exception__: true} = exception) do
def raise(%_{__exception__: _} = exception) do
exception
end
@@ -338,7 +339,7 @@ defmodule Kernel.Utils do
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
end
end
end
@@ -368,7 +369,7 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, module) do
defp unquote_refs_once(guard, refs, %{module: module}) do
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
+29 -19
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
@@ -437,7 +446,7 @@ defmodule Keyword do
end
@doc """
Gets the value from `key` and updates it, all in one pass.
Gets the value for `key` and updates it in one pass, deleting duplicate keys.
The `fun` argument receives the value of `key` (or `nil` if `key`
is not present) and must return a two-element tuple: the current value
@@ -483,7 +492,7 @@ defmodule Keyword do
defp get_and_update([{key, current} | t], acc, key, fun) do
case fun.(current) do
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | t])}
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop ->
{current, :lists.reverse(acc, t)}
@@ -509,7 +518,8 @@ defmodule Keyword do
end
@doc """
Gets the value under `key` and updates it. Raises if there is no `key`.
Gets the value for `key` and updates it in one pass, deleting duplicate keys,
raising if `key` can't be found in `keywords`.
The `fun` argument receives the value under `key` and must return a
two-element tuple: the current value (the retrieved value, which can be
@@ -545,21 +555,21 @@ defmodule Keyword do
get_and_update!(keywords, key, fun, [])
end
defp get_and_update!([{key, value} | keywords], key, fun, acc) do
defp get_and_update!([{key, value} | t], key, fun, acc) do
case fun.(value) do
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
:pop ->
{value, :lists.reverse(acc, keywords)}
{value, :lists.reverse(acc, t)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
get_and_update!(keywords, key, fun, [e | acc])
defp get_and_update!([{_, _} = h | t], key, fun, acc) do
get_and_update!(t, key, fun, [h | acc])
end
defp get_and_update!([], key, _fun, acc) when is_atom(key) do
@@ -955,7 +965,7 @@ defmodule Keyword do
iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
true
Comparison between values is done with `===/3`,
Comparison between values is done with `===/2`,
which means integers are not equivalent to floats:
iex> Keyword.equal?([a: 1.0], [a: 1])
+61 -5
View File
@@ -187,9 +187,10 @@ defmodule List do
"""
@spec duplicate(any, 0) :: []
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
def duplicate(elem, n) do
:lists.duplicate(n, elem)
end
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, [])
defp duplicate(0, _elem, acc), do: acc
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
@doc """
Flattens the given `list` of nested lists.
@@ -297,6 +298,33 @@ defmodule List do
def first([], default), do: default
def first([head | _], _default), do: head
@doc """
Returns the first element in `list`.
If `list` is empty, an error is raised.
## Examples
iex> List.first!([1])
1
iex> List.first!([1, 2, 3])
1
iex> List.first!([])
** (ArgumentError) attempted to get the first element of an empty list
"""
@doc since: "1.20.0"
@spec first!([elem, ...]) :: elem when elem: var
def first!(list)
def first!([head | _]), do: head
def first!([]) do
raise ArgumentError, "attempted to get the first element of an empty list"
end
@doc """
Returns the last element in `list` or `default` if `list` is empty.
@@ -325,6 +353,34 @@ defmodule List do
def last([head], _default), do: head
def last([_ | tail], default), do: last(tail, default)
@doc """
Returns the last element in `list`.
If `list` is empty, an error is raised.
## Examples
iex> List.last!([1])
1
iex> List.last!([1, 2, 3])
3
iex> List.last!([])
** (ArgumentError) attempted to get the last element of an empty list
"""
@doc since: "1.20.0"
@spec last!([elem, ...]) :: elem when elem: var
def last!(list)
def last!([head]), do: head
def last!([_ | tail]), do: last!(tail)
def last!([]) do
raise ArgumentError, "attempted to get the last element of an empty list"
end
@doc """
Receives a list of tuples and returns the first tuple
where the element at `position` in the tuple matches the
@@ -915,7 +971,7 @@ defmodule List do
If `prefix` is an empty list, it returns `true`.
### Examples
## Examples
iex> List.starts_with?([1, 2, 3], [1, 2])
true
@@ -945,7 +1001,7 @@ defmodule List do
If `suffix` is an empty list, it returns `true`.
### Examples
## Examples
iex> List.ends_with?([1, 2, 3], [2, 3])
true
-6
View File
@@ -19,12 +19,6 @@ defprotocol List.Chars do
"""
@spec to_charlist(t) :: charlist
def to_charlist(term)
@doc false
@deprecated "Use List.Chars.to_charlist/1 instead"
Kernel.def to_char_list(term) do
__MODULE__.to_charlist(term)
end
end
defimpl List.Chars, for: Atom do
+166 -56
View File
@@ -197,6 +197,16 @@ defmodule Macro do
@typedoc "A captured remote function in the format of &Mod.fun/arity"
@type captured_remote_function :: fun
@type escape_opts :: [
unquote: boolean(),
prune_metadata: boolean(),
generated: boolean()
]
@type inspect_atom_opts :: [
escape: (binary(), char() -> binary())
]
@doc """
Breaks a pipeline expression into a list.
@@ -498,13 +508,11 @@ defmodule Macro do
Generates AST nodes for a given number of required argument
variables using `Macro.unique_var/2`.
The second argument is generally the macro caller's module.
## Examples
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
iex> {:arg1, [counter: c1], __MODULE__} = var1
iex> {:arg2, [counter: c2], __MODULE__} = var2
iex> is_integer(c1) and is_integer(c2)
true
[var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module)
"""
@doc since: "1.11.3"
@@ -560,11 +568,11 @@ defmodule Macro do
generate another variable, with its own unique counter.
See `var/2` for an alternative.
The second argument is generally the macro caller's module.
## Examples
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
iex> is_integer(c)
true
var = Macro.unique_var(:foo, __CALLER__.module)
"""
@doc since: "1.11.3"
@@ -793,12 +801,18 @@ defmodule Macro do
* `:unquote` - when `true`, this function leaves `unquote/1` and
`unquote_splicing/1` expressions unescaped, effectively unquoting
the contents on escape. This option is useful only when escaping
ASTs which may have quoted fragments in them. Defaults to `false`.
ASTs which may have quoted fragments in them. Note this option
will give a special meaning to `quote`/`unquote` nodes, which need
to be valid AST before escaping. Defaults to `false`.
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
nodes. Note this option changes the semantics of escaped code and
it should only be used when escaping ASTs. Defaults to `false`.
* `:generated` - (since v1.19.0) Whether the AST should be considered as generated
by the compiler or not. This means the compiler and tools like Dialyzer may not
emit certain warnings.
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
assertion, so when an assertion fails we can show code snippets to users.
Without this option, each time the test module is compiled, we would get a
@@ -834,12 +848,82 @@ defmodule Macro do
`escape/2` is used to escape *values* (either directly passed or variable
bound), while `quote/2` produces syntax trees for
expressions.
## Dealing with references and other runtime values
Macros work at compile-time and therefore `Macro.escape/1` can only escape values
that are valid during compilation, such as numbers, atoms, tuples, maps, binaries,
etc.
However, you may have values at compile-time which cannot be escaped, such as
`reference`s and `pid`s, since the process or memory address they point to will
no longer exist once compilation completes. Attempting to escape said values will
raise an exception. This is a common issue when working with NIFs.
Luckily, Elixir v1.19 introduces a mechanism that allows those values to be escaped,
as long as they are encapsulated by a struct within a module that defines the
`__escape__/1` function. This is possible as long as the reference has a natural
text or binary representation that can be serialized during compilation.
Let's imagine we have the following struct:
defmodule WrapperStruct do
defstruct [:ref]
def new(...), do: %WrapperStruct{ref: ...}
# efficiently dump to / load from binaries
def dump_to_binary(%WrapperStruct{ref: ref}), do: ...
def load_from_binary(binary), do: %WrapperStruct{ref: ...}
end
Such a struct could not be used in module attributes or escaped with `Macro.escape/2`:
defmodule Foo do
@my_struct WrapperStruct.new(...)
def my_struct, do: @my_struct
end
** (ArgumentError) cannot inject attribute @my_struct into function/macro because cannot escape #Reference<...>
To address this, structs can re-define how they should be escaped by defining a custom
`__escape__/1` function which returns the AST. In our example:
defmodule WrapperStruct do
# ...
def __escape__(struct) do
# dump to a binary representation at compile-time
binary = dump_to_binary(struct)
quote do
# load from the binary representation at runtime
WrapperStruct.load_from_binary(unquote(Macro.escape(binary)))
end
end
end
Now, our example above will be expanded as:
def my_struct, do: WrapperStruct.load_from_binary(<<...>>)
When implementing `__escape__/1`, you must ensure that the quoted expression
will evaluate to a struct that represents the one given as argument.
"""
@spec escape(term, keyword) :: t()
@spec escape(term, escape_opts) :: t()
def escape(expr, opts \\ []) do
unquote = Keyword.get(opts, :unquote, false)
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
:elixir_quote.escape(expr, kind, unquote)
kind = if Keyword.get(opts, :prune_metadata, false), do: :escape_and_prune, else: :escape
generated = Keyword.get(opts, :generated, false)
case :elixir_quote.escape(expr, kind, unquote) do
# mark module attrs as shallow-generated since the ast for their representation
# might contain opaque terms
{caller, meta, args} when generated and is_list(meta) ->
{caller, [generated: true] ++ meta, args}
ast ->
ast
end
end
# TODO: Deprecate me on Elixir v1.22
@@ -852,25 +936,50 @@ defmodule Macro do
end
@doc """
Extracts the struct information (equivalent to calling
`module.__info__(:struct)`).
Extracts the struct information.
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is also capable of
expanding structs defined under the module being compiled.
not have been compiled (including structs in the defined
under the module being compiled). For compiled modules,
it will invoke `module.__info__(:struct)`.
Calling this function also adds an export dependency on the
given struct.
It will raise `ArgumentError` if the struct is not available.
## Compatibility considerations
This function currently returns both `:required` and `:default`
entries for each field. While this naming is inconsistent
(a required field should not have a default), this is done for
backwards compatibility purposes.
In future releases, Elixir may introduce truly required struct
fields, the required field will be removed and default will be
present only if the field is optional. Your code should prepare
for such scenario accordingly.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
[%{field: atom(), required: boolean(), default: term()}]
[
%{
required(:field) => atom(),
optional(:required) => boolean(),
optional(:default) => term()
}
]
def struct_info!(module, env) when is_atom(module) do
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
meta = [line: env.line]
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do
{:ok, info} ->
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
info
{:error, desc} ->
raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
end
@@ -1738,12 +1847,17 @@ defmodule Macro do
@doc """
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
This is used when you want to programmatically invoke a macro at
compile-time.
This is used when you want to dynamically invoke a function at
compile-time and force it to be tracked as a compile-time dependency.
For example, this is used by `dbg/1` to force the `dbg_callback`
configuration to be a compile-time dependency.
If you want to "invoke" a macro instead, remember macros are by
definition compile-time, and you can use `Macro.expand/2`.
"""
@doc since: "1.16.0"
def compile_apply(mod, fun, args, caller) do
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
Kernel.apply(mod, fun, args)
end
@@ -2082,7 +2196,7 @@ defmodule Macro do
Please check `expand_literals/2` for use cases and pitfalls.
"""
@doc since: "1.14.1"
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: {t(), acc} when acc: term()
def expand_literals(ast, acc, fun)
def expand_literals({:__aliases__, meta, args}, acc, fun) do
@@ -2399,7 +2513,7 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
@spec inspect_atom(:literal | :key | :remote_call, atom, inspect_atom_opts) :: binary
def inspect_atom(source_format, atom, opts \\ [])
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
@@ -2591,7 +2705,7 @@ defmodule Macro do
:ok
end
prelude = quote do: options = unquote(Macro.escape(options))
prelude = quote do: options = unquote(options)
acc = {prelude, dbg_format_header(env)}
{acc, nil} =
@@ -2612,34 +2726,29 @@ defmodule Macro do
# Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
value_var = unique_var(:value, __MODULE__)
values_acc_var = unique_var(:values, __MODULE__)
[start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
[start_ast | rest_asts] = for {ast, 0} <- unpipe(pipe_ast), do: ast
piped_rest_asts = Enum.map(rest_asts, &{&1, pipe(value_var, &1, 0)})
initial_acc =
first_entry =
quote do
unquote(value_var) = unquote(start_ast)
unquote(values_acc_var) = [unquote(value_var)]
{:multi_value, unquote(escape(start_ast)), unquote(value_var)}
end
values_ast =
for step_ast <- rest_asts, reduce: initial_acc do
ast_acc ->
quote do
unquote(ast_acc)
unquote(value_var) = unquote(step_ast)
unquote(values_acc_var) = [unquote(value_var) | unquote(values_acc_var)]
end
end
len = length(piped_rest_asts)
[
quote do
unquote(values_ast)
pipe_entries =
Enum.with_index(piped_rest_asts, fn {original_ast, step_ast}, i ->
tag = if i + 1 == len, do: :pipe_end, else: :pipe
{:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
end
]
quote do
unquote(value_var) = unquote(step_ast)
{unquote(tag), unquote(escape(original_ast)), unquote(value_var)}
end
end)
[first_entry | pipe_entries]
end
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
@@ -2859,18 +2968,19 @@ defmodule Macro do
result
end
defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
result = List.last(values)
code_strings = Enum.map(code_asts, &to_string_with_colors(&1, options))
[{first_ast, first_value} | asts_with_values] = Enum.zip(code_strings, values)
first_formatted = [dbg_format_ast(first_ast), " ", inspect(first_value, options), ?\n]
defp dbg_format_ast_to_debug({:pipe, code_ast, value}, options) do
formatted = [
[:faint, "|> ", :reset],
dbg_format_ast_with_value_no_newline(code_ast, value, options)
]
rest_formatted =
Enum.map(asts_with_values, fn {code_ast, value} ->
[:faint, "|> ", :reset, dbg_format_ast(code_ast), " ", inspect(value, options), ?\n]
end)
{formatted, value}
end
{[first_formatted | rest_formatted], result}
defp dbg_format_ast_to_debug({:pipe_end, code_ast, value}, options) do
{formatted, value} = dbg_format_ast_to_debug({:pipe, code_ast, value}, options)
{[formatted, ?\n], value}
end
defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do
+65 -13
View File
@@ -70,6 +70,42 @@ defmodule Macro.Env do
@typep tracers :: [module]
@typep versioned_vars :: %{optional(variable) => var_version :: non_neg_integer}
@type define_import_opts :: [
trace: boolean(),
emit_warnings: boolean(),
info_callback: (atom() -> [{atom(), arity()}]),
only: :functions | :macros | [{atom(), arity()}],
except: [{atom(), arity()}],
warn: boolean()
]
@type define_alias_opts :: [
trace: boolean(),
as: atom(),
warn: boolean()
]
@type define_require_opts :: [
trace: boolean(),
as: atom(),
warn: boolean()
]
@type expand_alias_opts :: [
trace: boolean()
]
@type expand_import_opts :: [
allow_locals: boolean() | (-> function() | false),
check_deprecations: boolean(),
trace: boolean()
]
@type expand_require_opts :: [
check_deprecations: boolean(),
trace: boolean()
]
@type t :: %{
__struct__: __MODULE__,
aliases: aliases,
@@ -264,7 +300,7 @@ defmodule Macro.Env do
iex> Macro.Env.required?(__ENV__, Integer)
false
iex> require Integer
iex> require Integer, warn: false
iex> Macro.Env.required?(__ENV__, Integer)
true
@@ -331,7 +367,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module) :: {:ok, t}
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
{:ok, t} | {:error, String.t()}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -391,7 +428,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
@spec define_import(t, Macro.metadata(), module, define_import_opts) ::
{:ok, t} | {:error, String.t()}
def define_import(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -441,7 +479,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
@spec define_alias(t, Macro.metadata(), module, define_alias_opts) ::
{:ok, t} | {:error, String.t()}
def define_alias(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -487,7 +526,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_alias(t, keyword, [atom()], keyword) ::
@spec expand_alias(t, keyword, [atom()], expand_alias_opts) ::
{:alias, atom()} | :error
def expand_alias(env, meta, list, opts \\ [])
when is_list(meta) and is_list(list) and is_list(opts) do
@@ -517,8 +556,15 @@ defmodule Macro.Env do
## Options
* `:allow_locals` - when set to `false`, it does not attempt to capture
local macros defined in the current module in `env`
* `:allow_locals` - controls how local macros are resolved.
Defaults to `true`.
- When `false`, does not attempt to capture local macros defined in the
current module in `env`
- When `true`, uses a default resolver that looks for public macros in
the current module
- When a function, it will be invoked to lazily compute a local function
(or return false). It has signature `(-> function() | false)`
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
@@ -527,7 +573,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
@spec expand_import(t, keyword, atom(), arity(), expand_import_opts) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| {:function, module(), atom()}
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
@@ -542,10 +588,16 @@ defmodule Macro.Env do
trace = Keyword.get(opts, :trace, true)
module = env.module
# When allow_locals is a callback, we don't need to pass module macros as extra
# because the callback will handle local macro resolution
extra =
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
if is_function(allow_locals, 0) do
[]
else
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
end
end
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
@@ -583,7 +635,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
@spec expand_require(t, keyword, module(), atom(), arity(), expand_require_opts) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| :error
def expand_require(env, meta, module, name, arity, opts \\ [])
@@ -606,7 +658,7 @@ defmodule Macro.Env do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end
quoted = expander.(args, env)
quoted = expander.(:elixir_dispatch.stop_generated(args), env)
next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end
+104 -53
View File
@@ -287,8 +287,13 @@ defmodule Map do
@doc """
Fetches the value for a specific `key` in the given `map`.
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
If `map` doesn't contain `key`, `:error` is returned.
If `map` contains the given `key` then its value is returned
in the shape of `{:ok, value}`. If `map` doesn't contain `key`,
`:error` is returned.
If the type system can verify `:error` is always returned
(which means key is never available in the map), it will emit
an error.
Inlined by the compiler.
@@ -296,7 +301,7 @@ defmodule Map do
iex> Map.fetch(%{a: 1}, :a)
{:ok, 1}
iex> Map.fetch(%{a: 1}, :b)
iex> Map.fetch(%{"foo" => "bar"}, "unknown")
:error
"""
@@ -307,8 +312,11 @@ defmodule Map do
Fetches the value for a specific `key` in the given `map`, erroring out if
`map` doesn't contain `key`.
If `map` contains `key`, the corresponding value is returned. If
`map` doesn't contain `key`, a `KeyError` exception is raised.
The exclamation mark (`!`) implies this function can raise a `KeyError`
exception at runtime if `map` doesn't contain `key`. If the type system
can verify this function will always raise (which means the key is never
available), then it will emit a warning at compile-time. See the "Type
checking" section below.
Inlined by the compiler.
@@ -317,11 +325,54 @@ defmodule Map do
iex> Map.fetch!(%{a: 1}, :a)
1
When the key is missing, an exception is raised:
Map.fetch!(%{a: 1}, :b)
** (KeyError) key :b not found in: %{a: 1}
## Type checking
The compiler will emit a warning if it can verify that
none of the keys given are available in the map.
When the key is an atom, because only single key is given,
a warning will be emitted in case the type system proves
the key is not present.
However, this behaviour matters when the type of the key
represents multiple values. For example:
key = returns_foo_or_bar() #=> :foo or :bar
Map.fetch!(%{foo: 123}, key)
Although the key can be `:foo` or `:bar`, there is no
warning emitted, as `:foo` will succeed. This is by design:
the exclamation mark in Elixir denotes precisely that a
runtime exception may be raised.
In case you are looking up multiple keys and you don't know
if they may be present, you can use `Map.fetch/2` instead
and deal with the error case accordingly:
case Map.fetch(%{foo: 123}, key) do
{:ok, value} -> ...
:error -> ...
end
Both `Map.fetch!/2` and `Map.fetch/2` will emit a warning if
it proves that both `:foo` or `:bar` are absent in the map.
Alternatively, if you want to statically prove that all of keys
are in the map, you can match on the possible values and access
them directly:
case returns_foo_or_bar() do
:foo -> map.foo
:bar -> map.bar
end
"""
@spec fetch!(map, key) :: value
def fetch!(map, key) do
:maps.get(key, map)
end
def fetch!(map, key), do: :maps.get(key, map)
@doc """
Puts the given `value` under `key` unless the entry `key`
@@ -357,8 +408,8 @@ defmodule Map do
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace(%{a: 1}, :b, 2)
%{a: 1}
iex> Map.replace(%{"a" => 1}, "b", 2)
%{"a" => 1}
"""
@doc since: "1.11.0"
@@ -379,7 +430,11 @@ defmodule Map do
@doc """
Puts a value under `key` only if the `key` already exists in `map`.
If `key` is not present in `map`, a `KeyError` exception is raised.
The exclamation mark (`!`) implies this function can raise a `KeyError`
exception at runtime if `map` doesn't contain `key`. If the type system
can verify this function will always raise (which means the key is never
available), then it will emit a warning at compile-time. See the "Type
checking" section in `Map.fetch!/2` for more information.
Inlined by the compiler.
@@ -388,8 +443,8 @@ defmodule Map do
iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace!(%{a: 1}, :b, 2)
** (KeyError) key :b not found in:
iex> Map.replace!(%{"foo" => "bar"}, "unknown", "new_bar")
** (KeyError) key "unknown" not found in:
...
"""
@@ -412,8 +467,8 @@ defmodule Map do
iex> Map.replace_lazy(%{a: 1, b: 2}, :a, fn v -> v * 4 end)
%{a: 4, b: 2}
iex> Map.replace_lazy(%{a: 1, b: 2}, :c, fn v -> v * 4 end)
%{a: 1, b: 2}
iex> Map.replace_lazy(%{"a" => 1, "b" => 2}, "c", fn v -> v * 4 end)
%{"a" => 1, "b" => 2}
"""
@doc since: "1.14.0"
@@ -492,6 +547,8 @@ defmodule Map do
:erlang.error({:badmap, non_map})
end
defp take([], _map, []), do: %{}
defp take([], _map, acc) do
:maps.from_list(acc)
end
@@ -516,15 +573,13 @@ defmodule Map do
## Examples
iex> Map.get(%{}, :a)
nil
iex> Map.get(%{a: 1}, :a)
iex> Map.get(%{"a" => 1}, "a")
1
iex> Map.get(%{a: 1}, :b)
iex> Map.get(%{"a" => 1}, "b")
nil
iex> Map.get(%{a: 1}, :b, 3)
iex> Map.get(%{"a" => 1}, "b", 3)
3
iex> Map.get(%{a: nil}, :a, 1)
iex> Map.get(%{"a" => nil}, "a", 1)
nil
"""
@@ -553,15 +608,11 @@ defmodule Map do
## Examples
iex> map = %{a: 1}
iex> fun = fn ->
...> # some expensive operation here
...> 13
...> end
iex> Map.get_lazy(map, :a, fun)
iex> Map.get_lazy(%{a: 1}, :a, fn -> :expensive_value end)
1
iex> Map.get_lazy(map, :b, fun)
13
iex> Map.get_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
:expensive_value
"""
@spec get_lazy(map, key, (-> value)) :: value
@@ -700,10 +751,10 @@ defmodule Map do
iex> Map.pop(%{a: 1}, :a)
{1, %{}}
iex> Map.pop(%{a: 1}, :b)
{nil, %{a: 1}}
iex> Map.pop(%{a: 1}, :b, 3)
{3, %{a: 1}}
iex> Map.pop(%{"a" => 1}, "b")
{nil, %{"a" => 1}}
iex> Map.pop(%{"a" => 1}, "b", 3)
{3, %{"a" => 1}}
"""
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
@@ -726,8 +777,8 @@ defmodule Map do
{1, %{}}
iex> Map.pop!(%{a: 1, b: 2}, :a)
{1, %{b: 2}}
iex> Map.pop!(%{a: 1}, :b)
** (KeyError) key :b not found in:
iex> Map.pop!(%{"a" => 1}, "b")
** (KeyError) key "b" not found in:
...
"""
@@ -753,15 +804,11 @@ defmodule Map do
## Examples
iex> map = %{a: 1}
iex> fun = fn ->
...> # some expensive operation here
...> 13
...> end
iex> Map.pop_lazy(map, :a, fun)
iex> Map.pop_lazy(%{a: 1}, :a, fn -> :expensive_value end)
{1, %{}}
iex> Map.pop_lazy(map, :b, fun)
{13, %{a: 1}}
iex> Map.pop_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
{:expensive_value, %{"a" => 1}}
"""
@spec pop_lazy(map, key, (-> value)) :: {value, map}
@@ -913,8 +960,8 @@ defmodule Map do
iex> Map.update!(%{a: 1}, :a, &(&1 * 2))
%{a: 2}
iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
** (KeyError) key :b not found in:
iex> Map.update!(%{"a" => 1}, "b", &(&1 * 2))
** (KeyError) key "b" not found in:
...
"""
@@ -1033,6 +1080,7 @@ defmodule Map do
#=> %{name: "john"}
"""
# TODO: implement this using row polymorphism
@spec from_struct(atom | struct) :: map
def from_struct(struct) when is_atom(struct) do
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
@@ -1061,7 +1109,7 @@ defmodule Map do
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
false
Comparison between keys and values is done with `===/3`,
Comparison between keys and values is done with `===/2`,
which means integers are not equivalent to floats:
iex> Map.equal?(%{a: 1.0}, %{a: 1})
@@ -1069,11 +1117,7 @@ defmodule Map do
"""
@spec equal?(map, map) :: boolean
def equal?(map1, map2)
def equal?(%{} = map1, %{} = map2), do: map1 === map2
def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2])
def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other])
@doc false
@deprecated "Use Kernel.map_size/1 instead"
@@ -1094,9 +1138,9 @@ defmodule Map do
> #### Performance considerations {: .tip}
>
> If you find yourself doing multiple calls to `Map.filter/2`
> and `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.map/2` and `Enum.filter/2` instead and convert to
> a map at the end using `Map.new/1`.
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
> a map at the end using `Map.new/1` or `Map.new/2`.
## Examples
@@ -1128,6 +1172,13 @@ defmodule Map do
See also `filter/2`.
> #### Performance considerations {: .tip}
>
> If you find yourself doing multiple calls to `Map.filter/2`
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
> a map at the end using `Map.new/1` or `Map.new/2`.
## Examples
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
+5 -1
View File
@@ -55,7 +55,11 @@ defmodule MapSet do
@type value :: term
@opaque internal(value) :: :sets.set(value)
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
# either via module attributes or by the compiler.
# Defaulting to a broad `term()` type to prevent opaqueness violations.
@typep internal(_value) :: term()
@type t(value) :: %__MODULE__{map: internal(value)}
@type t :: t(term)
+84 -61
View File
@@ -402,30 +402,21 @@ defmodule Module do
Accepts the function name (as an atom) of a function in the current module.
The function must have an arity of 0 (no arguments). If the function does
not return `:ok`, the loading of the module will be aborted.
For example:
not return `:ok`, the loading of the module will be aborted. Its primary
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
defmodule MyModule do
@on_load :load_check
@on_load :load_external_code
def load_check do
if some_condition() do
:ok
else
:abort
end
end
def some_condition do
false
def load_external_code do
:erlang.load_nif(~c"path/to/extension.so_or_dll")
end
end
The function given to `on_load` should avoid calling functions from
other modules. If you must call functions in other modules and those
modules are defined within the same project, the called modules must
have the `@compile {:autoload, true}` annotation, so they are loaded
upfront (and not from within the `@on_load` callback).
other modules. This is because, when running a `mix release`,
`on_load` runs extremely early, before any application starts running,
and therefore even systems like the `Logger` and `IO` are not yet available.
### `@vsn`
@@ -560,15 +551,20 @@ defmodule Module do
callback is invoked under different scenarios, Elixir provides no guarantees
of when in the compilation cycle nor in which process the callback runs.
Furthermore, after verification callbacks are not expected to raise.
Given they run after the code is compiled, artifacts have already been
written to disk, and therefore raising does not effectively halt compilation
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
or other callback. Given modules have already been compiled, functions in
this module, such as `get_attribute/2`, which expect modules to not have been
yet compiled, do not work on `@after_verify` callback.
Accepts a module or a `{module, function_name}` tuple. The function
must take one argument: the module name. When just a module is provided,
the function is assumed to be `__after_verify__/1`.
Callbacks will run in the order they are registered.
`Module` functions expecting not yet compiled modules are no longer available
at the time `@after_verify` is invoked.
#### Example
defmodule MyModule do
@@ -660,7 +656,8 @@ defmodule Module do
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: module()
@spec module_info(:nifs) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info]
@@ -681,12 +678,21 @@ defmodule Module do
This function is generated for all modules. It's similar to `module_info/1` but
includes some additional Elixir-specific information, such as struct and macro
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@type definition :: {function_name :: atom, arity}
@type def_kind :: :def | :defp | :defmacro | :defmacrop
@type create_opts :: [
file: binary(),
line: pos_integer(),
generated: boolean()
]
@type get_definition_opts :: [
skip_clauses: boolean()
]
@extra_error_msg_defines? "Use Kernel.function_exported?/3 and Kernel.macro_exported?/3 " <>
"to check for public functions and macros instead"
@@ -711,7 +717,8 @@ defmodule Module do
* `:module` - the module atom name
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order.
See `Macro.struct_info!/2` for more information
"""
@callback __info__(:attributes) :: keyword()
@@ -721,7 +728,14 @@ defmodule Module do
@callback __info__(:md5) :: binary()
@callback __info__(:module) :: module()
@callback __info__(:struct) ::
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
[
%{
required(:field) => atom(),
optional(:required) => boolean(),
optional(:default) => term()
}
]
| nil
@doc """
Returns information about module attributes used by Elixir.
@@ -918,7 +932,7 @@ defmodule Module do
when defining the module, while `Kernel.defmodule/2`
automatically uses the environment it is invoked at.
"""
@spec create(module, Macro.t(), Macro.Env.t() | keyword) :: {:module, module, binary, term}
@spec create(module, Macro.t(), Macro.Env.t() | create_opts) :: {:module, module, binary, term}
def create(module, quoted, opts)
def create(module, quoted, %Macro.Env{} = env) when is_atom(module) do
@@ -945,7 +959,7 @@ defmodule Module do
It handles binaries and atoms.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -971,7 +985,7 @@ defmodule Module do
It handles binaries and atoms. If one of the aliases
is nil, it is discarded.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1002,7 +1016,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1026,7 +1040,7 @@ defmodule Module do
If the alias was not referenced yet, fails with `ArgumentError`.
It handles binaries and atoms.
> #### Untracked compile-time dependencies {. :warning}
> #### Untracked compile-time dependencies {: .warning}
>
> Use this function with care, as dynamically defining
> module names at compilation time may lead to
@@ -1236,17 +1250,18 @@ defmodule Module do
"""
@spec defines?(module, definition) :: boolean
def defines?(module, {name, arity} = tuple)
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 do
def defines?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
:ets.member(set, {:def, tuple})
:ets.member(set, {:def, definition})
end
@doc """
Checks if the module defines a function or macro of the
given `kind`.
given kind.
`kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
`def_kind` can be any of `:def`, `:defp`, `:defmacro`, or `:defmacrop`.
This function can only be used on modules that have not yet been compiled.
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
@@ -1262,12 +1277,13 @@ defmodule Module do
"""
@spec defines?(module, definition, def_kind) :: boolean
def defines?(module, {name, arity} = tuple, def_kind)
when is_atom(module) and is_atom(name) and is_integer(arity) and arity >= 0 and arity <= 255 and
def defines?(module, {function_name, arity} = definition, def_kind)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 and
def_kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _bag} = data_tables_for!(module, __ENV__.function, @extra_error_msg_defines?)
case :ets.lookup(set, {:def, tuple}) do
case :ets.lookup(set, {:def, definition}) do
[{_, ^def_kind, _, _, _, _}] -> true
_ -> false
end
@@ -1280,7 +1296,9 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec defines_type?(module, definition) :: boolean
def defines_type?(module, definition) when is_atom(module) do
def defines_type?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
Kernel.Typespec.defines_type?(module, definition)
end
@@ -1293,7 +1311,9 @@ defmodule Module do
"""
@doc since: "1.7.0"
@spec spec_to_callback(module, definition) :: boolean
def spec_to_callback(module, definition) do
def spec_to_callback(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
Kernel.Typespec.spec_to_callback(module, definition)
end
@@ -1326,7 +1346,7 @@ defmodule Module do
@doc """
Returns all overridable definitions in `module`.
Note a definition is included even if it was was already overridden.
Note a definition is included even if it was already overridden.
You can use `defines?/2` to see if a definition exists or one is pending.
This function can only be used on modules that have not yet been compiled.
@@ -1345,7 +1365,7 @@ defmodule Module do
"""
@doc since: "1.13.0"
@spec overridables_in(module) :: [atom]
@spec overridables_in(module) :: [definition]
def overridables_in(module) when is_atom(module) do
assert_not_compiled!(__ENV__.function, module, :all)
:elixir_overridable.overridables_for(module)
@@ -1394,10 +1414,10 @@ defmodule Module do
"""
@spec definitions_in(module, def_kind) :: [definition]
def definitions_in(module, kind)
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
def definitions_in(module, def_kind)
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
{set, _} = data_tables_for!(module, __ENV__.function, @extra_error_msg_definitions_in)
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
:ets.select(set, [{{{:def, :"$1"}, def_kind, :_, :_, :_, :_}, [], [:"$1"]}])
end
@doc """
@@ -1423,21 +1443,22 @@ defmodule Module do
only an interest in fetching the kind and the metadata
"""
@spec get_definition(module, definition, keyword) ::
@spec get_definition(module, definition, get_definition_opts) ::
{:v1, def_kind, meta :: keyword,
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
| nil
@doc since: "1.12.0"
def get_definition(module, {name, arity}, options \\ [])
when is_atom(module) and is_atom(name) and is_integer(arity) and is_list(options) do
def get_definition(module, {function_name, arity} = _definition, options \\ [])
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 and is_list(options) do
{set, bag} = data_tables_for!(module, __ENV__.function, "")
case :ets.lookup(set, {:def, {name, arity}}) do
case :ets.lookup(set, {:def, {function_name, arity}}) do
[{_key, kind, meta, _, _, _}] ->
clauses =
if options[:skip_clauses],
do: [],
else: bag_lookup_element(bag, {:clauses, {name, arity}}, 2)
else: bag_lookup_element(bag, {:clauses, {function_name, arity}}, 2)
{:v1, kind, meta, clauses}
@@ -1454,10 +1475,11 @@ defmodule Module do
"""
@doc since: "1.12.0"
@spec delete_definition(module, definition) :: boolean()
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
def delete_definition(module, {function_name, arity} = _definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
assert_not_compiled!(__ENV__.function, module, :writeable)
:elixir_def.take_definition(module, {name, arity}) != false
:elixir_def.take_definition(module, {function_name, arity}) != false
end
@doc """
@@ -1473,20 +1495,20 @@ defmodule Module do
given.
"""
@spec make_overridable(module, [definition]) :: :ok
def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do
def make_overridable(module, definitions) when is_atom(module) and is_list(definitions) do
assert_not_compiled!(__ENV__.function, module, :writeable)
func = fn
{function_name, arity} = tuple
{function_name, arity} = definition
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 ->
case :elixir_def.take_definition(module, tuple) do
case :elixir_def.take_definition(module, definition) do
false ->
raise ArgumentError,
"cannot make function #{function_name}/#{arity} " <>
"overridable because it was not defined"
clause ->
:elixir_overridable.record_overridable(module, tuple, clause)
:elixir_overridable.record_overridable(module, definition, clause)
end
other ->
@@ -1495,7 +1517,7 @@ defmodule Module do
"{function_name :: atom, arity :: 0..255} tuple, got: #{inspect(other)}"
end
:lists.foreach(func, tuples)
:lists.foreach(func, definitions)
end
@spec make_overridable(module, module) :: :ok
@@ -1553,9 +1575,10 @@ defmodule Module do
exists or one is pending.
"""
@spec overridable?(module, definition) :: boolean
def overridable?(module, {function_name, arity} = tuple)
when is_atom(function_name) and is_integer(arity) and arity >= 0 and arity <= 255 do
:elixir_overridable.overridable_for(module, tuple) != :not_overridable
def overridable?(module, {function_name, arity} = definition)
when is_atom(module) and is_atom(function_name) and is_integer(arity) and arity >= 0 and
arity <= 255 do
:elixir_overridable.overridable_for(module, definition) != :not_overridable
end
@doc """
+28 -4
View File
@@ -181,8 +181,16 @@ defmodule Module.Behaviour do
behaviour not in behaviours ->
{:error, {:behaviour_not_declared, behaviour}}
not Code.ensure_loaded?(behaviour) ->
# Module does not exist, but we have already warned about it.
{:ok, []}
not behaviour_defined?(callbacks, behaviour) ->
# Module does not define behaviour, but we have already warned about it.
{:ok, []}
true ->
{:error, {:behaviour_not_defined, behaviour, callbacks}}
{:error, {:callback_not_defined, behaviour, callbacks}}
end
end
@@ -215,6 +223,18 @@ defmodule Module.Behaviour do
end
end
# Determines whether there is at least one callback defined for the given behaviour.
# If not, that means that the behaviour has not been defined.
defp behaviour_defined?(callbacks, behaviour) do
callbacks
|> Map.values()
|> List.flatten()
|> Enum.any?(fn
{_kind, ^behaviour, _optional?} -> true
{_kind, _behaviour, _optional?} -> false
end)
end
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
when map_size(callbacks) == 0 do
context
@@ -390,13 +410,17 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
defp format_warning({:callback_not_defined, callback, kind, behaviour, callbacks}) do
behaviour_string = inspect(behaviour)
[
"got \"@impl ",
inspect(behaviour),
behaviour_string,
"\" for ",
format_definition(kind, callback),
" but this behaviour does not specify such callback",
" but ",
behaviour_string,
" does not specify such callback",
known_callbacks(callbacks)
]
end
+57 -44
View File
@@ -4,16 +4,29 @@
defmodule Module.ParallelChecker do
@moduledoc false
@elixir_checker_version :elixir_erl.checker_version()
import Kernel, except: [spawn: 3]
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type error() :: term()
@type mode() :: :erlang | :elixir | :protocol
@typedoc """
Options for `start_link/1`.
"""
@type start_link_opts :: [
{:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {atom(), term()}
]
@doc """
Initializes the parallel checker process.
"""
@spec start_link(start_link_opts()) :: {:ok, cache()}
def start_link(opts \\ []) do
:proc_lib.start_link(__MODULE__, :init, [opts])
end
@@ -51,14 +64,14 @@ defmodule Module.ParallelChecker do
@doc """
Spawns a process that runs the parallel checker.
"""
def spawn({pid, {checker, table}}, module, module_map, beam_location, log?) do
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
# Protocols may have been consolidated. So if we know their beam location,
# we discard their module map on purpose and start from file.
info =
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
List.to_string(beam_location)
else
cache_from_module_map(table, module_map)
cache_from_module_map(table, module_map, signatures)
end
inner_spawn(pid, checker, table, module, info, log?)
@@ -85,12 +98,14 @@ defmodule Module.ParallelChecker do
end
with {:ok, binary} <- File.read(location),
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
cache_from_module_map(table, module_map)
{:ok,
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
else
_ -> {:not_found, nil}
_ -> {:uncached, nil}
end
is_tuple(info) ->
@@ -106,14 +121,14 @@ defmodule Module.ParallelChecker do
# Set the compiler info so we can collect warnings
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
{warnings, errors} =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
else
[]
{[], []}
end
send(pid, {__MODULE__, module, warnings})
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :done, module})
end
@@ -166,11 +181,11 @@ defmodule Module.ParallelChecker do
end
@doc """
Receives pairs of module maps and BEAM binaries. In parallel it verifies
the modules and adds the ExCk chunk to the binaries. Returns the updated
list of warnings from the verification.
Receives pairs of module maps and BEAM binaries.
Returns the updated list of warnings from the verification.
"""
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
@spec verify(cache(), [{module(), Path.t()}]) :: {[warning()], [error()]}
def verify({checker, table}, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
@@ -180,40 +195,37 @@ defmodule Module.ParallelChecker do
end
count = :gen_server.call(checker, :start, :infinity)
diagnostics = collect_results(count, [])
{warnings, errors} = collect_results(count, [], [])
case :erlang.get(:elixir_code_diagnostics) do
:undefined -> :ok
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {errors ++ warnings ++ tail, log?})
end
diagnostics
{warnings, errors}
end
defp collect_results(0, diagnostics) do
diagnostics
defp collect_results(0, warnings, errors) do
{warnings, errors}
end
defp collect_results(count, diagnostics) do
defp collect_results(count, warnings, errors) do
receive do
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
diagnostic = %{diagnostic | file: file && Path.absname(file)}
collect_results(count, [diagnostic | diagnostics])
{__MODULE__, _module, new_diagnostics} ->
collect_results(count - 1, new_diagnostics ++ diagnostics)
if Map.get(diagnostic, :severity, :warning) == :error do
collect_results(count, warnings, [diagnostic | errors])
else
collect_results(count, [diagnostic | warnings], errors)
end
{__MODULE__, _module, new_warnings, new_errors} ->
collect_results(count - 1, new_warnings ++ warnings, new_errors ++ errors)
end
end
@doc """
Test cache.
"""
def test_cache do
{:ok, cache} = start_link()
cache
end
@doc """
Returns the export kind and deprecation reason for the given MFA from
the cache. If the module does not exist return `:badmodule`,
@@ -264,18 +276,19 @@ defmodule Module.ParallelChecker do
definitions
)
diagnostics =
{warnings, errors} =
module
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(file, log?)
|> group_diagnostics()
|> emit_diagnostics(file, log?)
|> Enum.split_with(&(&1.severity == :warning))
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
diagnostics
{warnings, errors}
end
defp module_map_to_module_tuple(module_map) do
@@ -326,16 +339,16 @@ defmodule Module.ParallelChecker do
## Warning helpers
defp group_warnings(warnings) do
defp group_diagnostics(triplets) do
{ungrouped, grouped} =
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(warning)
Enum.reduce(triplets, {[], %{}}, fn {module, term, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(term)
if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location])
grouped =
Map.update(grouped, warning, {locations, diagnostic}, fn
Map.update(grouped, term, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end)
@@ -353,7 +366,7 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped)
end
defp emit_warnings(warnings, file, log?) do
defp emit_diagnostics(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
log? and print_diagnostics(diagnostics)
@@ -412,7 +425,7 @@ defmodule Module.ParallelChecker do
mode =
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
{@elixir_checker_version, contents} <- :erlang.binary_to_term(chunk) do
# The chunk has more information, so that's our preference
cache_chunk(table, module, contents)
else
@@ -463,12 +476,12 @@ defmodule Module.ParallelChecker do
if Keyword.has_key?(attributes, :__protocol__), do: :protocol, else: :elixir
end
defp cache_from_module_map(table, map) do
defp cache_from_module_map(table, map, signatures) do
exports =
behaviour_exports(map) ++
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures)
cache_info(table, map.module, exports, Map.new(map.deprecated), signatures)
{elixir_mode(map.attributes), module_map_to_module_tuple(map)}
end
@@ -641,7 +654,7 @@ defmodule Module.ParallelChecker do
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
send(pid, {ref, :check})
timer = Process.send_after(self(), {__MODULE__, :timeout, module, pid}, state.threshold)
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
spawned = Map.put(state.spawned, module, timer)
run_checkers(%{state | modules: modules, spawned: spawned})
end
+309 -88
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
@@ -24,20 +24,17 @@ defmodule Module.Types do
#
# * :infer - Same as :dynamic but skips remote calls.
#
# * :traversal - Focused mostly on traversing AST, skips most type system
# operations. Used by macros and when skipping inference.
#
# The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback).
@modes [:static, :dynamic, :infer, :traversal]
@modes [:static, :dynamic, :infer]
# These functions are not inferred because they are added/managed by the compiler
@no_infer [behaviour_info: 1]
@doc false
def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
def infer(module, file, attrs, defs, used_private, env, {_, cache}) do
# We don't care about inferring signatures for protocols,
# those will be replaced anyway. There is also nothing to
# infer if there is no cache system, we only do traversals.
@@ -49,8 +46,8 @@ defmodule Module.Types do
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause ->
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
{_, kind, _, _} = def ->
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
false ->
false
@@ -75,23 +72,27 @@ defmodule Module.Types do
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
{types, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- defs,
kind in [:def, :defmacro],
reduce: {[], context()} do
{types, context} ->
# Optimized version of finder, since we already the definition
# In case there are loops, the other we traverse matters,
# so we sort the definitions for determinism
{types, private, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs),
reduce: {[], [], context()} do
{types, private, context} when kind in [:def, :defmacro] ->
# Optimized version of finder, since we already have the definition
finder = fn _ ->
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], context}
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
else
{types, context}
{types, private, context}
end
{types, private, context} ->
{types, [def | private], context}
end
# Now traverse all used privates to find any other private that have been used by them.
@@ -105,8 +106,8 @@ defmodule Module.Types do
{unreachable, _context} =
Enum.reduce(private, {[], context}, fn
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
warn_unused_def(info, used_sigs, env)
{fun_arity, kind, meta, _clauses}, {unreachable, context} ->
warn_unused_def(fun_arity, kind, meta, used_sigs, env)
# Find anything undefined within unused functions
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
@@ -125,7 +126,7 @@ defmodule Module.Types do
end
defp infer_mode(kind, infer_signatures?) do
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traverse
end
defp protocol?(attrs) do
@@ -146,12 +147,24 @@ defmodule Module.Types do
end
end
defp default_domain({_, arity} = fun_arity, impl) do
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
with {for, callbacks} <- impl,
true <- fun_arity in callbacks do
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
args = [
Descr.dynamic(Module.Types.Of.impl(for))
| List.duplicate(Descr.dynamic(), arity - 1)
]
{_fun_arity, kind, meta, clauses} = def
clauses =
for {meta, args, guards, body} <- clauses do
{[type_check: {:impl, for}] ++ meta, args, guards, body}
end
{mode, {fun_arity, kind, meta, clauses}, args}
else
_ -> List.duplicate(Descr.dynamic(), arity)
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
end
end
@@ -161,29 +174,30 @@ defmodule Module.Types do
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
end
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
:ok
end
defp warn_unused_def(fun_arity, kind, meta, used, env) do
default = Keyword.get(meta, :defaults, 0)
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
cond do
Keyword.get(meta, :context) != nil or Keyword.get(meta, :from_super) == true ->
:ok
:ok
end
default == 0 ->
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
default > 0 ->
{name, arity} = fun_arity
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff})
end
end
:ok
@@ -208,7 +222,7 @@ defmodule Module.Types do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
false -> false
end
end
@@ -219,7 +233,7 @@ defmodule Module.Types do
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already the definition
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
@@ -237,17 +251,27 @@ defmodule Module.Types do
context ->
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
clauses_indexes =
for type_index <- pending,
not skip_unused_clause?(info, type_index),
{clause_index, ^type_index} <- mapping,
do: clause_index
if pending != [] do
{used_indexes, unused_indexes} =
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
{used_indexes, unused_indexes} ->
if type_index in pending and not skip_unused_clause?(info, type_index) do
{used_indexes, [clause_index | unused_indexes]}
else
{[clause_index | used_indexes], unused_indexes}
end
end)
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
else
context
end
end
end
@@ -279,7 +303,7 @@ defmodule Module.Types do
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -292,36 +316,121 @@ defmodule Module.Types do
end
end
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
context =
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
Module.Types.Traverse.of_expr(body, stack, context)
end)
inferred = {:infer, nil, [{List.duplicate(Descr.term(), arity), Descr.dynamic()}]}
{inferred, [{0, 0}], context}
end
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
{fun, _arity} = fun_arity
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
base_info = {:def, kind, fun, expected}
{_, _, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, [], [], context}, fn
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
context = fresh_context(context)
case clauses do
[{meta, args, [], {:super, _, [_ | _]} = body}] ->
default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context)
_ ->
infer_local_handler(clauses, base_info, kind, fun, expected, stack, context)
end
end
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
guards = []
previous = Pattern.init_previous()
fresh_context = fresh_context(context)
info = {base_info, args, guards}
try do
{trees, _, _, _, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
# Compute the intersected arrows from the function call
{:super, meta, call_args} = body
{_kind, call_fun} = Keyword.fetch!(meta, :super)
term = Descr.term()
of_fun = &Expr.of_expr/5
{arrows, body_context} =
Apply.local_arrows(call_fun, call_args, term, body, stack, head_context, of_fun)
# For each arrow, compute the default arrow
{_, _, mapping, inferred} =
Enum.reduce(arrows, {0, 0, [], []}, fn
{clause_domain, return_type}, {index, total, mapping, inferred} ->
of_fun = &Expr.of_expr(&1, &2, body, stack, &3)
{_clause_args, clause_context} =
Helpers.zip_map_reduce(call_args, clause_domain, head_context, of_fun)
clause_types = Pattern.of_domain(trees, stack, clause_context)
{type_index, inferred} =
add_inferred(inferred, clause_types, return_type, total - 1, [])
total = if type_index == -1, do: total + 1, else: total
{index + 1, total, [{0, index} | mapping], inferred}
end)
domain =
case inferred do
[_] ->
nil
_ ->
inferred
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
end
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end
defp infer_local_handler(clauses, base_info, kind, fun, expected, stack, context) do
{_, _, _, domain, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
{meta, args, guards, body},
{index, total, previous, domain, mapping, inferred, acc_context} ->
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
try do
{trees, context} =
Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context)
{trees, _precise?, head_no_previous_args_types, previous, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
{return_type, context} =
Expr.of_expr(body, Descr.term(), body, stack, context)
Expr.of_expr(body, Descr.term(), body, stack, head_context)
args_types =
if stack.mode == :traversal do
expected
else
Pattern.of_domain(trees, expected, context)
end
args_types = Pattern.of_domain(trees, stack, context)
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
domain =
case domain do
[] ->
args_types
_ ->
head_args_types = Pattern.of_domain(trees, stack, head_context)
compute_domain(args_types, head_args_types, head_no_previous_args_types, domain)
end
if type_index == -1 do
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
mapping = [{index, total} | mapping]
{index + 1, total + 1, previous, domain, mapping, inferred, context}
else
{index + 1, total, [{index, type_index} | mapping], inferred, context}
mapping = [{index, type_index} | mapping]
{index + 1, total, previous, domain, mapping, inferred, context}
end
rescue
e ->
@@ -331,19 +440,69 @@ defmodule Module.Types do
domain =
case clauses_types do
[_] ->
nil
_ ->
clauses_types
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
[_] -> nil
_ -> domain
end
inferred = {:infer, domain, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(clauses_context, context)}
end
defp compute_domain(
[arg | args_types],
[head_arg | head_args_types],
[no_prev_arg | no_prev_args_types],
[d | domain]
) do
[
# This is an optimization that broadens the domain, but it is acceptable
# because the domain is used for reverse arrows and not type checking.
#
# The overall idea is that, if we have a function with three clauses,
# the domain is computed by unioning their inferred types. However, their
# inferred types often have the different of the previous clauses:
#
# union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
#
# Where `rN` represents the refinement in every function body.
#
# What this function does is, if the type of a given arg in a clause
# before and after the body is the same (meaning r3 is term), then
# we replace all of `(c3 - c2 - c1)` by just `c3`, which removes
# many of the differences in the node. However, keep in mind that,
# because `r2` may have refine `c2` in the previous clause, the domain
# may end-up being broader. Take this example:
#
# % %{..., foo: integer()} -> binary()
# def example(%{foo: var}), do: Integer.to_string(var)
#
# % %{...} and not %{..., foo: term()} -> :error
# def example(%{}), do: :error
#
# The actual domain is:
#
# %{..., foo: not_set()} or %{..., foo: integer()}
# #=> %{..., foo: if_set(integer())}
#
# But we will infer:
#
# %{...} or %{..., foo: integer()}
# #=> %{...}
#
# We lose precision but this is exactly what we want: to have simpler types.
# Furthermore, the signature used in type checking is not refined in any way,
# so type checking is still sound.
if arg == head_arg do
Descr.union(Descr.upper_bound(no_prev_arg), d)
else
Descr.union(arg, d)
end
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
]
end
defp compute_domain([], [], [], []), do: []
# We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
@@ -355,6 +514,58 @@ defmodule Module.Types do
defp add_inferred([], args, return, -1, acc),
do: {-1, [{args, return} | Enum.reverse(acc)]}
# Compact clauses that have the same return and differ in exactly one
# argument by unioning that argument. For example:
#
# (integer(), atom() -> boolean()) and (float(), atom() -> boolean())
#
# becomes:
#
# (number(), atom() -> boolean())
#
# Arity-zero clauses have no argument position to widen.
defp group_clauses_by_return({:infer, domain, [{[_ | _], _} | _] = clauses}) do
clauses =
Enum.reduce(clauses, [], fn {args, return}, acc ->
group_clause_by_return(acc, args, return)
end)
{:infer, domain, clauses}
end
defp group_clauses_by_return(info), do: info
defp group_clause_by_return([{existing_args, return} | tail], args, return) do
case union_args(existing_args, args, [], false) do
nil ->
[{existing_args, return} | group_clause_by_return(tail, args, return)]
new_args ->
[{new_args, return} | tail]
end
end
defp group_clause_by_return([head | tail], args, return) do
[head | group_clause_by_return(tail, args, return)]
end
defp group_clause_by_return([], args, return), do: [{args, return}]
defp union_args([arg | existing], [arg | args], acc, changed?) do
union_args(existing, args, [arg | acc], changed?)
end
# Allow exactly one differing argument. That one position is widened
# with union/2. A second difference means the clauses must stay separate.
defp union_args([existing_arg | existing], [arg | args], acc, false) do
union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true)
end
defp union_args([_ | _], [_ | _], _acc, true), do: nil
# In theory fully equal args are merged on add_inferred
defp union_args([], [], acc, _changed?), do: Enum.reverse(acc)
defp with_file_meta(stack, meta) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
@@ -417,9 +628,8 @@ defmodule Module.Types do
mode: mode,
# The function for handling local calls
local_handler: handler,
# Control if variable refinement is enabled.
# It is disabled only on dynamic dispatches.
refine_vars: true
# Reverse arrow handling (nil | :cache | :use)
reverse_arrow: nil
}
end
@@ -430,34 +640,45 @@ defmodule Module.Types do
warnings: [],
# All vars and their types
vars: %{},
# Variables and arguments from patterns
# Stores special metadata used by list heads and domain keys in patterns
subpatterns: %{},
# Variables that are specific to the current environment/conditional
conditional_vars: nil,
# Track metadata specific to patterns and guards
pattern_info: nil,
# If type checking has found an error/failure
failed: false,
# Local signatures used by local handler
local_sigs: %{},
# Track which clauses have been used across private local calls
local_used: %{}
local_used: %{},
# Cached reverse arrows
reverse_arrows: %{}
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
%{stack | mode: mode, function: function, reverse_arrow: nil}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
end
defp restore_context(later_context, %{vars: vars, failed: failed}) do
%{later_context | vars: vars, failed: failed}
defp restore_context(later_context, %{
vars: vars,
failed: failed,
reverse_arrows: reverse_arrows
}) do
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
end
## Diagnostics
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message: "this clause of #{kind} #{fun}/#{arity} is never used"
message:
"this clause of #{kind} #{fun}/#{arity} is never used (or it will always fail/warn when invoked)"
}
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+173 -62
View File
@@ -11,7 +11,7 @@ defmodule Module.Types.Helpers do
@doc """
Returns true if the mode cares about warnings.
"""
defguard is_warning(stack) when stack.mode not in [:traversal, :infer]
defguard is_warning(stack) when stack.mode != :infer
@doc """
Guard function to check if an AST node is a variable.
@@ -91,29 +91,6 @@ defmodule Module.Types.Helpers do
"var.fun()" (with parentheses) means "var" is an atom()
"""
:interpolation ->
"""
#{hint()} string interpolation uses the String.Chars protocol to \
convert a data structure into a string. Either convert the data type into a \
string upfront or implement the protocol accordingly
"""
:generator ->
"""
#{hint()} for-comprehensions use the Enumerable protocol to traverse \
data structures. Either convert the data type into a list (or another Enumerable) \
or implement the protocol accordingly
"""
:into ->
"""
#{hint()} the :into option in for-comprehensions use the Collectable protocol to \
build its result. Either pass a valid data type or implement the protocol accordingly
"""
:anonymous_rescue ->
"""
@@ -132,6 +109,20 @@ defmodule Module.Types.Helpers do
the union (which may be none)
"""
{:impl, for} ->
# Get the type without dynamic for better pretty printing
type =
for
|> Module.Types.Of.impl()
|> Module.Types.Descr.dynamic()
|> Map.fetch!(:dynamic)
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
"""
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
"""
:empty_domain ->
"""
@@ -141,7 +132,8 @@ defmodule Module.Types.Helpers do
end)
end
defp hint, do: :elixir_errors.prefix(:hint)
@doc "The hint prefix"
def hint, do: :elixir_errors.prefix(:hint)
@doc """
Collect traces from variables in expression.
@@ -151,12 +143,13 @@ defmodule Module.Types.Helpers do
"""
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
Macro.prewalk(expr, %{}, fn
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
Macro.prewalk(expr, %{}, fn node, versions ->
with {var_name, meta, var_context} when is_atom(var_name) and is_atom(var_context) <- node,
false <- String.starts_with?(Atom.to_string(var_name), "_") do
version = meta[:version]
case vars do
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
@@ -168,9 +161,9 @@ defmodule Module.Types.Helpers do
_ ->
{:ok, versions}
end
node, versions ->
{node, versions}
else
_ -> {node, versions}
end
end)
versions
@@ -269,7 +262,12 @@ defmodule Module.Types.Helpers do
Converts the given expression to a string,
translating inlined Erlang calls back to Elixir.
We also undo some macro expressions done by the Kernel module.
We also undo some macro expressions done by the Kernel module
and collapse complex expressions.
## Options
* `:collapse_structs` - when false, show structs full representation
"""
def expr_to_string(expr, opts \\ []) do
string = prewalk_expr_to_string(expr, opts)
@@ -331,6 +329,13 @@ defmodule Module.Types.Helpers do
end
end
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
call
else
{:in, meta, [expr, args]}
end
{{:., _, [Elixir.String.Chars, :to_string]}, meta, [arg]} ->
{:to_string, meta, [arg]}
@@ -340,6 +345,13 @@ defmodule Module.Types.Helpers do
{{:., _, [mod, fun]}, meta, args} ->
erl_to_ex(mod, fun, args, meta)
{:fn, meta, [{:->, _, [_args, return]}]} = expr ->
if meta[:capture] do
{:&, meta, [return]}
else
expr
end
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]} ->
{mod, fun} =
case :elixir_rewrite.erl_to_ex(mod, fun, arity) do
@@ -349,40 +361,139 @@ defmodule Module.Types.Helpers do
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]}
{:case, meta, [expr, [do: clauses]]} = case ->
if meta[:type_check] == :expr do
case clauses do
[
{:->, _,
[
{:case, meta, [expr, [do: clauses]]} ->
case meta[:type_check] do
{:case, op} ->
case clauses do
[
{:->, _,
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
else_block
]},
{:->, _, [[{:_, _, Kernel}], do_block]}
] ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
true
]},
{:->, _, [[{:_, _, Kernel}], false]}
]
when op == :! ->
{:!, meta, [expr]}
[
{:->, _, [[false], else_block]},
{:->, _, [[true], do_block]}
] ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _,
[
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
right_side
]},
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
]
when op == :|| ->
{:||, meta, [expr, right_side]}
_ ->
case
end
[
{:->, _,
[
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
{var, _, Kernel}
]},
{:->, _, [[{:_, _, Kernel}], right_side]}
]
when op == :&& ->
{:&&, meta, [expr, right_side]}
[
{:->, _,
[
[
{:when, _,
[
{var, _, Kernel},
{{:., _, [:erlang, :orelse]}, _,
[
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]},
{{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]}
]}
]}
],
else_block
]},
{:->, _, [[{:_, _, Kernel}], do_block]}
]
when op == :if ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _, [[false], else_block]},
{:->, _, [[true], do_block]}
]
when op == :if ->
{:if, meta, [expr, [do: do_block, else: else_block]]}
[
{:->, _, [[false], false]},
{:->, _, [[true], right]}
| _
]
when op == :and ->
{:and, meta, [expr, right]}
[
{:->, _, [[false], right]},
{:->, _, [[true], true]}
| _
]
when op == :or ->
{:or, meta, [expr, right]}
_ ->
{:case, meta, [expr, [do: {:..., [], []}]]}
end
_ ->
{:case, meta, [expr, [do: {:..., [], []}]]}
end
{:try, meta, [[do: _] ++ _]} ->
{:try, meta, [[do: {:..., [], []}]]}
{:cond, meta, [[do: _]]} ->
{:cond, meta, [[do: {:..., [], []}]]}
{:receive, meta, [[do: _] ++ _]} ->
{:receive, meta, [[do: {:..., [], []}]]}
{var, meta, context} = expr when is_atom(var) and is_atom(context) ->
if is_integer(meta[:capture]) do
{:&, meta, [meta[:capture]]}
else
case
expr
end
other ->
+447 -204
View File
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
@@ -11,10 +12,10 @@ defmodule Module.Types.Of do
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer integer()
@float float()
@binary binary()
@bitstring bitstring()
## Variables
@@ -29,19 +30,53 @@ defmodule Module.Types.Of do
@doc """
Marks a variable with error.
This purposedly deletes all traces of the variable,
as it is often invoked when the cause for error is elsewhere.
"""
def error_var(var, context) do
def error_var({_, meta, _}, context) do
error_var(Keyword.fetch!(meta, :version), context)
end
def error_var(version, context) do
update_in(context.vars[version], fn
%{errored: true} = data -> data
data -> Map.put(%{data | type: error_type(), off_traces: []}, :errored, true)
end)
end
@doc """
Declares a variable.
"""
def declare_var(var, type \\ term(), context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
data = %{
type: error_type(),
name: var_name,
context: var_context,
off_traces: []
}
case context.vars do
%{^version => _} ->
context
put_in(context.vars[version], data)
vars ->
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: [],
paths: [],
deps: %{}
}
%{context | vars: Map.put(vars, version, data)}
end
end
@doc """
Tracks metadata about variables dependencies and paths.
"""
def track_var(version, new_deps, new_paths, context) do
update_in(context.vars[version], fn %{paths: paths, deps: deps} = data ->
%{data | paths: new_paths ++ paths, deps: Enum.reduce(new_deps, deps, &Map.put(&2, &1, []))}
end)
end
@doc """
@@ -51,26 +86,70 @@ defmodule Module.Types.Of do
or if we are doing a guard analysis or occurrence typing.
Returns `true` if there was a refinement, `false` otherwise.
"""
@skip_refinement_for [term(), dynamic()]
def refine_body_var(var_or_version, type, expr, stack, context)
def refine_body_var({_, meta, _}, type, expr, stack, context) do
version = Keyword.fetch!(meta, :version)
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
end
def refine_body_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
if gradual?(old_type) and type not in [term(), dynamic()] do
case compatible_intersection(old_type, type) do
{:ok, new_type} when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
context =
case context.conditional_vars do
%{} = conditional_vars ->
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
nil ->
context
end
else
{old_type, context}
case context do
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
{old_type, context}
%{pattern_info: %{guard_context: guard_context}} ->
new_type = intersection(old_type, type)
case empty?(new_type) do
true when guard_context == :orelse ->
data = %{
data
| type: none(),
off_traces: new_trace(expr, none(), stack, off_traces)
}
{none(), %{context | vars: %{vars | version => data}}}
false when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
_ ->
case gradual?(old_type) and compatible_intersection(old_type, type) do
{:ok, new_type} when new_type != old_type ->
data = %{
data
| type: new_type,
off_traces: new_trace(expr, new_type, stack, off_traces)
}
{new_type, %{context | vars: %{vars | version => data}}}
_ ->
{old_type, context}
end
end
end
@@ -81,11 +160,16 @@ defmodule Module.Types.Of do
because we want to refine types. Otherwise we should
use compatibility.
"""
def refine_head_var(var, type, expr, stack, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
def refine_head_var({_, meta, _}, type, expr, stack, context) do
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
end
def refine_head_var(version, type, expr, stack, context)
when is_integer(version) or is_reference(version) do
case context.vars do
%{^version => %{errored: true}} ->
{:ok, error_type(), context}
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
new_type = intersection(type, old_type)
@@ -95,26 +179,14 @@ defmodule Module.Types.Of do
off_traces: new_trace(expr, type, stack, off_traces)
}
context = %{context | vars: %{vars | version => data}}
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do
{:error, error_type(),
error({:refine_head_var, old_type, type, var, context}, meta, stack, context)}
data = Map.put(%{data | type: error_type()}, :errored, true)
context = %{context | vars: %{vars | version => data}}
{:error, old_type, context}
else
context = %{context | vars: %{vars | version => data}}
{:ok, new_type, context}
end
%{} = vars ->
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: new_trace(expr, type, stack, [])
}
context = %{context | vars: Map.put(vars, version, data)}
{:ok, type, context}
end
end
@@ -124,11 +196,71 @@ defmodule Module.Types.Of do
defp new_trace(expr, type, stack, traces),
do: [{expr, stack.file, type} | traces]
@doc """
Preserves `context` in first argument while
resetting it to the vars in the second argument.
"""
def reset_vars(context, %{
subpatterns: subpatterns,
vars: vars,
conditional_vars: conditional_vars
}),
do: %{context | subpatterns: subpatterns, vars: vars, conditional_vars: conditional_vars}
@doc """
Returns true if all entries have the same conditional vars.
"""
def all_same_conditional_vars?([{_, cond} | tail]) do
Enum.all?(tail, fn {_, tail_cond} -> cond == tail_cond end)
end
@doc """
Executes the args with acc using conditional variables.
"""
def with_conditional_vars(args, acc, expr, stack, context, fun) do
%{vars: vars, conditional_vars: conditional_vars} = context
{vars_conds, {acc, context}} =
Enum.map_reduce(args, {acc, context}, fn arg, {acc, context} ->
{acc, context} = fun.(arg, acc, %{context | vars: vars, conditional_vars: %{}})
%{vars: vars, conditional_vars: cond_vars} = context
{{vars, cond_vars}, {acc, context}}
end)
context = %{context | vars: vars, conditional_vars: conditional_vars}
{acc, reduce_conditional_vars(vars_conds, expr, stack, context)}
end
@doc """
Reduces conditional variables collected separately.
"""
def reduce_conditional_vars([{vars, cond} | vars_conds], expr, stack, context) do
%{vars: pre_vars} = context
Enum.reduce(Map.keys(cond), context, fn version, context ->
if is_map_key(pre_vars, version) and
Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
%{^version => %{type: type}} = vars
type =
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
%{^version => %{type: type}} = vars
union(acc, type)
end)
{_, context} = refine_body_var(version, type, expr, stack, context)
context
else
context
end
end)
end
## Implementations
impls = [
{Atom, atom()},
{BitString, binary()},
{BitString, bitstring()},
{Float, float()},
{Function, fun()},
{Integer, integer()},
@@ -141,14 +273,25 @@ defmodule Module.Types.Of do
{Any, term()}
]
@doc """
Currently, for protocol implementations, we only store
the open struct definition. This is because we don't want
to reconsolidate whenever the struct changes, but at the
moment we can't store references either. Ideally struct
types on protocol dispatches would be lazily resolved.
"""
def impl(for, mode \\ :closed)
for {for, type} <- impls do
def impl(unquote(for)), do: unquote(Macro.escape(type))
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
end
def impl(struct) do
# Elixir did not strictly require the implementation to be available, so we need a fallback.
def impl(struct, mode) do
# Elixir did not strictly require the implementation to be available,
# so we need to deal with such cases accordingly.
# TODO: Assume implementation is available on Elixir v2.0.
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
# A warning is emitted since v1.19+.
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
struct_type(struct, info)
else
open_map(__struct__: atom([struct]))
@@ -161,7 +304,7 @@ defmodule Module.Types.Of do
Handles fetching a map key.
"""
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch(type, field) do
case map_fetch_key(type, field) do
{_optional?, value_type} ->
{value_type, context}
@@ -176,107 +319,134 @@ defmodule Module.Types.Of do
def closed_map(pairs, expected, stack, context, of_fun) do
{pairs_types, context} = pairs(pairs, expected, stack, context, of_fun)
map =
permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
# TODO: Use the fallback type to actually indicate if open or closed.
if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
{dynamic?, domain, single, multiple} =
Enum.reduce(pairs_types, {false, [], [], []}, fn
{pos_neg_domain, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
dynamic? = dynamic? or dynamic_pair?
case pos_neg_domain do
# If atom is included in domain keys, it unions all previous
# single and multiple, except the ones negated:
#
# %{foo: :bar, term() => :baz}
# #=> %{foo: :bar or :baz, term() => :baz}
#
# %{foo: :bar, not :foo => :baz}
# #=> %{foo: :bar, term() => :baz}
#
# In case the negated term does not appear, we set it to none():
#
# %{foo: :bar, term() => :baz}
# #=> %{term() => :baz, foo: :bar or :baz}
#
# %{not :foo => :baz}
# #=> %{term() => :baz, foo: none()}
#
# In case we are dealing with multiple keys, we always merge the
# domain. A more precise approach would be to postpone doing so
# until the cartesian map is distributed but those should be very
# uncommon.
{[], negs, domain_keys} ->
if :atom in domain_keys do
{single, multiple} = union_negated(negs, value_type, single, multiple)
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
else
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
end
{pos, [], domain_keys} ->
domain =
case domain_keys do
[] -> domain
_ -> [{domain_keys, value_type} | domain]
end
case pos do
# Because a multiple key may override single keys, we can only
# collect single keys while there are no multiples.
[key] when multiple == [] ->
{dynamic?, domain, [{key, value_type} | single], multiple}
_ ->
{dynamic?, domain, single, [{pos, value_type} | multiple]}
end
end
end)
{map, context}
non_multiple = Enum.reverse(single, domain)
map =
case Enum.reverse(multiple) do
[] ->
closed_map(non_multiple)
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
closed_map(non_multiple ++ [{key, type} | t])
end
|> Enum.reduce(&union/2)
end
{if(dynamic?, do: dynamic(map), else: map), context}
end
@doc """
Computes the types of key-value pairs.
"""
def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do
Enum.map_reduce(pairs, context, fn {key, value}, context ->
{_key_type, context} = of_fun.(key, term(), stack, context)
{value_type, context} = of_fun.(value, term(), stack, context)
{{true, :none, value_type}, context}
end)
defp union_negated([], new_type, single, multiple) do
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
{single, multiple}
end
def pairs(pairs, expected, stack, context, of_fun) do
defp union_negated(negated, new_type, single, multiple) do
{single, matched} =
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
if key in negated do
{{key, old_type}, [key | matched]}
else
{{key, union(old_type, new_type)}, matched}
end
end)
multiple =
Enum.map(multiple, fn {keys, old_type} ->
{keys, union(old_type, new_type)}
end)
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
end
defp pairs(pairs, expected, stack, context, of_fun) do
Enum.map_reduce(pairs, context, fn {key, value}, context ->
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
{pos_neg_domain, dynamic_key?, context} = map_key_type(key, stack, context, of_fun)
expected_value_type =
with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do
with {[key], [], []} <- pos_neg_domain,
{_, expected_value_type} <- map_fetch_key(expected, key) do
expected_value_type
else
_ -> term()
end
{value_type, context} = of_fun.(value, expected_value_type, stack, context)
{{dynamic_key? or gradual?(value_type), keys, value_type}, context}
{{pos_neg_domain, dynamic_key? or gradual?(value_type), value_type}, context}
end)
end
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{false, [key], context}
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{{[key], [], []}, false, context}
end
defp finite_key_type(key, stack, context, of_fun) do
defp map_key_type(key, stack, context, of_fun) do
{key_type, context} = of_fun.(key, term(), stack, context)
domain_keys = to_domain_keys(key_type)
case atom_fetch(key_type) do
{:finite, list} -> {gradual?(key_type), list, context}
_ -> {gradual?(key_type), :none, context}
end
end
@doc """
Builds permutation of maps according to the given pairs types.
"""
def permutate_map(_pairs_types, %{mode: :traversal}, _of_map) do
dynamic()
end
def permutate_map(pairs_types, _stack, of_map) do
{dynamic?, fallback, single, multiple, assert} =
Enum.reduce(pairs_types, {false, none(), [], [], []}, fn
{dynamic_pair?, keys, value_type}, {dynamic?, fallback, single, multiple, assert} ->
dynamic? = dynamic? or dynamic_pair?
case keys do
:none ->
fallback = union(fallback, value_type)
{fallback, assert} =
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
{union(fallback, type), [key | assert]}
end)
{fallback, assert} =
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
{union(fallback, type), keys ++ assert}
end)
{dynamic?, fallback, [], [], assert}
# Because a multiple key may override single keys, we can only
# collect single keys while there are no multiples.
[key] when multiple == [] ->
{dynamic?, fallback, [{key, value_type} | single], multiple, assert}
keys ->
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert}
end
end)
map =
case Enum.reverse(multiple) do
[] ->
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
end
|> Enum.reduce(&union/2)
pos_neg_domain =
case atom_fetch(key_type) do
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)}
{:infinite, list} -> {[], list, domain_keys}
:error -> {[], [], domain_keys}
end
if dynamic?, do: dynamic(map), else: map
{pos_neg_domain, gradual?(key_type), context}
end
defp cartesian_map(lists) do
@@ -291,20 +461,24 @@ defmodule Module.Types.Of do
@doc """
Handles instantiation of a new struct.
This is expanded and validated by the compiler, so don't need to check the fields.
"""
# TODO: Type check the fields match the struct
def struct_instance(struct, args, expected, meta, %{mode: mode} = stack, context, of_fun)
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
when is_atom(struct) do
{_info, context} = struct_info(struct, meta, stack, context)
{info, context} = struct_info(struct, :expr, meta, stack, context)
if is_nil(info) do
raise "expected #{inspect(struct)} to return struct metadata, but got none"
end
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
with true <- mode != :traversal,
{_, expected_value_type} <- map_fetch(expected, key) do
expected_value_type
else
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
@@ -318,23 +492,28 @@ defmodule Module.Types.Of do
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, meta, stack, context) do
def struct_info(struct, kind, meta, stack, context) do
case stack.no_warn_undefined do
%Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
{:ok, info} -> {info, context}
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
{:error, _desc} -> {nil, context}
end
_ ->
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
Code.ensure_loaded?(struct) and function_exported?(struct, :__info__, 1) and
struct.__info__(:struct)
{info, context}
if info do
{_, context} =
Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
{info, context}
else
error = {:unknown_struct, kind, struct}
{nil, error(error, meta, stack, context)}
end
end
end
@@ -351,45 +530,74 @@ defmodule Module.Types.Of do
closed_map(pairs)
end
## Binary
@doc """
Returns shared error for unknown struct field.
"""
def unknown_struct_field(struct, field, kind, meta, stack, context) do
error = {:unknown_struct_field, kind, struct, field}
error(error, meta, stack, context)
end
## Bitstrings
@doc """
Handles binaries.
Handles bitstrings.
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _kind, _stack, context) do
context
def bitstring([], _kind, _stack, context) do
{binary(), context}
end
def binary([head], kind, stack, context) do
binary_segment(head, kind, [head], stack, context)
def bitstring([head], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head], stack, context)
{alignment_to_type(alignment), context}
end
def binary([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
def bitstring([head | tail], kind, stack, context) do
{alignment, context} = bitstring_segment(head, kind, [head, @suffix], stack, context)
bitstring_tail(tail, alignment, kind, stack, context)
end
defp binary_many([last], kind, stack, context) do
binary_segment(last, kind, [@prefix, last], stack, context)
defp bitstring_tail([last], alignment, kind, stack, context) do
{seg_alignment, context} = bitstring_segment(last, kind, [@prefix, last], stack, context)
{alignment_to_type(alignment(seg_alignment, alignment)), context}
end
defp binary_many([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
defp bitstring_tail([head | tail], alignment, kind, stack, context) do
{seg_alignment, context} =
bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context)
bitstring_tail(tail, alignment(seg_alignment, alignment), kind, stack, context)
end
defp alignment(left, right) when is_integer(left) and is_integer(right), do: left + right
defp alignment(_left, _right), do: :unknown
defp alignment_to_type(:unknown), do: bitstring()
defp alignment_to_type(integer) when rem(integer, 8) == 0, do: binary()
defp alignment_to_type(_integer), do: bitstring_no_binary()
# If the segment is a literal, the compiler has already checked its validity,
# so we just skip it.
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
# so we just check the size.
defp bitstring_segment({:"::", _meta, [left, right]}, kind, _args, stack, context)
when is_binary(left) or is_number(left) do
context
{_type, alignment_type} = specifier_type(kind, right)
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
# We don't need to check for bitstrings because the left side
# is either a binary (aligned), float (aligned), or integer
# (which we check below).
if alignment_type == :integer and alignment_value != :default do
{alignment_value, context}
else
{0, context}
end
end
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
{type, alignment_type} = specifier_type(kind, right)
expr = {:<<>>, meta, args}
{actual, context} =
@@ -406,10 +614,26 @@ defmodule Module.Types.Of do
end
if compatible?(actual, type) do
specifier_size(kind, right, stack, context)
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
case alignment_type do
:aligned ->
{0, context}
:integer when alignment_value == :default ->
{0, context}
# There is no size, so the alignment depends on the type.
# If the type is exclusively a binary, then it is aligned.
:bitstring when alignment_value == :default ->
if bitstring_no_binary_type?(actual), do: {:unknown, context}, else: {0, context}
_ ->
{alignment_value, context}
end
else
error = {:badbinary, kind, meta, expr, type, actual, context}
error(error, meta, stack, context)
{:unknown, error(error, meta, stack, context)}
end
end
@@ -425,39 +649,48 @@ defmodule Module.Types.Of do
end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
defp specifier_type(:match, {:utf8, _, _}), do: @integer
defp specifier_type(:match, {:utf16, _, _}), do: @integer
defp specifier_type(:match, {:utf32, _, _}), do: @integer
defp specifier_type(:match, {:float, _, _}), do: @float
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:integer, _, _}), do: @integer
defp specifier_type(_kind, {:bits, _, _}), do: @binary
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer
defp specifier_type(:match, {:utf8, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:utf16, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:utf32, _, _}), do: {@integer, :aligned}
defp specifier_type(:match, {:float, _, _}), do: {@float, :aligned}
defp specifier_type(_kind, {:float, _, _}), do: {@integer_or_float, :aligned}
defp specifier_type(_kind, {:utf8, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:utf16, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:utf32, _, _}), do: {@integer, :aligned}
defp specifier_type(_kind, {:integer, _, _}), do: {@integer, :integer}
defp specifier_type(_kind, {:bits, _, _}), do: {@bitstring, :bitstring}
defp specifier_type(_kind, {:bitstring, _, _}), do: {@bitstring, :bitstring}
defp specifier_type(_kind, {:bytes, _, _}), do: {@binary, :aligned}
defp specifier_type(_kind, {:binary, _, _}), do: {@binary, :aligned}
defp specifier_type(_kind, _specifier), do: {@integer, :integer}
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context))
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
defp specifier_size(_, {:size, _, [arg]}, _stack, {unit, context})
when is_integer(arg) do
size = if unit == :default, do: arg, else: arg * unit
{size, context}
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, {_, context}) do
{actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context)
compatible_size(actual, expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)}
end
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
defp specifier_size(_match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
compatible_size(actual, expr, stack, context)
{:unknown, compatible_size(actual, expr, stack, context)}
end
defp specifier_size(_kind, _specifier, _stack, context) do
context
# We currently assume the unit always comes before size
defp specifier_size(_, {:unit, _, [unit]}, _stack, {:default, context}) do
{unit, context}
end
defp specifier_size(_kind, _specifier, _stack, align_context) do
align_context
end
defp compatible_size(actual, expr, stack, context) do
@@ -478,9 +711,12 @@ defmodule Module.Types.Of do
"""
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do
{_, mods} ->
{:finite, mods} ->
{mods, context}
{:infinite, _} ->
{[], context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], error(warning, meta, stack, context)}
@@ -493,23 +729,6 @@ defmodule Module.Types.Of do
error(__MODULE__, warning, meta, stack, context)
end
def format_diagnostic({:refine_head_var, old_type, new_type, var, context}) do
traces = collect_traces(var, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types assigned to #{format_var(var)}:
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
""",
format_traces(traces)
])
}
end
def format_diagnostic({:badbinary, kind, meta, expr, expected_type, actual_type, context}) do
type = if kind == :match, do: "matching", else: "construction"
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
@@ -629,6 +848,30 @@ defmodule Module.Types.Of do
}
end
def format_diagnostic({:unknown_struct, kind, module}) do
message =
if Code.ensure_loaded?(module) do
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
else
"struct #{inspect(module)} is undefined " <>
"(module #{inspect(module)} is not available or is yet to be defined)"
end
%{
message: message,
group: true,
severity: if(kind == :pattern, do: :error, else: :warning)
}
end
def format_diagnostic({:unknown_struct_field, kind, module, field}) do
%{
message: "unknown key #{inspect(field)} for struct #{inspect(module)}",
group: true,
severity: if(kind == :pattern, do: :error, else: :warning)
}
end
defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
File diff suppressed because it is too large Load Diff
+210
View File
@@ -0,0 +1,210 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Module.Types.Traverse do
@moduledoc false
# Traverses expressions to find local calls when inference is disabled.
# Literals
def of_expr(literal, _stack, context)
when is_atom(literal) or is_integer(literal) or is_float(literal) or is_binary(literal) or
is_pid(literal) or literal == [] do
context
end
# [expr, ...]
def of_expr(list, stack, context) when is_list(list) do
Enum.reduce(list, context, &of_expr(&1, stack, &2))
end
# {left, right}
def of_expr({left, right}, stack, context) do
context = of_expr(left, stack, context)
of_expr(right, stack, context)
end
# <<...>>
def of_expr({:<<>>, _meta, args}, stack, context) do
Enum.reduce(args, context, fn
{:"::", _meta, [left, _right]}, context ->
of_expr(left, stack, context)
expr, context ->
of_expr(expr, stack, context)
end)
end
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]}, stack, context) do
context = of_expr(map, stack, context)
context = of_expr(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
def of_expr({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) do
context = of_expr(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
# Map update, tail operator
def of_expr({:|, _meta, [left, right]}, stack, context) do
context = of_expr(left, stack, context)
of_expr(right, stack, context)
end
# Tuples, maps
def of_expr({container, _meta, exprs}, stack, context) when container in [:{}, :%{}] do
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
end
# left = right, left <- right
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:=, :<-] do
context = of_pattern(left, stack, context)
of_expr(right, stack, context)
end
# Blocks
def of_expr({:__block__, _, args}, stack, context) do
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# cond do ... end
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
Enum.reduce(clauses, context, fn {:->, _meta, [[head], body]}, context ->
context = of_expr(head, stack, context)
of_expr(body, stack, context)
end)
end
# All non-handled -> are patterns
def of_expr({:->, _, [head, body]}, stack, context) do
context = of_pattern(head, stack, context)
of_expr(body, stack, context)
end
# case expr do ... end
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
context = of_expr(case_expr, stack, context)
of_expr(clauses, stack, context)
end
# fn pat -> expr end
def of_expr({:fn, _meta, clauses}, stack, context) do
of_expr(clauses, stack, context)
end
# try do ... end
def of_expr({:try, _meta, [blocks]}, stack, context) do
Enum.reduce(blocks, context, fn {_, clauses_or_body}, context ->
of_expr(clauses_or_body, stack, context)
end)
end
# receive do ... end
def of_expr({:receive, _meta, [blocks]}, stack, context) do
Enum.reduce(blocks, context, fn
{:do, clauses_or_empty_body}, context ->
of_expr(clauses_or_empty_body, stack, context)
{:after, [{:->, _meta, [[timeout], body]}]}, context ->
context = of_expr(timeout, stack, context)
of_expr(body, stack, context)
end)
end
# for, with
def of_expr({op, _meta, [_ | _] = args}, stack, context) when op in [:for, :with] do
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# fun.(args)
def of_expr({{:., _meta, [fun]}, _call_meta, args}, stack, context) do
context = of_expr(fun, stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# remote.fun(args)
def of_expr({{:., _, [remote, name]}, _meta, args}, stack, context)
when is_atom(name) do
context = of_expr(remote, stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# &Mod.fun/arity
def of_expr({:&, _, [{:/, _, [{{:., _, [_remote, name]}, _, []}, arity]}]}, _stack, context)
when is_atom(name) and is_integer(arity) do
context
end
# &fun/arity
def of_expr({:&, meta, [{:/, _, [{name, _, _ctx}, arity]}]}, stack, context)
when is_atom(name) and is_integer(arity) do
local_fun(meta, name, arity, stack, context)
end
# super(args)
def of_expr({:super, meta, args}, stack, context) when is_list(args) do
{_kind, name} = Keyword.fetch!(meta, :super)
context = local_fun(meta, name, length(args), stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# local_fun(args)
def of_expr({name, meta, args}, stack, context)
when is_atom(name) and is_list(args) do
context = local_fun(meta, name, length(args), stack, context)
Enum.reduce(args, context, &of_expr(&1, stack, &2))
end
# var
def of_expr({name, _meta, ctx}, _stack, context)
when is_atom(name) and is_atom(ctx) do
context
end
defp of_pattern({:%, meta, [module, {:%{}, _, pairs}]}, stack, context) when is_atom(module) do
context = of_pattern(pairs, stack, context)
of_struct(module, pairs, :expr, meta, stack, context)
end
defp of_pattern({left, _meta, right}, stack, context) do
context = of_pattern(left, stack, context)
of_pattern(right, stack, context)
end
defp of_pattern({left, right}, stack, context) do
context = of_pattern(left, stack, context)
of_pattern(right, stack, context)
end
defp of_pattern([_ | _] = list, stack, context) do
Enum.reduce(list, context, &of_pattern(&1, stack, &2))
end
defp of_pattern(_, _stack, context) do
context
end
defp of_struct(module, pairs, kind, meta, stack, context) do
{info, context} = Module.Types.Of.struct_info(module, kind, meta, stack, context)
if info do
Enum.reduce(pairs, context, fn {key, _value}, context ->
if Enum.any?(info, &(&1.field == key)) do
context
else
Module.Types.Of.unknown_struct_field(module, key, kind, meta, stack, context)
end
end)
else
context
end
end
defp local_fun(meta, fun, arity, stack, context) do
case stack.local_handler.(meta, {fun, arity}, stack, context) do
false -> context
{_kind, _info, context} -> context
end
end
end
+87 -10
View File
@@ -129,6 +129,7 @@ defmodule OptionParser do
* `:integer` - parses the value as an integer
* `:float` - parses the value as a float
* `:string` - parses the value as a string
* `:regex` - parses the value as a regular expression with Unicode support
If a switch can't be parsed according to the given type, it is
returned in the invalid options list.
@@ -282,11 +283,11 @@ defmodule OptionParser do
iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
** (OptionParser.ParseError) 1 error found!
--limit : Expected type integer, got "xyz"
--limit : Expected type integer, got "xyz"...
iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
** (OptionParser.ParseError) 1 error found!
--unknown : Unknown option
--unknown : Unknown option...
iex> OptionParser.parse!(
...> ["-l", "xyz", "-f", "bar"],
@@ -295,7 +296,7 @@ defmodule OptionParser do
...> )
** (OptionParser.ParseError) 2 errors found!
-l : Expected type integer, got "xyz"
-f : Expected type integer, got "bar"
-f : Expected type integer, got "bar"...
"""
@spec parse!(argv, options) :: {parsed, argv}
@@ -354,7 +355,7 @@ defmodule OptionParser do
...> strict: [number: :integer]
...> )
** (OptionParser.ParseError) 1 error found!
--number : Expected type integer, got "lib"
--number : Expected type integer, got "lib"...
iex> OptionParser.parse_head!(
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
@@ -362,7 +363,7 @@ defmodule OptionParser do
...> )
** (OptionParser.ParseError) 2 errors found!
--verbose : Missing argument of type integer
--source : Expected type integer, got "lib"
--source : Expected type integer, got "lib"...
"""
@spec parse_head!(argv, options) :: {parsed, argv}
@@ -664,7 +665,7 @@ defmodule OptionParser do
end
defp validate_switch({_name, type_or_type_and_modifiers}) do
valid = [:boolean, :count, :integer, :float, :string, :keep]
valid = [:boolean, :count, :integer, :float, :string, :regex, :keep]
invalid = List.wrap(type_or_type_and_modifiers) -- valid
if invalid != [] do
@@ -704,6 +705,12 @@ defmodule OptionParser do
_ -> {true, value}
end
:regex in kinds ->
case Regex.compile(value, "u") do
{:ok, regex} -> {false, regex}
{:error, _} -> {true, value}
end
true ->
{false, value}
end
@@ -863,15 +870,20 @@ defmodule OptionParser do
error_count = length(errors)
error = if error_count == 1, do: "error", else: "errors"
"#{error_count} #{error} found!\n" <>
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
slogan =
"#{error_count} #{error} found!\n" <>
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
case format_available_options(opts, types) do
"" -> slogan
available_options -> slogan <> "\n\n#{available_options}"
end
end
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
if String.contains?(option, "_") do
msg = "#{option} : Unknown option"
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
else
"#{option} : Missing argument of type #{type}"
@@ -891,7 +903,13 @@ defmodule OptionParser do
defp format_error({option, value}, opts, types) do
type = get_type(option, opts, types)
"#{option} : Expected type #{type}, got #{inspect(value)}"
with :regex <- type,
{:error, {reason, position}} <- Regex.compile(value, "u") do
"#{option} : Invalid regular expression #{inspect(value)}: #{reason} at position #{position}"
else
_ -> "#{option} : Expected type #{type}, got #{inspect(value)}"
end
end
defp get_type(option, opts, types) do
@@ -917,4 +935,63 @@ defmodule OptionParser do
option = String.replace(source, "_", "-")
if score < current, do: best, else: {option, score}
end
defp format_available_options(opts, switches) do
reverse_aliases =
opts
|> Keyword.get(:aliases, [])
|> Enum.reduce(%{}, fn {alias, target}, acc ->
Map.update(acc, target, [alias], &[alias | &1])
end)
formatted_options =
switches
|> Enum.sort()
|> Enum.map(fn {name, types} ->
types = List.wrap(types)
case types |> List.delete(:keep) |> List.first(:string) do
:boolean ->
case to_switch(name) do
"--no-" <> _ = switch ->
add_aliases(switch, name, reverse_aliases)
switch ->
base = "#{switch}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
end
type ->
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
base = add_aliases(base, name, reverse_aliases)
if :keep in types do
base <> " (may be given more than once)"
else
base
end
end
end)
if formatted_options == [] do
""
else
"Supported options:\n" <> Enum.map_join(formatted_options, "\n", &(" " <> &1))
end
end
defp add_aliases(base, name, reverse_aliases) do
case Map.get(reverse_aliases, name, []) do
[] ->
base
alias_list ->
alias_str =
alias_list
|> Enum.sort()
|> Enum.map_join(", ", &("-" <> Atom.to_string(&1)))
base <> " (alias: #{alias_str})"
end
end
end
+10 -8
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
@@ -450,7 +452,7 @@ defmodule PartitionSupervisor do
end
@doc """
Returns a list with information about all children.
Returns a list with information about all children of the given supervisor.
This function returns a list of tuples containing:
@@ -470,7 +472,7 @@ defmodule PartitionSupervisor do
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
]
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
def which_children(name) when is_name(name) do
Supervisor.which_children(name)
end
@@ -498,7 +500,7 @@ defmodule PartitionSupervisor do
supervisors: non_neg_integer,
workers: non_neg_integer
}
def count_children(supervisor) when is_atom(supervisor) do
def count_children(supervisor) when is_name(supervisor) do
Supervisor.count_children(supervisor)
end
@@ -514,7 +516,7 @@ defmodule PartitionSupervisor do
"""
@doc since: "1.14.0"
@spec stop(name(), reason :: term, timeout) :: :ok
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_atom(supervisor) do
def stop(supervisor, reason \\ :normal, timeout \\ :infinity) when is_name(supervisor) do
Supervisor.stop(supervisor, reason, timeout)
end
@@ -546,7 +548,7 @@ defmodule PartitionSupervisor do
end
@doc false
def unregister_name(_, _) do
def unregister_name(_) do
raise "{:via, PartitionSupervisor, _} cannot be given on unregistration"
end
end
+6 -4
View File
@@ -22,6 +22,8 @@ defmodule Path do
"""
@type t :: IO.chardata()
@type relative_to_opts :: [force: boolean()]
@doc """
Converts the given path to an absolute one.
@@ -401,7 +403,7 @@ defmodule Path do
Path.relative_to("../foo", "/usr/local") #=> "../foo"
"""
@spec relative_to(t, t, keyword) :: binary
@spec relative_to(t, t, relative_to_opts) :: binary
def relative_to(path, cwd, opts \\ []) when is_list(opts) do
os_type = major_os_type()
split_path = split(path)
@@ -479,11 +481,11 @@ defmodule Path do
Check `relative_to/3` for the supported options.
"""
@spec relative_to_cwd(t, keyword) :: binary
@spec relative_to_cwd(t, relative_to_opts) :: binary
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
case :file.get_cwd() do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
_ -> path
_ -> IO.chardata_to_string(path)
end
end
@@ -801,7 +803,7 @@ defmodule Path do
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
"""
@spec wildcard(t, keyword) :: [binary]
@spec wildcard(t, match_dot: boolean()) :: [binary]
def wildcard(glob, opts \\ []) when is_list(opts) do
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
+1 -1
View File
@@ -78,7 +78,7 @@ defmodule Port do
The port can be opened through four main mechanisms.
As a short summary, prefer to using the `:spawn` and `:spawn_executable`
As a short summary, prefer to use the `:spawn` and `:spawn_executable`
options mentioned below. The other two options, `:spawn_driver` and `:fd`
are for advanced usage within the VM. Also consider using `System.cmd/3`
if all you want is to execute a program and retrieve its return value.
+43 -27
View File
@@ -202,24 +202,23 @@ defmodule Process do
@doc """
Sends an exit signal with the given `reason` to `pid`.
The following behavior applies if `reason` is any term except `:normal`
or `:kill`:
Exit behavior differs based on the value of `reason`:
1. If `pid` is not trapping exits, `pid` will exit with the given
`reason`.
- If `:normal`, `pid` will not exit unless it is the calling process, in
which case it will exit with the reason `:normal`. If it is trapping exits,
the exit signal is transformed into a message `{:EXIT, from, :normal}` and
delivered to its message queue.
2. If `pid` is trapping exits, the exit signal is transformed into a
message `{:EXIT, from, reason}` and delivered to the message queue
of `pid`.
- If `:kill`, which occurs when `Process.exit(pid, :kill)` is called, an
untrappable exit signal is sent to `pid` which will unconditionally exit
with reason `:killed`.
If `reason` is the atom `:normal`, `pid` will not exit (unless `pid` is
the calling process, in which case it will exit with the reason `:normal`).
If it is trapping exits, the exit signal is transformed into a message
`{:EXIT, from, :normal}` and delivered to its message queue.
- If any other term and `pid` is not trapping exits, `pid` will exit with
the given `reason`.
If `reason` is the atom `:kill`, that is if `Process.exit(pid, :kill)` is called,
an untrappable exit signal is sent to `pid` which will unconditionally exit
with reason `:killed`.
- If any other term and `pid` is trapping exits, the exit signal is
transformed into a message `{:EXIT, from, reason}` and delivered to its
message queue.
Inlined by the compiler.
@@ -388,8 +387,6 @@ defmodule Process do
automatically canceled when `dest` is an atom (as the atom resolution is done
on delivery).
Inlined by the compiler.
## Options
* `:abs` - (boolean) when `false`, `time` is treated as relative to the
@@ -535,7 +532,7 @@ defmodule Process do
If the process is already dead when calling `Process.monitor/1`, a
`:DOWN` message is delivered immediately.
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
See ["Links and monitors"](genservers.md#links-and-monitors)
for an example. See `:erlang.monitor/2` for more information.
Inlined by the compiler.
@@ -840,7 +837,7 @@ defmodule Process do
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:priority, priority_level) :: priority_level
@spec flag(:save_calls, 0..10000) :: 0..10000
@spec flag(:save_calls, 0..10_000) :: 0..10_000
@spec flag(:sensitive, boolean) :: boolean
@spec flag(:trap_exit, boolean) :: boolean
defdelegate flag(flag, value), to: :erlang, as: :process_flag
@@ -859,7 +856,7 @@ defmodule Process do
Inlined by the compiler.
"""
@spec flag(pid, :save_calls, 0..10000) :: 0..10000
@spec flag(pid, :save_calls, 0..10_000) :: 0..10_000
defdelegate flag(pid, flag, value), to: :erlang, as: :process_flag
@doc """
@@ -980,11 +977,35 @@ defmodule Process do
@spec unalias(alias) :: boolean
defdelegate unalias(alias), to: :erlang
@doc """
Returns the label set for the process `pid` as set with `set_label/1`
or `:proc_lib.set_label/1`.
Defaults to the current process when `pid` is not passed.
## Examples
Process.set_label({:any, "term"})
Process.get_label()
#=> {:any, "term"}
Returns `nil` when not set:
Process.get_label(pid)
#=> nil
"""
@doc since: "1.20.0"
@spec get_label(pid()) :: term()
def get_label(pid \\ self()) do
nilify(:proc_lib.get_label(pid))
end
@doc """
Add a descriptive term to the current process.
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
Observer and in crash logs.
The term does not need to be unique, and will be shown in Observer and in
crash logs.
This label may be useful for identifying a process as one of multiple in a
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
@@ -998,12 +1019,7 @@ defmodule Process do
"""
@doc since: "1.17.0"
@spec set_label(term()) :: :ok
def set_label(label) do
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
Process.put(:"$process_label", label)
# mimic return value of `:proc_lib.set_label/2`
:ok
end
defdelegate set_label(label), to: :proc_lib
@compile {:inline, nilify: 1}
defp nilify(:undefined), do: nil
+199 -146
View File
@@ -21,7 +21,7 @@ defmodule Protocol do
the data structure.
Although Elixir includes specific functions such as `tuple_size`,
`binary_size` and `map_size`, sometimes we want to be able to
`byte_size` and `map_size`, sometimes we want to be able to
retrieve the size of a data structure regardless of its type.
In Elixir we can write polymorphic code, i.e. code that works
with different shapes/types, by using protocols. A size protocol
@@ -267,6 +267,8 @@ defmodule Protocol do
@optional_callbacks __deriving__: 2
@elixir_checker_version :elixir_erl.checker_version()
@doc false
defmacro def(signature)
@@ -285,6 +287,11 @@ defmodule Protocol do
call_args = :lists.map(to_var, :lists.seq(2, arity))
call_args = [quote(do: term) | call_args]
# TODO: Raise in Elixir v2.0
if :lists.any(&match?({:\\, _, [_, _]}, &1), args) do
IO.warn("default arguments in protocol definitions is deprecated", __CALLER__)
end
quote generated: true do
name = unquote(name)
arity = unquote(arity)
@@ -451,7 +458,7 @@ defmodule Protocol do
true
"""
@spec extract_protocols([charlist | String.t()]) :: [atom]
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_protocols(paths) do
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
case attributes[:__protocol__] do
@@ -480,7 +487,7 @@ defmodule Protocol do
true
"""
@spec extract_impls(module, [charlist | String.t()]) :: [atom]
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_impls(protocol, paths) when is_atom(protocol) do
prefix = Atom.to_charlist(protocol) ++ [?.]
@@ -494,17 +501,25 @@ defmodule Protocol do
defp extract_matching_by_attribute(paths, prefix, callback) do
for path <- paths,
# Do not use protocols as they may be consolidating
path = if(is_list(path), do: path, else: String.to_charlist(path)),
file <- list_dir(path),
{path, files} = list_dir(path),
file <- files,
mod = extract_from_file(path, file, prefix, callback),
do: mod
end
# Do not use protocols as they may be consolidating
defp list_dir({path, files}) when is_list(path) and is_list(files) do
{path, files}
end
defp list_dir(path) when is_binary(path) do
list_dir(String.to_charlist(path))
end
defp list_dir(path) when is_list(path) do
case :file.list_dir(path) do
{:ok, files} -> files
_ -> []
{:ok, files} -> {path, files}
_ -> {path, []}
end
end
@@ -564,31 +579,34 @@ defmodule Protocol do
# Ensure the types are sorted so the compiled beam is deterministic
types = Enum.sort(types)
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
{:ok, definitions, signatures} <-
consolidate(protocol, any, definitions, signatures, types),
do: compile(definitions, signatures, compile_info)
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
{:ok, definitions, checker} <-
consolidate(protocol, any, definitions, checker, types),
do: compile(definitions, checker, compile_info)
end
defp beam_protocol(protocol) do
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
opts = [:allow_missing_chunks]
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
%{attributes: attributes, definitions: definitions} = module_map
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
# TODO: Remove this on Elixir v1.23.
signatures = Map.get(module_map, :signatures, %{})
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
case attributes[:__protocol__] do
[fallback_to_any: any] ->
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
checker =
with true <- is_binary(checker),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
contents
else
_ -> nil
end
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
{:ok, any, definitions, checker, {module_map, specs, chunks}}
_ ->
{:error, :not_a_protocol}
@@ -607,7 +625,7 @@ defmodule Protocol do
end
# Consolidate the protocol for faster implementations and fine-grained type information.
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
case List.keytake(definitions, {:__protocol__, 1}, 0) do
{protocol_def, definitions} ->
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
@@ -623,25 +641,37 @@ defmodule Protocol do
protocol_def = change_protocol(protocol_def, types)
impl_for = change_impl_for(impl_for, protocol, types)
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
new_signatures = new_signatures(definitions, protocol_funs, protocol, types)
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
signatures = Enum.into(new_signatures, signatures)
{:ok, definitions, signatures}
checker =
if checker do
update_in(checker.exports, fn exports ->
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
for {fun, info} <- exports do
if sig = Map.get(signatures, fun) do
{fun, %{info | sig: sig}}
else
{fun, info}
end
end
end)
end
{:ok, definitions, checker}
nil ->
{:error, :not_a_protocol}
end
end
defp new_signatures(definitions, protocol_funs, protocol, types) do
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
alias Module.Types.Descr
types_minus_any = List.delete(types, Any)
clauses =
types
|> List.delete(Any)
|> Enum.map(fn impl ->
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
Enum.map(types_minus_any, fn impl ->
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
end)
{domain, impl_for, impl_for!} =
@@ -656,10 +686,16 @@ defmodule Protocol do
end
_ ->
structs_domain =
case structs do
[] -> Descr.none()
_ -> Descr.open_map(__struct__: Descr.atom(structs))
end
domain =
clauses
|> Enum.map(fn {[domain], _} -> domain end)
|> Enum.reduce(&Descr.union/2)
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
Descr.union(Module.Types.Of.impl(impl, :open), acc)
end)
not_domain = Descr.negation(domain)
@@ -680,10 +716,12 @@ defmodule Protocol do
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
end
[
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
] ++ new_signatures
Map.new(
[
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
] ++ new_signatures
)
end
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
@@ -748,15 +786,13 @@ defmodule Protocol do
end
defp fallback_clause_for(value, _protocol, meta) do
{meta, [quote(do: _)], [], value}
{meta, [{:_, [version: -1], __MODULE__}], [], value}
end
# Finally compile the module and emit its bytecode.
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
# TODO: Remove this on Elixir v1.23.
module_map = %{module_map | definitions: definitions} |> Map.put(:signatures, signatures)
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
module_map = %{module_map | definitions: definitions}
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
end
## Definition callbacks
@@ -769,16 +805,7 @@ defmodule Protocol do
@before_compile Protocol
# We don't allow function definition inside protocols
import Kernel,
except: [
def: 1,
def: 2,
defdelegate: 2,
defguard: 1,
defguardp: 1,
defstruct: 1,
defexception: 1
]
import Kernel, except: [def: 1, def: 2]
# Import the new `def` that is used by protocols
import Protocol, only: [def: 1]
@@ -789,12 +816,11 @@ defmodule Protocol do
# Set up a clear slate to store defined functions
@__functions__ []
@fallback_to_any false
@undefined_impl_description ""
# Invoke the user given block
_ = unquote(block)
# Finalize expansion
res = unquote(block)
unquote(after_defprotocol())
res
end
end
end
@@ -841,6 +867,21 @@ defmodule Protocol do
)
end
extra =
((Module.definitions_in(env.module, :def) ++ Module.definitions_in(env.module, :defmacro)) --
functions) --
[impl_for: 1, impl_for!: 1, __protocol__: 1, __deriving__: 2, __deriving__: 3]
# TODO: Make an error on Elixir v2.0
if extra != [] do
warn(
"protocols can only define functions without implementation via def/1, found: " <>
Enum.map_join(extra, ", ", fn {name, arity} -> "#{name}/#{arity}" end),
env,
nil
)
end
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
@@ -885,57 +926,105 @@ defmodule Protocol do
end
defp after_defprotocol do
quote bind_quoted: [built_in: built_in()] do
any_impl_for =
if @fallback_to_any do
__MODULE__.Any
else
nil
prefix =
quote bind_quoted: [built_in: built_in()] do
any_impl_for =
if @fallback_to_any do
Protocol.__concat__(__MODULE__, "Any")
else
nil
end
# Disable Dialyzer checks - before and after consolidation
# the types could be more strict
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
@doc false
@spec impl_for(term) :: atom | nil
Kernel.def(impl_for(data))
# Define the implementation for structs.
#
# It simply delegates to struct_impl_for which is then
# optimized during protocol consolidation.
Kernel.def impl_for(%struct{}) do
struct_impl_for(struct)
end
# Disable Dialyzer checks - before and after consolidation
# the types could be more strict
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
# Define the implementation for built-ins
:lists.foreach(
fn {mod, guard} ->
target = Protocol.__concat__(__MODULE__, mod)
@doc false
@spec impl_for(term) :: atom | nil
Kernel.def(impl_for(data))
# Define the implementation for structs.
#
# It simply delegates to struct_impl_for which is then
# optimized during protocol consolidation.
Kernel.def impl_for(%struct{}) do
struct_impl_for(struct)
end
# Define the implementation for built-ins
:lists.foreach(
fn {mod, guard} ->
target = Protocol.__concat__(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
end
end,
built_in
)
end,
built_in
)
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
# destructuring opaque types is illegal, Dialyzer will think none of the
# previous clauses matches opaque types, and without this clause, will
# conclude that impl_for can't handle an opaque argument). This is a hack
# since it relies on Dialyzer not being smart enough to conclude that all
# opaque types will get the any_impl_for/0 implementation.
Kernel.def impl_for(_) do
unquote(any_impl_for)
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
# Inline struct implementation for performance
@compile {:inline, struct_impl_for: 1}
if not Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@type t :: term
end
# Store information as an attribute so it
# can be read without loading the module.
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
@__protocol__ [fallback_to_any: !!@fallback_to_any]
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: [{atom(), arity()}]
@spec __protocol__(:consolidated?) :: boolean()
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
Kernel.def(__protocol__(:module), do: __MODULE__)
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
Kernel.def(__protocol__(:consolidated?), do: false)
Kernel.def(__protocol__(:impls), do: :not_consolidated)
end
undefined_impl_description =
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
raise =
quote do
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: @undefined_impl_description
)
end
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
# destructuring opaque types is illegal, Dialyzer will think none of the
# previous clauses matches opaque types, and without this clause, will
# conclude that impl_for can't handle an opaque argument). This is a hack
# since it relies on Dialyzer not being smart enough to conclude that all
# opaque types will get the any_impl_for/0 implementation.
impl_for_fallback =
quote generated: true, bind_quoted: [] do
Kernel.def impl_for(_) do
unquote(any_impl_for)
end
end
quote generated: true do
unquote(prefix)
unquote(impl_for_fallback)
@doc false
@spec impl_for!(term) :: atom
@@ -945,47 +1034,9 @@ defmodule Protocol do
end
else
Kernel.def impl_for!(data) do
impl_for(data) ||
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: unquote(undefined_impl_description)
)
impl_for(data) || unquote(raise)
end
end
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
# Inline struct implementation for performance
@compile {:inline, struct_impl_for: 1}
if not Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@type t :: term
end
# Store information as an attribute so it
# can be read without loading the module.
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
@__protocol__ [fallback_to_any: !!@fallback_to_any]
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: [{atom(), arity()}]
@spec __protocol__(:consolidated?) :: boolean()
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
Kernel.def(__protocol__(:module), do: __MODULE__)
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
Kernel.def(__protocol__(:consolidated?), do: false)
Kernel.def(__protocol__(:impls), do: :not_consolidated)
end
end
@@ -1172,10 +1223,12 @@ defmodule Protocol do
end
@doc false
def __concat__(left, right) do
String.to_atom(
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
)
def __concat__(left, right) when is_atom(right) do
__concat__(left, remove_prefix(Atom.to_string(right)))
end
def __concat__(left, right) when is_binary(right) do
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
end
defp ensure_prefix("Elixir." <> _ = left), do: left

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