Compare commits

...
Author SHA1 Message Date
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
166 changed files with 7282 additions and 2679 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
+7 -7
View File
@@ -26,12 +26,12 @@ jobs:
fail-fast: false
matrix:
include:
- otp_version: "29.0-rc1"
- otp_version: "28.1"
- otp_version: "29.0"
deterministic: true
- otp_version: "28.1"
- otp_version: "28.4"
docs: true
coverage: true
- otp_version: "28.1"
- otp_version: "27.3"
- otp_version: "27.0"
- otp_version: master
@@ -46,7 +46,7 @@ jobs:
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: ${{ matrix.otp_version }}
@@ -92,7 +92,7 @@ jobs:
- name: "Upload Coverage Artifact"
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: TestCoverage
path: cover/*
@@ -115,7 +115,7 @@ jobs:
strategy:
matrix:
otp_version:
- "29.0-rc1"
- "29.0"
- "28.1"
- "27.3"
@@ -127,7 +127,7 @@ jobs:
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: ${{ matrix.otp_version }}
+3 -3
View File
@@ -32,12 +32,12 @@ jobs:
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
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@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: "/language:${{matrix.language}}"
@@ -52,4 +52,4 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+1 -1
View File
@@ -38,4 +38,4 @@ jobs:
persist-credentials: false
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
+14 -14
View File
@@ -79,7 +79,7 @@ jobs:
build_docs: build_docs
- otp: 29
otp_version: "29.0-rc1"
otp_version: "29.0"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -100,19 +100,19 @@ jobs:
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload Windows release artifacts"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.build_docs
with:
name: Docs
@@ -139,20 +139,20 @@ jobs:
steps:
- name: "Download build"
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@87c2e83e6868da99d3380aa309851b32ed9a8346 # v1.1.0
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
@@ -182,7 +182,7 @@ jobs:
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
@@ -211,7 +211,7 @@ jobs:
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
@@ -226,7 +226,7 @@ jobs:
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@07e74fc4e78d1aad915e867f9a094073a9f71527 # v4.0.0
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
@@ -258,7 +258,7 @@ jobs:
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
- name: "Assemble Release SBoM Artifacts"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "SBoM"
path: |
@@ -268,7 +268,7 @@ jobs:
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "Attestations"
path: "attestations/*.sigstore"
@@ -282,7 +282,7 @@ jobs:
contents: write
steps:
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
@@ -333,7 +333,7 @@ jobs:
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
with:
persist-credentials: false
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
- uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: "27.3"
elixir-version: "1.18.3"
+135 -35
View File
@@ -10,29 +10,9 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
## Type system improvements
This release includes type inference of all constructs.
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
### Type inference of function definitions
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
```elixir
def add_foo_and_bar(data) do
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
### Type inference of guards
@@ -70,6 +50,28 @@ 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
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
### Typing across clauses
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
@@ -81,11 +83,11 @@ case System.get_env("SOME_VAR") do
end
```
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system now 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.
`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.
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.
### Complete typing of maps keys
### 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()`.
@@ -175,6 +177,110 @@ The code above has a type violation, which is now caught by the type system:
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
## Compile-time improvements
Elixir's v1.20 improves compilation times once more, especially on applications with many 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:
* 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
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
## v1.20.0-rc.6 (2026-05-21)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### Elixir
* [Kernel] Perform type inference across applications
### 2. Bug fixes
#### Elixir
* [Kernel] Fix type checker bug when validating a `case` inside a `cond` condition (regression)
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
#### Mix
* [mix deps] Do not show dependency as diverged when it actually requires compilation (regression)
* [mix deps] Revert allowing overriding specific dependencies in `:override` due to conflicts with Hex (revert)
* [mix test] Respect --raise when mix test --warnings-as-errors passes with warnings
### 3. Hard deprecations
#### Mix
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
## v1.20.0-rc.5 (2026-05-13)
### 1. Enhancements
#### EEx
* [EEx] Optimize compiler by flattening expr list only once
#### Elixir
* [Base] Optimize Base validation functions by using SWAR techniques
* [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)
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
* [Registry] Switch `{:duplicate, :key}` key_ets to ordered_set with composite keys
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
#### ExUnit
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
#### IEx
* [IEx.Helpers] Add `source/1`
#### Mix
* [mix app.tree] Support `--output` option
* [mix deps.tree] Support `--output` option
* [mix help] Support printing docs for types and callbacks
* [mix format] Support `--no-compile` option
* [mix source] Add `mix source MODULE` to print or open a given module/function location
### 2. Potential breaking changes
#### Elixir
* [Kernel] Disallow raw CR line ending in strings, comments and after `?` for security reasons
### 3. Bug fixes
#### Elixir
* [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] Reject negative Duration in `to_timeout/1`
* [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`
#### Logger
* [Logger] Persist log level to app env in `Logger.configure/1`
#### Mix
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
## v1.20.0-rc.4 (2026-03-31)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
@@ -187,7 +293,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [File] Add support for `[:raw]` opts in `File.read/2`
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Module] Purge and delete modules if `after_verify/2` callback fails
* [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`
@@ -232,7 +338,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
#### Elixir
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Preserve directory permissions in `File.cp_r/3`
* [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] Warn when defining `@type record()`, fixes CI on Erlang/OTP 29
@@ -261,13 +367,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
* [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 deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
### 2. Potential breaking changes
#### Elixir
* `map.foo()` (accessing a map field with parens) and `mod.foo` (invoking a function without parens) will now raise instead of emitting runtime warnings, aligning themselves with the type system behaviour
### 3. Bug fixes
### 2. Bug fixes
#### IEx
@@ -318,7 +418,7 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
#### Elixir
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note that while Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
* `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, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
### 3. Hard deprecations
+68 -25
View File
@@ -99,15 +99,11 @@ case you are looking for some examples:
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
All pull requests must be reviewed before being merged into the repository.
If changes are necessary, the team will leave appropriate comments requesting
changes to the code. Unfortunately, we cannot guarantee a pull request will
be merged, even when modifications are requested, as the Elixir team will
re-evaluate the contribution as it changes.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
@@ -118,6 +114,29 @@ into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir.
After cloning and compiling Elixir, run:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
# and generate HTML and EPUB documents:
make docs
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
## Licensing and Compliance Requirements
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
@@ -181,25 +200,49 @@ affecting **all external contributors**:
See <https://developercertificate.org/> for a copy of the Developer Certificate
of Origin license.
## Building documentation
## Using AI and coding agents
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir.
While we allow the use of AI on contributions and discussions, please be mindful
when doing so. Generally speaking, Elixir maintainers already have access to AI
(like many other developers). Therefore, if we need the feedback or help of a
coding agent, we can request so ourselves. For this reason, we often find
the point of view of the human behind the agent more valuable.
After cloning and compiling Elixir, run:
That said, here are examples of how one might (or might not) use AI and coding
agents in Elixir spaces:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
* When it comes to discussions, using AI to help express yourself is welcome,
but avoid directly copy and pasting AI generated content. If there is a language
barrier, use AI to translate, review, and improve your text, but do not use AI
to respond on your behalf.
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
* Do not use coding agents to tackle existing issues unless they have the
"Contributions Welcome" label.
# and generate HTML and EPUB documents:
make docs
```
* If you request a feature on the mailing list and it is accepted, you may
use coding agents to implement it, as long as it follows the AI Contributions
guidelines below.
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
* When automating AI usage on the Elixir codebase for performance improvements
or security fixes, pair it with a separate set of agents whose job is to argue
against and try to invalidate any proposed change. And treat their approval as
advisory: a human must still validate it before opening issues or pull requests.
If any code is written by AI, then you must follow the guidelines below.
### AI contributions
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
Certificate of Origin (DCO). The human submitter is responsible for:
* Reviewing all AI-generated code
* Ensuring compliance with licensing requirements
* Adding their own Signed-off-by tag to certify the DCO
* Taking full responsibility for the contribution
* Disclosing use of AI for comments and code contributions
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
AI in the development process. Contributions should include an Assisted-by tag in the
following format:
Assisted-by: AGENT_NAME:MODEL_VERSION
+1 -1
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
+4 -3
View File
@@ -122,9 +122,10 @@ variable `ERL_COMPILER_OPTIONS=deterministic`.
Contributions to Elixir are always welcome! Before you get started, please check
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
guidelines on how to set up your environment, run the test suite, format your
code, and submit pull requests. We also include information on our review
process, licensing requirements, and helpful tips to ensure a smooth
contribution experience.
code, and submit pull requests.
Note you must disclose the use of coding agents and AI written code in your
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
## Development links
+2 -3
View File
@@ -12,12 +12,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Development
1.19 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.20.0-rc.4
1.20.0-rc.6
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.0-rc.4
ELIXIR_VERSION=1.20.0-rc.6
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.20.0-rc.4
set ELIXIR_VERSION=1.20.0-rc.6
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+25 -12
View File
@@ -303,7 +303,7 @@ defmodule EEx.Compiler do
file: file,
source: source,
line: line,
quoted: [],
quoted: %{},
parser_options: [indentation: indentation] ++ parser_options,
indentation: indentation
}
@@ -347,7 +347,7 @@ defmodule EEx.Compiler do
state.parser_options
expr = Code.string_to_quoted!(chars, options)
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
buffer = handle_expr(buffer, mark, expr, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -366,7 +366,7 @@ defmodule EEx.Compiler do
rest,
state.engine.handle_begin(buffer),
[{contents, start_line, start_column} | scope],
%{state | quoted: [], line: line}
%{state | quoted: %{}, line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
@@ -376,7 +376,7 @@ defmodule EEx.Compiler do
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
buffer = handle_expr(buffer, mark, contents, meta, state)
generate_buffer(rest, buffer, scope, state)
end
@@ -410,7 +410,7 @@ defmodule EEx.Compiler do
) do
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
options = [file: state.file, line: line, column: column] ++ state.parser_options
tuples = Code.string_to_quoted!(wrapped, options)
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options)
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
end
@@ -426,7 +426,7 @@ defmodule EEx.Compiler do
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
message = "expected a closing '<% end %>' for block expression in EEx"
expr_meta = non_whitespace_meta(content, line, column, state)
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state)
syntax_error!(message, expr_meta, state)
end
@@ -443,10 +443,10 @@ defmodule EEx.Compiler do
defp wrap_expr(current, line, buffer, chars, state) do
new_lines = List.duplicate(?\n, line - state.line)
key = length(state.quoted)
placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
count = current ++ placeholder ++ new_lines ++ chars
new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
key = map_size(state.quoted)
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"]
count = [current, placeholder, new_lines, chars]
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))}
{count, new_state}
end
@@ -479,8 +479,7 @@ defmodule EEx.Compiler do
# Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
{^key, value} = List.keyfind(quoted, key, 0)
value
Map.fetch!(quoted, key)
end
defp insert_quoted({left, line, right}, quoted) do
@@ -513,6 +512,20 @@ defmodule EEx.Compiler do
column: meta.column
end
defp handle_expr(buffer, mark, expr, meta, state) do
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
rescue
e in EEx.SyntaxError ->
reraise %{
e
| file: e.file || state.file,
line: e.line || meta.line,
column: e.column || meta.column,
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
},
__STACKTRACE__
end
defp code_snippet(source, indentation, meta) do
line_start = max(meta.line - 3, 1)
line_end = meta.line
+12
View File
@@ -575,6 +575,18 @@ defmodule EExTest do
EEx.compile_string("foo <%= bar", file: "my_file.eex")
end
end
test "unsupported marker error carries template location metadata" do
error =
assert_raise EEx.SyntaxError, fn ->
EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
end
assert error.file == "sample.eex"
assert error.line == 7
assert error.column == 1
assert Exception.message(error) =~ "sample.eex:7:1:"
end
end
describe "warnings" do
+5
View File
@@ -1091,6 +1091,11 @@ defmodule Access do
defp normalize_range(range, _list), do: range
defp get_and_update_slice(rest, %Range{last: last}, _next, updates, gets, index)
when index > last do
{:lists.reverse(gets), :lists.reverse(updates, rest)}
end
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
if index in range do
case next.(head) do
+278 -54
View File
@@ -155,6 +155,255 @@ defmodule Base do
for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>>
end
# SWAR (SIMD Within A Register) fast paths for valid16?/2 and valid32?/2
# (non-hex). Each chunk of 8 bytes is validated in one guard: 7 bytes via
# bitwise arithmetic on a single 56-bit integer, plus a per-byte range
# check for the 8th byte. 56 bits is the largest width that fits in a BEAM
# small int on 64-bit (fixnum range is 59-bit signed); at 64 bits every
# `w + 0x80..` would allocate a bignum on the heap and the optimisation
# would collapse. See https://github.com/erlang/otp/pull/10938 for the
# corresponding pattern in OTP.
@swar_mask80 0x80808080808080
# Per-range SWAR constants, broadcast across 7 lanes. Naming convention:
# @swar_ge_X = 0x80 - X → high bit of `(w + @swar_ge_X)` lane is set
# iff that byte is ≥ X
# @swar_gt_X = 0x7F - X → high bit of `(w + @swar_gt_X)` lane is set
# iff that byte is > X
# A byte is in range [lo, hi] iff
# `bxor(w + @swar_ge_lo, w + @swar_gt_hi)` has its high bit set.
@swar_ge_0 0x50505050505050
@swar_gt_9 0x46464646464646
@swar_ge_2 0x4E4E4E4E4E4E4E
@swar_gt_7 0x48484848484848
@swar_ge_A 0x3F3F3F3F3F3F3F
@swar_gt_F 0x39393939393939
@swar_gt_V 0x29292929292929
@swar_gt_Z 0x25252525252525
@swar_ge_a 0x1F1F1F1F1F1F1F
@swar_gt_f 0x19191919191919
@swar_gt_v 0x09090909090909
@swar_gt_z 0x05050505050505
# For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we
# extend the digit range to [0x2F, 0x39], which absorbs '/' into one range
# check — saves one Mycroft singleton. Trick lifted from
# https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/
@swar_ge_slash 0x51515151515151
# Mycroft zero-byte detection for base64 singletons (+, -, _).
# Per lane: high bit set iff `bxor(w, K*ones) - 0x01..01` has its high bit
# set, i.e. that byte's V value was 0 → original byte was K. Simplified
# (no `bnot V` term) — for ASCII-gated `w`, borrow-propagation false
# positives only occur for adjacent bytes that happen to equal `K xor 0x01`,
# which is outside the base64 alphabet, so it never matters here.
# Pattern follows https://github.com/elixir-lang/elixir/pull/15255.
@swar_mask01 0x01010101010101
@swar_plus_x7 0x2B2B2B2B2B2B2B
@swar_dash_x7 0x2D2D2D2D2D2D2D
@swar_under_x7 0x5F5F5F5F5F5F5F
# Per-byte validity checks (used in both the SWAR clauses for the 8th byte
# of each stride and in the body of the sub-8-byte tail clauses).
@compile {:inline,
valid_char16upper?: 1,
valid_char16lower?: 1,
valid_char16mixed?: 1,
valid_char32upper?: 1,
valid_char32lower?: 1,
valid_char32mixed?: 1,
valid_char32hexupper?: 1,
valid_char32hexlower?: 1,
valid_char32hexmixed?: 1,
valid_char64base?: 1,
valid_char64url?: 1,
valid_word16upper?: 1,
valid_word16lower?: 1,
valid_word16mixed?: 1,
valid_word32upper?: 1,
valid_word32lower?: 1,
valid_word32mixed?: 1,
valid_word32hexupper?: 1,
valid_word32hexlower?: 1,
valid_word32hexmixed?: 1,
valid_word64base?: 1,
valid_word64url?: 1}
defp valid_char16upper?(c), do: c in ?0..?9 or c in ?A..?F
defp valid_char16lower?(c), do: c in ?0..?9 or c in ?a..?f
defp valid_char16mixed?(c), do: c in ?0..?9 or c in ?A..?F or c in ?a..?f
defp valid_char32upper?(c), do: c in ?A..?Z or c in ?2..?7
defp valid_char32lower?(c), do: c in ?a..?z or c in ?2..?7
defp valid_char32mixed?(c), do: c in ?A..?Z or c in ?a..?z or c in ?2..?7
# Most common range first — letters dominate (22/32) over digits (10/32)
# in hex base32, so letters go first in the OR short-circuit.
defp valid_char32hexupper?(c), do: c in ?A..?V or c in ?0..?9
defp valid_char32hexlower?(c), do: c in ?a..?v or c in ?0..?9
defp valid_char32hexmixed?(c), do: c in ?A..?V or c in ?a..?v or c in ?0..?9
defp valid_char64base?(c),
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?+ or c == ?/
defp valid_char64url?(c),
do: c in ?A..?Z or c in ?a..?z or c in ?0..?9 or c == ?- or c == ?_
# SWAR 7-byte word validity. Structure for each function:
# 1. ASCII gate `band(w, MASK80) == 0` — every byte < 0x80 so the
# additions below cannot carry across lanes.
# 2. "Each byte is in range A OR range B (OR range C)" gate — OR per-
# range XOR masks (high bit set in lane iff byte in that range), AND
# with MASK80, demand all 7 high bits set.
defp valid_word16upper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_F)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word16lower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_a, w + @swar_gt_f)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word16mixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_F)
),
bxor(w + @swar_ge_a, w + @swar_gt_f)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32upper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32lower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_a, w + @swar_gt_z),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32mixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_2, w + @swar_gt_7)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexupper?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_V)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexlower?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_a, w + @swar_gt_v)
),
@swar_mask80
) == @swar_mask80
end
defp valid_word32hexmixed?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bxor(w + @swar_ge_0, w + @swar_gt_9),
bxor(w + @swar_ge_A, w + @swar_gt_V)
),
bxor(w + @swar_ge_a, w + @swar_gt_v)
),
@swar_mask80
) == @swar_mask80
end
# base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons.
# For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as
# part of one range (Lemire merge), leaving only '+' as a Mycroft singleton.
# For url, the singletons '-' and '_' are detected via two Mycroft terms.
defp valid_word64base?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_slash, w + @swar_gt_9)
),
bxor(w, @swar_plus_x7) - @swar_mask01
),
@swar_mask80
) == @swar_mask80
end
defp valid_word64url?(w) do
band(w, @swar_mask80) == 0 and
band(
bor(
bor(
bor(
bxor(w + @swar_ge_A, w + @swar_gt_Z),
bxor(w + @swar_ge_a, w + @swar_gt_z)
),
bxor(w + @swar_ge_0, w + @swar_gt_9)
),
bor(
bxor(w, @swar_dash_x7) - @swar_mask01,
bxor(w, @swar_under_x7) - @swar_mask01
)
),
@swar_mask80
) == @swar_mask80
end
@doc """
Encodes a binary string into a base 16 encoded string.
@@ -371,25 +620,21 @@ defmodule Base do
decode_name = :"decode16#{base}!"
validate_name = :"validate16#{base}?"
valid_char_name = :"valid_char16#{base}?"
valid_word_name = :"valid_word16#{base}?"
{min, decoded} = to_decode_list.(alphabet)
# SWAR fast path: 7 bytes per stride, validated entirely via
# `valid_word16<base>?` in the body. The `and` short-circuits when SWAR
# fails on any byte. Tail bytes (1-6 leftover) recurse through the
# single-byte clause below.
defp unquote(validate_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_name)(rest)
defp unquote(validate_name)(<<>>), do: true
defp unquote(validate_name)(<<c1, c2, rest::binary>>) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(validate_name)(rest)
end
defp unquote(validate_name)(<<_char, _rest::binary>>), do: false
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(validate_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_name)(rest)
defp unquote(decode_name)(char) do
index = char - unquote(min)
@@ -761,23 +1006,19 @@ defmodule Base do
validate_name = :"validate64#{base}?"
validate_main_name = :"validate_main64#{validate_name}?"
valid_char_name = :"valid_char64#{base}?"
valid_word_name = :"valid_word64#{base}?"
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
# SWAR fast path: 7 bytes per stride, validated via `valid_word64<base>?`
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
# 8-byte-multiple `main`) recurses through the single-byte clause.
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_main_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -863,13 +1104,6 @@ defmodule Base do
end
end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do
index = char - unquote(min)
@@ -1425,21 +1659,18 @@ defmodule Base do
valid_char_name = :"valid_char32#{base}?"
{min, decoded} = to_decode_list.(alphabet)
# SWAR fast path: 7 bytes per stride, validated via `valid_word32<base>?`
# in the body. Tail leftover (1-6 bytes after a 7-byte stride hits an
# 8-byte-multiple `main`) recurses through the single-byte clause.
valid_word_name = :"valid_word32#{base}?"
defp unquote(validate_main_name)(<<w::56, rest::binary>>),
do: unquote(valid_word_name)(w) and unquote(validate_main_name)(rest)
defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_main_name)(<<char, rest::binary>>),
do: unquote(valid_char_name)(char) and unquote(validate_main_name)(rest)
defp unquote(validate_name)(<<>>, _pad?), do: true
@@ -1519,13 +1750,6 @@ defmodule Base do
end
end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do
index = char - unquote(min)
+8 -5
View File
@@ -671,7 +671,7 @@ defmodule Calendar.ISO do
{{year, month, day}, time_from_day_fraction(day_fraction)}
{extra_days, day_fraction} ->
base_days = date_to_iso_days(year, month, day)
base_days = valid_date_to_iso_days(year, month, day)
{date_from_iso_days(base_days + extra_days), time_from_day_fraction(day_fraction)}
end
@@ -892,12 +892,15 @@ defmodule Calendar.ISO do
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_iso_days(0, 1, 1), do: 0
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
def date_to_iso_days(year, month, day) do
ensure_day_in_month!(year, month, day)
valid_date_to_iso_days(year, month, day)
end
defp valid_date_to_iso_days(0, 1, 1), do: 0
defp valid_date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
defp valid_date_to_iso_days(year, month, day) do
y = if month <= 2, do: year - 1, else: year
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
year_of_era = y - era * @years_per_era
@@ -1875,7 +1878,7 @@ defmodule Calendar.ISO do
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
valid_date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
+36 -30
View File
@@ -715,16 +715,18 @@ defmodule NaiveDateTime do
"""
@spec to_date(Calendar.naive_datetime()) :: Date.t()
def to_date(%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
}) do
def to_date(
%{
year: year,
month: month,
day: day,
calendar: calendar,
hour: _,
minute: _,
second: _,
microsecond: _
} = _naive_datetime
) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@@ -741,16 +743,18 @@ defmodule NaiveDateTime do
"""
@spec to_time(Calendar.naive_datetime()) :: Time.t()
def to_time(%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
def to_time(
%{
year: _,
month: _,
day: _,
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = _naive_datetime
) do
%Time{
hour: hour,
minute: minute,
@@ -1146,16 +1150,18 @@ defmodule NaiveDateTime do
"""
@doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds(%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
}) do
def to_gregorian_seconds(
%{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: {microsecond, precision}
} = _naive_datetime
) do
{days, day_fraction} =
calendar.naive_datetime_to_iso_days(
year,
+5 -8
View File
@@ -286,7 +286,7 @@ defmodule Code do
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (atom() -> term()),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
emit_warnings: boolean()
]
@@ -1319,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
@@ -1764,12 +1764,9 @@ 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.
When setting this option, we recommend running `mix clean` so the modules can be
recompiled with the new behaviour. `mix test` automatically disables this option
via the `:test_elixirc_options` project configuration, as there is typically no
need to infer signatures for test files.
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]`.
* `:module_definition` (since v1.20.0) - stores if the module definition should
be `:compiled` (the default) or `:interpreted`. Note this does not affect the
+16 -9
View File
@@ -68,7 +68,7 @@ defmodule Code.Normalizer do
# Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state)
normalize_bitstring(quoted, state, false)
end
# Atoms with interpolations
@@ -89,13 +89,7 @@ defmodule Code.Normalizer do
normalize_literal(:utf8, [], state)
end
string =
if state.escape do
normalize_bitstring(string, state, true)
else
normalize_bitstring(string, state)
end
string = normalize_bitstring(string, state, state.escape)
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end
@@ -118,6 +112,7 @@ defmodule Code.Normalizer do
end
end)
parts = maybe_add_trailing_newline(call_meta, parts, state)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else
normalize_call(quoted, state)
@@ -405,7 +400,8 @@ defmodule Code.Normalizer do
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do
parts = maybe_add_trailing_newline(meta, parts, state)
meta = patch_meta_line(meta, state.parent_meta)
parts =
@@ -427,6 +423,17 @@ defmodule Code.Normalizer do
{:<<>>, meta, parts}
end
defp maybe_add_trailing_newline(meta, parts, state) do
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
last = List.last(parts),
true <- is_binary(last) and not String.ends_with?(last, "\n") do
[_last | rest] = Enum.reverse(parts)
Enum.reverse([last <> "\n" | rest])
else
_ -> parts
end
end
defp normalize_interpolation_parts(parts, state, escape_interpolation) do
Enum.map(parts, fn
{:"::", interpolation_meta,
+16
View File
@@ -69,6 +69,22 @@ defprotocol Collectable do
iex> Enum.into([1, 2, 3], MapSet.new())
MapSet.new([1, 2, 3])
## Halting
The `:halt` flag will be given whenever the collection won't
terminate correctly and must be used to clean up existing resources
(such as sockets, file handles, etc).
Note it is not guaranteed that the accumulator given to halt will
be the latest version of the accumulator returned by a previous call
with `{:cont, elem}`. Therefore, you must track the collected results
within the resource you intend to halt.
This is by design: ensuring halt is always called with the latest
accumulator would make pure collectables (the ones that do not implement
halt) expensive. However, given the collectables that must implement halt
already need to track state, the burden of tracking the accumulator
across invocations is put on them.
"""
@type command :: {:cont, term} | :done | :halt
+13 -7
View File
@@ -377,21 +377,27 @@ defmodule Config do
end
end
defp validate!(config, file) do
Enum.all?(config, fn
defp validate!(config, file) when is_list(config) do
Enum.each(config, fn
{app, value} when is_atom(app) ->
if Keyword.keyword?(value) do
true
else
if not Keyword.keyword?(value) do
raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}"
end
_ ->
false
other ->
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
end)
config
end
defp validate!(config, file) do
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got: #{inspect(config)}"
end
end
+11 -16
View File
@@ -829,7 +829,7 @@ defmodule Enum do
"""
@spec dedup(t) :: list
def dedup(enumerable) when is_list(enumerable) do
dedup_list(enumerable, []) |> :lists.reverse()
dedup_list(enumerable)
end
def dedup(enumerable) do
@@ -2023,11 +2023,16 @@ defmodule Enum do
operators work by using this function.
"""
@spec member?(t, element) :: boolean
def member?(enumerable, element) when is_list(enumerable) do
def member?(enumerable, element) do
__in__(element, enumerable)
end
@doc false
def __in__(element, enumerable) when is_list(enumerable) do
:lists.member(element, enumerable)
end
def member?(enumerable, element) do
def __in__(element, enumerable) do
case Enumerable.member?(enumerable, element) do
{:ok, element} when is_boolean(element) ->
element
@@ -4507,19 +4512,9 @@ defmodule Enum do
# dedup
defp dedup_list([value | tail], acc) do
acc =
case acc do
[^value | _] -> acc
_ -> [value | acc]
end
dedup_list(tail, acc)
end
defp dedup_list([], acc) do
acc
end
defp dedup_list([value | [value | _] = tail]), do: dedup_list(tail)
defp dedup_list([value | tail]), do: [value | dedup_list(tail)]
defp dedup_list([]), do: []
## drop
+4 -4
View File
@@ -26,7 +26,7 @@ defmodule Exception do
@typedoc "The exception type"
@type t :: %{
required(:__struct__) => module,
required(:__exception__) => true,
required(:__exception__) => term,
optional(atom) => any
}
@@ -77,7 +77,7 @@ defmodule Exception do
@doc false
@deprecated "Use Kernel.is_exception/1 instead"
def exception?(term)
def exception?(%_{__exception__: true}), do: true
def exception?(%_{__exception__: _}), do: true
def exception?(_), do: false
@doc """
@@ -89,7 +89,7 @@ defmodule Exception do
return a descriptive error message instead.
"""
@spec message(t) :: String.t()
def message(%module{__exception__: true} = exception) do
def message(%module{__exception__: _} = exception) do
try do
module.message(exception)
rescue
@@ -123,7 +123,7 @@ defmodule Exception do
@spec normalize(:error, any, stacktrace) :: t
@spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var
def normalize(kind, payload, stacktrace \\ [])
def normalize(:error, %_{__exception__: true} = payload, _stacktrace), do: payload
def normalize(:error, %_{__exception__: _} = payload, _stacktrace), do: payload
def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace)
def normalize(_kind, payload, _stacktrace), do: payload
+52 -24
View File
@@ -1103,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.
@@ -1114,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
@@ -1151,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"
@@ -1176,7 +1183,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()
) ::
{:ok, [binary]} | {:error, posix | :badarg | :terminated, binary}
@@ -1198,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
@@ -1217,7 +1226,14 @@ defmodule File do
else
dereference = if dereference?, do: MapSet.new(), else: nil
case do_cp_r(source, destination, on_conflict, dereference, []) do
case do_cp_r(
source,
destination,
on_conflict,
dereference,
preserve_directory_permissions?,
[]
) do
{:error, _, _} = error -> error
res -> {:ok, res}
end
@@ -1241,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
@@ -1258,7 +1275,7 @@ defmodule File do
end
end
defp do_cp_r(src, dest, on_conflict, dereference, acc) when is_list(acc) do
defp do_cp_r(src, dest, on_conflict, dereference, preserve_dir_perms?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do
{:ok, :regular} ->
case do_cp_file(src, dest, on_conflict, acc) do
@@ -1278,7 +1295,7 @@ defmodule File do
{:error, :eloop, src}
else
dereference = MapSet.put(dereference, resolved)
do_cp_r(resolved, dest, on_conflict, dereference, acc)
do_cp_r(resolved, dest, on_conflict, dereference, preserve_dir_perms?, acc)
end
{:ok, link} ->
@@ -1293,23 +1310,34 @@ defmodule File do
{:ok, files} ->
case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] ->
case copy_file_mode(src, dest) do
:ok ->
Enum.reduce_while(files, [dest | acc], fn x, acc ->
case do_cp_r(
Path.join(src, x),
Path.join(dest, x),
on_conflict,
dereference,
acc
) do
{:error, _, _} = error -> {:halt, error}
acc -> {:cont, acc}
end
end)
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
{:error, reason} ->
{:error, reason, src}
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} ->
@@ -1329,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
+133 -101
View File
@@ -344,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
@@ -366,135 +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 -> -0.0
:half_up when sign === 1 -> -0.0
_ -> 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 ->
-0.0
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
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.
+6 -3
View File
@@ -567,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}
+5
View File
@@ -669,6 +669,11 @@ defimpl Inspect, for: Any do
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
end
# A temporary clause to deal with native records until they are officially supported
def inspect(native_record, _opts) do
:io_lib.format("~p", [native_record]) |> IO.iodata_to_binary()
end
def inspect_as_struct(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
+3 -3
View File
@@ -46,8 +46,8 @@ defmodule Inspect.Opts do
* `:limit` - limits the number of items that are inspected for tuples,
bitstrings, maps, lists and any other collection of items, with the exception of
printable strings and printable charlists which use the `:printable_limit` option.
It accepts a positive integer or `:infinity`. It defaults to `100` since
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
It accepts a positive integer or `:infinity`. It defaults to `200` since
`Elixir v1.20.0`, as it has better defaults to deal with nested collections.
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
@@ -90,7 +90,7 @@ defmodule Inspect.Opts do
charlists: :infer,
custom_options: [],
inspect_fun: &Inspect.inspect/2,
limit: 100,
limit: 200,
pretty: false,
printable_limit: 4096,
safe: true,
+42 -33
View File
@@ -2752,7 +2752,7 @@ defmodule Kernel do
nil ->
quote do
case unquote(term) do
%_{__exception__: true} -> true
%_{__exception__: _} -> true
_ -> false
end
end
@@ -2764,8 +2764,7 @@ defmodule Kernel do
quote do
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -2792,7 +2791,7 @@ defmodule Kernel do
case unquote(name) do
name when is_atom(name) ->
case unquote(term) do
%{__struct__: ^name, __exception__: true} -> true
%{__struct__: ^name, __exception__: _} -> true
_ -> false
end
@@ -2810,8 +2809,7 @@ defmodule Kernel do
(is_atom(unquote(name)) or :fail) and
:erlang.is_map_key(:__struct__, unquote(term)) and
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
:erlang.is_map_key(:__exception__, unquote(term)) and
:erlang.map_get(:__exception__, unquote(term)) == true
:erlang.is_map_key(:__exception__, unquote(term))
end
end
end
@@ -4725,7 +4723,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)
@@ -5875,11 +5873,15 @@ defmodule Kernel do
end
@doc """
Defines a macro suitable for use in guard expressions.
Defines a custom guard with the given name.
It raises at compile time if the `guard` uses expressions that aren't
allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a macro that can be used both inside or outside guards.
Once defined, custom guards can be invoked within regular code or in
guards. The module that contains the custom guard must be required before usage.
Custom guards are defined by providing a valid guard expression to
the right-hand side of `when`. `defguard` will then expand and validate
the expressions as guards. `defguard` will raise at compile time if the
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
@@ -5887,31 +5889,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()
@@ -6423,12 +6424,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
+4 -5
View File
@@ -25,9 +25,8 @@ defmodule Kernel.ParallelCompiler do
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
{:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
each_cycle: (-> {:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(),
verification: boolean(),
@@ -340,7 +339,7 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(schedulers, checker, files, output, options) do
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
timer_ref = Process.send_after(self(), :threshold_check, threshold)
timer_ref = :erlang.send_after(threshold, self(), :threshold_check)
purge_compiler_modules =
if Keyword.get(options, :purge_compiler_modules, false) do
@@ -841,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)
+1 -2
View File
@@ -610,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
)
+11 -3
View File
@@ -266,7 +266,7 @@ defmodule Kernel.Utils do
module.exception([])
end
def raise(%_{__exception__: true} = exception) do
def raise(%_{__exception__: _} = exception) do
exception
end
@@ -339,7 +339,7 @@ defmodule Kernel.Utils do
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
end
end
end
@@ -369,7 +369,9 @@ 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} = env) do
env = %{env | context: nil}
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -391,6 +393,12 @@ defmodule Kernel.Utils do
{var, acc}
end
{{:., dot_meta, [:erlang, :orelse]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :or]}, meta, [left, right]}, env), acc}
{{:., dot_meta, [:erlang, :andalso]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :and]}, meta, [left, right]}, env), acc}
node, acc ->
{node, acc}
end)
+10 -10
View File
@@ -270,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
@@ -965,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])
+2 -2
View File
@@ -973,7 +973,7 @@ defmodule Macro do
def struct_info!(module, env) when is_atom(module) do
meta = [line: env.line]
case :elixir_map.maybe_load_struct_info(meta, module, env) do
case :elixir_map.maybe_load_struct_info(meta, module, :hard, env) do
{:ok, info} ->
:elixir_env.trace({:struct_expansion, meta, module, []}, env)
info
@@ -2196,7 +2196,7 @@ defmodule Macro do
Please check `expand_literals/2` for use cases and pitfalls.
"""
@doc since: "1.14.1"
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: {t(), acc} when acc: term()
def expand_literals(ast, acc, fun)
def expand_literals({:__aliases__, meta, args}, acc, fun) do
+2 -1
View File
@@ -367,7 +367,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
@spec define_require(t, Macro.metadata(), module, define_require_opts) ::
{:ok, t} | {:error, String.t()}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
+11 -4
View File
@@ -1109,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})
@@ -1138,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
@@ -1172,6 +1172,13 @@ defmodule Map do
See also `filter/2`.
> #### Performance considerations {: .tip}
>
> If you find yourself doing multiple calls to `Map.filter/2`
> and/or `Map.reject/2` in a pipeline, it is likely more efficient
> to use `Enum.filter/2` and `Enum.reject/2` instead and convert to
> a map at the end using `Map.new/1` or `Map.new/2`.
## Examples
iex> Map.reject(%{one: 1, two: 2, three: 3}, fn {_key, val} -> rem(val, 2) == 1 end)
+2 -1
View File
@@ -57,7 +57,8 @@ defmodule MapSet do
# We don't use @opaque (or `:sets.set` which is opaque) because MapSets can be inlined,
# either via module attributes or by the compiler.
@typep internal(value) :: %{optional(value) => term()}
# Defaulting to a broad `term()` type to prevent opaqueness violations.
@typep internal(_value) :: term()
@type t(value) :: %__MODULE__{map: internal(value)}
@type t :: t(term)
+1 -1
View File
@@ -654,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
+230 -32
View File
@@ -4,7 +4,7 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
alias Module.Types.{Apply, Descr, Expr, Helpers, Pattern}
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
@@ -86,7 +86,7 @@ defmodule Module.Types do
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], private, context}
{[{fun_arity, group_clauses_by_return(inferred)} | types], private, context}
else
{types, private, context}
end
@@ -251,17 +251,27 @@ defmodule Module.Types do
context ->
{_kind, info, mapping} = Map.fetch!(context.local_sigs, fun_arity)
clauses_indexes =
for type_index <- pending,
not skip_unused_clause?(info, type_index),
{clause_index, ^type_index} <- mapping,
do: clause_index
if pending != [] do
{used_indexes, unused_indexes} =
Enum.reduce(mapping, {[], []}, fn {clause_index, type_index},
{used_indexes, unused_indexes} ->
if type_index in pending and not skip_unused_clause?(info, type_index) do
{used_indexes, [clause_index | unused_indexes]}
else
{[clause_index | used_indexes], unused_indexes}
end
end)
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
unused_indexes = Enum.uniq(unused_indexes) -- used_indexes
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
else
context
end
end
end
@@ -321,28 +331,106 @@ defmodule Module.Types do
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, Pattern.init_previous(), [], [], context}, fn
{meta, args, guards, body}, {index, total, previous, mapping, inferred, acc_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, previous, context} =
{trees, _precise?, head_no_previous_args_types, previous, head_context} =
Pattern.of_head(args, guards, expected, previous, info, meta, stack, fresh_context)
{return_type, context} =
Expr.of_expr(body, Descr.term(), body, stack, context)
Expr.of_expr(body, Descr.term(), body, stack, head_context)
args_types = Pattern.of_domain(trees, stack, context)
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
domain =
case domain do
[] ->
args_types
_ ->
head_args_types = Pattern.of_domain(trees, stack, head_context)
compute_domain(args_types, head_args_types, head_no_previous_args_types, domain)
end
if type_index == -1 do
{index + 1, total + 1, previous, [{index, total} | mapping], inferred, context}
mapping = [{index, total} | mapping]
{index + 1, total + 1, previous, domain, mapping, inferred, context}
else
{index + 1, total, previous, [{index, type_index} | mapping], inferred, context}
mapping = [{index, type_index} | mapping]
{index + 1, total, previous, domain, mapping, inferred, context}
end
rescue
e ->
@@ -352,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),
@@ -376,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}
@@ -437,7 +627,9 @@ defmodule Module.Types do
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
local_handler: handler,
# Reverse arrow handling (nil | :cache | :use)
reverse_arrow: nil
}
end
@@ -459,20 +651,26 @@ defmodule Module.Types do
# 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
+390 -94
View File
@@ -236,24 +236,39 @@ defmodule Module.Types.Apply do
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
## Kernel
{Kernel, :elem, [{[open_tuple([]), integer()], dynamic()}]},
{Kernel, :is_map_key, [{[open_map(), term()], boolean()}]},
{Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
## Lists
{:lists, :member, [{[term(), list(term())], boolean()}]},
## Map
{Map, :delete, [{[open_map(), term()], open_map()}]},
{Map, :fetch,
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
{Map, :fetch!, [{[open_map(), term()], term()}]},
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
{Map, :get, [{[open_map(), term()], term()}]},
{Map, :get, [{[open_map(), term(), term()], term()}]},
{Map, :get_lazy, [{[open_map(), term(), fun(0)], term()}]},
{Map, :has_key?, [{[open_map(), term()], boolean()}]},
{Map, :pop, [{[open_map(), term()], tuple([term(), open_map()])}]},
{Map, :pop, [{[open_map(), term(), term()], tuple([term(), open_map()])}]},
{Map, :pop!, [{[open_map(), term()], tuple([term(), open_map()])}]},
{Map, :pop_lazy, [{[open_map(), term(), fun(0)], tuple([term(), open_map()])}]},
{Map, :put, [{[open_map(), term(), term()], open_map()}]},
{Map, :put_new, [{[open_map(), term(), term()], open_map()}]},
{Map, :put_new_lazy, [{[open_map(), term(), fun(0)], open_map()}]},
{Map, :replace, [{[open_map(), term(), term()], open_map()}]},
{Map, :replace!, [{[open_map(), term(), term()], open_map()}]},
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
{Map, :update, [{[open_map(), term(), term(), fun(1)], open_map()}]},
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
{Tuple, :delete_at, [{[open_tuple([]), integer()], dynamic(open_tuple([]))}]},
{Tuple, :duplicate, [{[term(), integer()], tuple()}]},
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
{:maps, :find,
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
@@ -270,8 +285,6 @@ defmodule Module.Types.Apply do
] do
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
true = Code.ensure_loaded?(mod)
domain_clauses =
case clauses do
[_] ->
@@ -393,6 +406,7 @@ defmodule Module.Types.Apply do
case sized_order(name, left, right, expected) do
{arg, expected, precise?, return} ->
{actual, context} = of_fun.(arg, expected, expr, stack, context)
# expected is a static type, so we can use subtype? rather than compatible?
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
{result, context}
@@ -439,15 +453,19 @@ defmodule Module.Types.Apply do
tuple_type = open_tuple(List.duplicate(term(), max(index - 1, 0)) ++ [expected])
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
case tuple_fetch(tuple_type, index - 1) do
{_optional?, value_type} ->
{return(value_type, [tuple_type], stack), context}
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :element, 2, expr, stack, context)
else
case tuple_fetch(tuple_type, index - 1) do
{_optional?, value_type} ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(:erlang, :element, [integer(), tuple_type], expr, stack, context)
:badtuple ->
remote_error(:erlang, :element, [integer(), tuple_type], expr, stack, context)
:badindex ->
remote_error({:badindex, index, tuple_type}, :erlang, :element, 2, expr, stack, context)
:badindex ->
remote_error({:badindex, index, tuple_type}, :erlang, :element, 2, expr, stack, context)
end
end
end
@@ -464,17 +482,46 @@ defmodule Module.Types.Apply do
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
case tuple_insert_at(tuple_type, index - 1, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :insert_element, 3, expr, stack, context)
else
case tuple_insert_at(tuple_type, index - 1, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [integer(), tuple_type, elem_type]
remote_error(:erlang, :insert_element, args_types, expr, stack, context)
:badtuple ->
args_types = [integer(), tuple_type, elem_type]
remote_error(:erlang, :insert_element, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index - 1, tuple_type}
remote_error(error, :erlang, :insert_element, 3, expr, stack, context)
:badindex ->
error = {:badindex, index - 1, tuple_type}
remote_error(error, :erlang, :insert_element, 3, expr, stack, context)
end
end
end
defp do_remote(:erlang, :setelement, [index, tuple, elem], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :setelement, 3, expr, stack, context)
else
case tuple_replace_at(tuple_type, index - 1, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [integer(), tuple_type, elem_type]
remote_error(:erlang, :setelement, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, :erlang, :setelement, 3, expr, stack, context)
end
end
end
@@ -483,64 +530,172 @@ defmodule Module.Types.Apply do
tuple_type = open_tuple(List.duplicate(term(), max(index, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
case tuple_delete_at(tuple_type, index - 1) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type], stack), context}
if index < 1 do
remote_error({:negindex, index - 1}, :erlang, :delete_element, 2, expr, stack, context)
else
case tuple_delete_at(tuple_type, index - 1) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(:erlang, :delete_element, [integer(), tuple_type], expr, stack, context)
:badtuple ->
remote_error(:erlang, :delete_element, [integer(), tuple_type], expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, :erlang, :delete_element, 2, expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, :erlang, :delete_element, 2, expr, stack, context)
end
end
end
defp do_remote(Kernel, :elem, [tuple, index], expected, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index, 0)) ++ [expected])
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Kernel, :elem, 2, expr, stack, context)
else
case tuple_fetch(tuple_type, index) do
{_optional?, value_type} ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(Kernel, :elem, [tuple_type, integer()], expr, stack, context)
:badindex ->
remote_error({:badindex, index + 1, tuple_type}, Kernel, :elem, 2, expr, stack, context)
end
end
end
defp do_remote(Kernel, :put_elem, [tuple, index, elem], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index + 1, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Kernel, :put_elem, 3, expr, stack, context)
else
case tuple_replace_at(tuple_type, index, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [tuple_type, integer(), elem_type]
remote_error(Kernel, :put_elem, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index + 1, tuple_type}
remote_error(error, Kernel, :put_elem, 3, expr, stack, context)
end
end
end
defp do_remote(Tuple, :duplicate, [elem, size], _, expr, stack, context, of_fun)
when is_integer(size) and size >= 0 do
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
{return(tuple(List.duplicate(elem_type, size)), [elem_type], stack), context}
end
defp do_remote(Tuple, :insert_at, [tuple, index, elem], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index, 0)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
{elem_type, context} = of_fun.(elem, term(), expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Tuple, :insert_at, 3, expr, stack, context)
else
case tuple_insert_at(tuple_type, index, elem_type) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type, elem_type], stack), context}
:badtuple ->
args_types = [tuple_type, integer(), elem_type]
remote_error(Tuple, :insert_at, args_types, expr, stack, context)
:badindex ->
error = {:badindex, index, tuple_type}
remote_error(error, Tuple, :insert_at, 3, expr, stack, context)
end
end
end
defp do_remote(Tuple, :delete_at, [tuple, index], _, expr, stack, context, of_fun)
when is_integer(index) do
tuple_type = open_tuple(List.duplicate(term(), max(index + 1, 1)))
{tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context)
if index < 0 do
remote_error({:negindex, index}, Tuple, :delete_at, 2, expr, stack, context)
else
case tuple_delete_at(tuple_type, index) do
value_type when is_descr(value_type) ->
{return(value_type, [tuple_type], stack), context}
:badtuple ->
remote_error(Tuple, :delete_at, [tuple_type, integer()], expr, stack, context)
:badindex ->
error = {:badindex, index + 1, tuple_type}
remote_error(error, Tuple, :delete_at, 2, expr, stack, context)
end
end
end
defp do_remote(:lists, :member, [arg, list] = args, expected, expr, stack, context, of_fun)
when is_list(list) and list != [] do
case booleaness(expected) do
{polarity, _maybe_or_always} ->
{return, acc} =
case polarity do
true -> {@atom_true, none()}
false -> {@atom_false, term()}
when is_list(list) do
if list == [] or Enum.any?(list, &match?({:|, _, [_, _]}, &1)) do
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
else
case booleaness(expected) do
{polarity, _maybe_or_always} ->
{return, acc} =
case polarity do
true -> {@atom_true, none()}
false -> {@atom_false, term()}
end
{expected, singleton?, context} =
Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} ->
{type, context} = of_fun.(literal, term(), expr, stack, context)
if singleton?(type) do
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
{acc, all_singleton?, context}
else
acc = if polarity, do: union(acc, type), else: acc
{acc, false, context}
end
end)
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
cond do
# expected can have dynamic terms but, since we have verified
# them to be singletons, we can compute the upper bound.
singleton? and subtype?(arg_type, upper_bound(expected)) ->
{return, context}
# Singleton types with reverse polarity are negated, so we don't check for disjoint
(singleton? and not polarity) or not is_warning(stack) ->
{return(boolean(), [arg_type, expected], stack), context}
# Nothing in common between left and right, emit a warning
disjoint?(arg_type, expected) ->
error = {:mismatched_comparison, arg_type, list(expected)}
remote_error(error, :lists, :member, 2, expr, stack, context)
true ->
{return(boolean(), [arg_type, expected], stack), context}
end
{expected, singleton?, context} =
Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} ->
{type, context} = of_fun.(literal, term(), expr, stack, context)
if singleton?(type) do
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
{acc, all_singleton?, context}
else
acc = if polarity, do: union(acc, type), else: acc
{acc, false, context}
end
end)
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
cond do
# Return a precise result
singleton? and subtype?(arg_type, expected) ->
{return(return, [arg_type, expected], stack), context}
# Singleton types with reverse polarity are negated, so we don't check for disjoint
(singleton? and not polarity) or not is_warning(stack) ->
{return(boolean(), [arg_type, expected], stack), context}
# Nothing in common between left and right, emit a warning
disjoint?(arg_type, expected) ->
error = {:mismatched_comparison, arg_type, list(expected)}
remote_error(error, :lists, :member, 2, expr, stack, context)
true ->
{return(boolean(), [arg_type, expected], stack), context}
end
_ ->
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
_ ->
remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context)
end
end
end
@@ -584,17 +739,29 @@ defmodule Module.Types.Apply do
{expected, precise?} =
case fun do
:length when :erlang.xor(polarity, literal > 0) ->
{@empty_list, literal == 0}
:length when polarity and literal == 0 ->
{@empty_list, true}
:length when not polarity and literal == 0 ->
{@non_empty_list, true}
:length when polarity ->
{@non_empty_list, false}
:length ->
{@non_empty_list, literal == 0}
{@list, false}
:map_size when :erlang.xor(polarity, literal > 0) ->
{@empty_map, literal == 0}
:map_size when polarity and literal == 0 ->
{@empty_map, true}
:map_size when not polarity and literal == 0 ->
{@non_empty_map, true}
:map_size when polarity ->
{@non_empty_map, false}
:map_size ->
{@non_empty_map, literal == 0}
{open_map(), false}
:tuple_size when polarity ->
{tuple(List.duplicate(term(), literal)), true}
@@ -604,6 +771,8 @@ defmodule Module.Types.Apply do
end
{actual, context} = of_fun.(arg, expected, expr, stack, context)
# expected here is a static type, so subtyping relation is fine
result = if precise? and subtype?(actual, expected), do: return, else: boolean()
# We can skip return compare because literal is always an integer,
@@ -631,7 +800,7 @@ defmodule Module.Types.Apply do
if singleton?(type) do
expected = if polarity, do: type, else: negation(type)
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
result = if subtype?(arg_type, expected), do: return, else: boolean()
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
# Because reverse polarity means we will infer negated types
# (which are naturally disjoint), we skip checks in such cases
@@ -688,17 +857,16 @@ defmodule Module.Types.Apply do
defp mismatched_ordered_comparison(left_type, right_type, stack) do
if is_warning(stack) do
common = intersection(left_type, right_type)
cond do
# This check is incomplete. After all, we could have the number type nested
# inside a tuple or a list and the comparison would still be valid.
# However, nested comparison between distinct numbers is very uncommon,
# so we only check the direct value here.
empty?(common) and not (number_type?(left_type) and number_type?(right_type)) ->
# These checks are incomplete. After all, we could have numbers and
# structs nested inside tuples or lists, but we only check the direct
# value here.
not (number_type?(left_type) and number_type?(right_type)) and
disjoint?(left_type, right_type) ->
{:mismatched_comparison, left_type, right_type}
match?({false, _}, map_fetch_key(dynamic(common), :__struct__)) ->
match?({false, _}, map_fetch_key(dynamic(left_type), :__struct__)) and
match?({false, _}, map_fetch_key(dynamic(right_type), :__struct__)) ->
{:struct_comparison, left_type, right_type}
true ->
@@ -718,8 +886,8 @@ defmodule Module.Types.Apply do
{size, {{:., _, [:erlang, fun]}, _, [arg]}} when is_data_size(fun, size) ->
case booleaness(expected) do
{true, _} -> sized_order(invert_order(name), fun, size, arg, @atom_true)
{false, _} -> sized_order(name, fun, size, arg, @atom_false)
{true, _} -> sized_order(mirror_order(name), fun, size, arg, @atom_true)
{false, _} -> sized_order(invert_order(mirror_order(name)), fun, size, arg, @atom_false)
_ -> :none
end
@@ -771,6 +939,11 @@ defmodule Module.Types.Apply do
defp invert_order(:>), do: :"=<"
defp invert_order(:<), do: :>=
defp mirror_order(:>=), do: :"=<"
defp mirror_order(:"=<"), do: :>=
defp mirror_order(:>), do: :<
defp mirror_order(:<), do: :>
@doc """
Returns the domain of an unknown module.
@@ -813,17 +986,40 @@ defmodule Module.Types.Apply do
{info, filter_domain(info, expected, 2), context}
end
def remote_domain(Kernel, :is_map_key, [_map, key], expected, _meta, _stack, context)
when is_atom(key) do
info =
{:strong, [open_map(), term()],
[
{[open_map([{key, term()}]), term()], atom([true])},
{[open_map([{key, not_set()}]), term()], atom([false])}
]}
{info, filter_domain(info, expected, 2), context}
end
def remote_domain(:erlang, :map_get, [key, _], expected, _meta, _stack, context)
when is_atom(key) do
domain = [term(), open_map([{key, expected}])]
{{:strong, nil, [{domain, term()}]}, domain, context}
end
def remote_domain(Map, :fetch!, [_, key], expected, _meta, _stack, context) when is_atom(key) do
domain = [open_map([{key, expected}]), term()]
{{:strong, nil, [{domain, term()}]}, domain, context}
end
def remote_domain(:maps, :get, [key, _], expected, _meta, _stack, context) when is_atom(key) do
domain = [term(), open_map([{key, expected}])]
{{:strong, nil, [{domain, term()}]}, domain, context}
end
def remote_domain(Map, :replace!, [_, key, _], _expected, _meta, _stack, context)
when is_atom(key) do
domain = [open_map([{key, term()}]), term(), term()]
{{:strong, nil, [{domain, open_map()}]}, domain, context}
end
def remote_domain(:maps, :update, [key, _, _], _expected, _meta, _stack, context)
when is_atom(key) do
domain = [term(), term(), open_map([{key, term()}])]
@@ -903,6 +1099,36 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(Map, :delete, _info, [map, key] = args_types, stack) do
case map_update(map, key, not_set(), false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :delete, args_types)}
{:error, _errors} -> {:ok, map}
end
end
defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{_, value} ->
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
{:ok, return(result, args_types, stack)}
:badmap ->
{:error, badremote(Map, :fetch, args_types)}
:error ->
{:error, {:badkeydomain, map, key, atom([:error])}}
end
end
defp remote_apply(Map, :fetch!, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{:ok, value} -> {:ok, return(value, args_types, stack)}
:badmap -> {:error, badremote(Map, :fetch!, args_types)}
:error -> {:error, {:badkeydomain, map, key, "raise"}}
end
end
defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do
case map_get(map, key) do
{:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)}
@@ -984,6 +1210,14 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(Map, :put, _info, [map, key, value] = args_types, stack) do
case map_update(map, key, value, false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :put, args_types)}
{:error, _errors} -> {:ok, map}
end
end
defp remote_apply(Map, :pop!, _info, [map, key] = args_types, stack) do
case map_update(map, key, not_set(), true, false) do
{value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)}
@@ -1002,6 +1236,16 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(Map, :replace!, _info, [map, key, value] = args_types, stack) do
fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end
case map_update_fun(map, key, fun, false, false) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :replace!, args_types)}
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
end
end
defp remote_apply(Map, :replace_lazy, _info, args_types, stack) do
map_update_or_replace_lazy(:replace_lazy, args_types, stack, "do nothing")
end
@@ -1350,33 +1594,62 @@ defmodule Module.Types.Apply do
## Local
def local(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
{local_info, domain, context} = local_domain(fun, args, expected, meta, stack, context)
{updated_used?, info, domain, context} =
local_domain(fun, args, expected, meta, stack, context)
{args_types, context} =
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
local_apply(local_info, fun, args_types, expr, stack, context)
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
{_indexes, type, context} -> {type, context}
{type, context} -> {type, context}
end
end
def local_arrows(fun, args, expected, {_, meta, _} = expr, stack, context, of_fun) do
{updated_used?, info, domain, context} =
local_domain(fun, args, expected, meta, stack, context)
{args_types, context} =
zip_map_reduce(args, domain, context, &of_fun.(&1, &2, expr, stack, &3))
case local_apply(updated_used?, info, fun, args_types, expr, stack, context) do
{indexes, _type, context} -> {indexes_to_arrows(indexes, info), context}
{type, context} -> {[{Enum.map(args, fn _ -> type end), type}], context}
end
end
defp indexes_to_arrows(indexes, {_, _domain, clauses}),
do: indexes_to_arrows(Enum.reverse(indexes), clauses, 0)
defp indexes_to_arrows([index | indexes], [clause | clauses], index),
do: [clause | indexes_to_arrows(indexes, clauses, index + 1)]
defp indexes_to_arrows([_ | _] = indexes, [_clause | clauses], index),
do: indexes_to_arrows(indexes, clauses, index + 1)
defp indexes_to_arrows([], _clauses, _index),
do: []
defp local_domain(fun, args, expected, meta, stack, context) do
arity = length(args)
case stack.local_handler.(meta, {fun, arity}, stack, context) do
false ->
{{false, :none}, List.duplicate(term(), arity), context}
{false, :none, List.duplicate(term(), arity), context}
{kind, info, context} ->
update_used? = is_warning(stack) and kind == :defp
if info == :none do
{{update_used?, :none}, List.duplicate(term(), arity), context}
{update_used?, :none, List.duplicate(term(), arity), context}
else
{{update_used?, info}, filter_domain(info, expected, arity), context}
{update_used?, info, filter_domain(info, expected, arity), context}
end
end
end
defp local_apply({update_used?, :none}, fun, args_types, _expr, _stack, context) do
defp local_apply(update_used?, :none, fun, args_types, _expr, _stack, context) do
if update_used? do
{dynamic(), put_in(context.local_used[{fun, length(args_types)}], [])}
else
@@ -1384,7 +1657,7 @@ defmodule Module.Types.Apply do
end
end
defp local_apply({update_used?, info}, fun, args_types, expr, stack, context) do
defp local_apply(update_used?, info, fun, args_types, expr, stack, context) do
case local_apply(info, args_types, stack) do
{indexes, type} ->
context =
@@ -1396,7 +1669,7 @@ defmodule Module.Types.Apply do
context
end
{type, context}
{indexes, type, context}
:error ->
error = {:badlocal, info, args_types, expr, context}
@@ -1619,7 +1892,7 @@ defmodule Module.Types.Apply do
{message, to_trace, hints} =
case reason do
{:badarg, domain} ->
{:badarg, domain, _empty?} ->
message = """
incompatible types given on #{mfa_or_call}:
@@ -1754,6 +2027,29 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({{:negindex, index}, mfac, expr, context}) do
traces = collect_traces(expr, context)
{mod, fun, arity, _converter} = mfac
mfa = Exception.format_mfa(mod, fun, arity)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a non-negative integer as index in #{mfa}:
#{expr_to_string(expr) |> indent(4)}
got the index:
#{index}
""",
format_traces(traces)
])
}
end
def format_diagnostic({{:badkeydomain, map, key, error}, mfac, expr, context}) do
{mod, fun, arity, _converter} = mfac
mfa = Exception.format_mfa(mod, fun, arity)
File diff suppressed because it is too large Load Diff
+456 -210
View File
@@ -15,7 +15,6 @@ defmodule Module.Types.Expr do
list_of_modules = list(atom())
@try_catch atom([:error, :exit, :throw])
@atom_true atom([true])
@caller closed_map(
__struct__: atom([Macro.Env]),
@@ -35,13 +34,9 @@ defmodule Module.Types.Expr do
versioned_vars: open_map()
)
# An annotation for terms where the reverse arrow is not yet fully defined.
# Also revisit all users of dynamic() in this module in a later date.
@pending term()
# We do not make exception dynamic on purpose. If you do a blank rescue,
# then we will assume you need to statically handle all possible exceptions.
@exception open_map(__struct__: atom(), __exception__: @atom_true)
@exception open_map(__struct__: atom(), __exception__: term())
args_or_arity = union(list(term()), integer())
@@ -59,6 +54,9 @@ defmodule Module.Types.Expr do
)
)
@falsy atom([false, nil])
@truthy negation(atom([false, nil]))
# :atom
def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom),
do: {atom([atom]), context}
@@ -139,7 +137,7 @@ defmodule Module.Types.Expr do
end
# left = right
def of_expr({:=, _, [left_expr, right_expr]} = match, expected, expr, stack, context) do
def of_expr({:=, meta, [left_expr, right_expr]} = match, expected, expr, stack, context) do
{left_expr, right_expr} = repack_match(left_expr, right_expr)
case left_expr do
@@ -148,14 +146,20 @@ defmodule Module.Types.Expr do
of_expr(right_expr, expected, expr, stack, context)
_ ->
type_fun = fn pattern_type, context ->
# See if we can use the expected type to further refine the pattern type,
# if we cannot, use the pattern type as that will fail later on.
{_ok_or_error, type} = compatible_intersection(dynamic(pattern_type), expected)
of_expr(right_expr, type, expr, stack, context)
end
type_fun =
fn pattern_type, context ->
# See if we can use the expected type to further refine the pattern type,
# if we cannot, use the pattern type as that will fail later on.
type = intersection(pattern_type, expected)
type = if empty?(type), do: pattern_type, else: type
{result, context} = of_expr(right_expr, type, expr, stack, context)
Pattern.of_match(left_expr, type_fun, match, stack, context)
# The function may still return a too broad type,
# so we refine once again to assign the most appropriate to the pattern.
{intersection(result, expected), context}
end
Pattern.of_match(left_expr, type_fun, match, meta, stack, context)
end
end
@@ -282,38 +286,151 @@ defmodule Module.Types.Expr do
of_expr(post, expected, post, stack, context)
end
def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected, expr, stack, original) do
clauses
|> reduce_non_empty({none(), original}, fn
{:->, meta, [[head], body]}, {acc, context}, last? ->
{head_type, context} = of_expr(head, @pending, head, stack, context)
def of_expr(
{:cond, meta,
[
[
{:do,
[
{:->, pos_meta, [[pos_head], pos_body]},
{:->, _neg_meta, [[neg_head], neg_body]}
]}
]
]},
expected,
expr,
stack,
acc_context
)
when is_atom(neg_head) and neg_head not in [false, nil] do
cache_result(meta, stack, acc_context, fn ->
{pos_head_type, context} =
of_expr(pos_head, term(), pos_head, %{stack | reverse_arrow: :cache}, acc_context)
context =
if is_warning(stack) do
case truthiness(head_type) do
:always_true when not last? ->
warning = {:badcond, "always match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
context =
maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false)
:always_false ->
warning = {:badcond, "never match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
{_, truthy_context} =
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :use}, context)
_ ->
context
end
else
context
end
# Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context)
{body_type, context} = of_expr(body, expected, expr, stack, context)
{union(body_type, acc), Of.reset_vars(context, original)}
{pos_body_type, pos_body_context} =
of_expr(pos_body, expected, expr, stack, truthy_context)
# Reset the context vars once again to compute the falsy type
context = Of.reset_vars(pos_body_context, context)
{_, falsy_context} =
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :use}, context)
falsy_context = reset_warnings(falsy_context, context)
{neg_body_type, neg_body_context} =
of_expr(neg_body, expected, expr, stack, falsy_context)
body_type = union(pos_body_type, neg_body_type)
context =
cond do
empty?(pos_body_type) -> Of.reset_vars(neg_body_context, falsy_context)
empty?(neg_body_type) -> Of.reset_vars(neg_body_context, truthy_context)
true -> Of.reset_vars(neg_body_context, acc_context)
end
dynamic_unless_static({body_type, context}, stack)
end)
|> dynamic_unless_static(stack)
end
def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, _expr, stack, context) do
{case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context)
def of_expr({:cond, meta, [[{:do, clauses}]]}, expected, expr, stack, context) do
cache_result(meta, stack, context, fn ->
{body_type, acc_context} =
reduce_non_empty(clauses, {none(), context}, fn
{:->, meta, [[head], body]}, {acc, context}, last? ->
{head_type, context} =
of_expr(head, term(), head, %{stack | reverse_arrow: :cache}, context)
context =
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
{_, truthy_context} =
of_expr(head, @truthy, head, %{stack | reverse_arrow: :use}, context)
# Keep the context except the warnings, and compute the body
truthy_context = reset_warnings(truthy_context, context)
{body_type, body_context} = of_expr(body, expected, expr, stack, truthy_context)
# Reset the context vars to the head definition to compute the falsy type
context = Of.reset_vars(body_context, context)
context =
if last? do
context
else
{_, falsy_context} =
of_expr(head, @falsy, head, %{stack | reverse_arrow: :use}, context)
reset_warnings(falsy_context, context)
end
{union(body_type, acc), context}
end)
dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack)
end)
end
def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context)
when stack.reverse_arrow == :use do
version = Keyword.fetch!(meta, :version)
clauses = Map.fetch!(context.reverse_arrows, version)
original = context
context =
clauses
|> Enum.reduce({0, []}, fn {_arg_type, body_type, _clause} = triplet, {counter, acc} ->
if disjoint?(body_type, expected) do
{counter + 1, acc}
else
{counter, [triplet | acc]}
end
end)
|> case do
# Nothing skipped, just return the context as is
{0, _} ->
context
# If there is a single clause, we assume it is always evaluated
# by doing reverse arrows and incorporting all variables into the context.
# We have to evaluate the head again but it might have been preferred
# if we could somehow merge a previously computed context.
{_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} ->
{case_type, context} = of_expr(case_expr, arg_type, case_expr, stack, context)
{patterns, guards} = extract_head(head)
previous = Pattern.init_previous()
info = {{:case, meta, case_expr, case_type}, head}
{_, _, _, _, context} =
Pattern.of_head(patterns, guards, [case_type], previous, info, meta, stack, context)
{_, context} = of_expr(body, expected, body, stack, context)
reset_warnings(context, original)
{_, filtered} ->
case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2))
{_, context} = of_expr(case_expr, case_expected, case_expr, stack, context)
reset_warnings(context, original)
end
dynamic_unless_static({expected, context}, stack)
end
def of_expr({:case, meta, [case_expr, [do: clauses]]}, expected, _expr, stack, base_context) do
{case_type, context} =
of_expr(case_expr, term(), case_expr, %{stack | reverse_arrow: :cache}, base_context)
info = {:case, meta, case_expr, case_type}
added_meta =
@@ -326,146 +443,227 @@ defmodule Module.Types.Expr do
# If the expression is generated or the construct is a literal,
# it is most likely a macro code. However, if no clause is matched,
# we should still check for that.
if added_meta != [] do
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
else
clauses
end
|> of_clauses([case_type], expected, info, stack, context, none())
|> dynamic_unless_static(stack)
end
# fn pat -> expr end
def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do
[{:->, _, [head, _]} | _] = clauses
{patterns, _guards} = extract_head(head)
domain = Enum.map(patterns, fn _ -> dynamic() end)
{acc, context} =
of_clauses_fun(clauses, domain, @pending, :fn, stack, context, [], fn
trees, body, context, acc ->
args_types = Pattern.of_domain(trees, stack, context)
add_inferred(acc, args_types, body)
end)
{fun_from_inferred_clauses(acc), context}
end
def of_expr({:try, meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
{after_block, blocks} = Keyword.pop(blocks, :after)
{else_block, blocks} = Keyword.pop(blocks, :else)
{type, context} =
if else_block do
{type, context} = of_expr(body, @pending, body, stack, original)
info = {:try_else, meta, body, type}
of_clauses(else_block, [type], expected, info, stack, context, none())
clauses =
if added_meta != [] do
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
else
of_expr(body, expected, expr, stack, original)
clauses
end
{type, context} =
blocks
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
{:rescue, clauses}, acc_context ->
Enum.reduce(clauses, acc_context, fn
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
{type, context} =
of_rescue(var, exceptions, body, expr, :rescue, meta, stack, context)
cache_arrows(meta, stack, fn ->
acc = {false, none(), []}
{union(type, acc), context}
{{none?, body_acc, clauses_acc}, context} =
of_clauses_fun(clauses, [case_type], info, stack, context, acc, fn
trees, precise?, {:->, _, [_, body]} = clause, context, acc ->
# Compute the arg type based on the clause itself
[arg_type] = Pattern.of_domain(trees, stack, context)
{:->, meta, [[var], body]}, {acc, context} ->
{type, context} =
of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context)
# Now we refine the case_expr context and use it to compute the body
{_, refined_context} =
of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context)
{union(type, acc), context}
end)
{body_type, context} =
of_expr(body, expected, body, stack, reset_warnings(refined_context, context))
{:catch, clauses}, {acc, context} ->
args = [@try_catch, dynamic()]
of_clauses(clauses, args, expected, :try_catch, stack, context, acc)
end)
|> dynamic_unless_static(stack)
# Now we compute the return type and the clauses for reverse arrow
{none?, body_acc, clauses_acc} = acc
if after_block do
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
{type, context}
else
{type, context}
end
end
if precise? and empty?(body_type) do
{{true, body_acc, clauses_acc}, context}
else
[arg_type] = Pattern.of_domain(trees, stack, context)
clauses_acc = [{arg_type, body_type, clause} | clauses_acc]
{{none?, union(body_type, body_acc), clauses_acc}, context}
end
end)
@timeout_type union(integer(), atom([:infinity]))
context =
if none? or stack.mode != :static do
head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2))
def of_expr({:receive, _meta, [blocks]}, expected, expr, stack, original) do
blocks
|> Enum.reduce({none(), original}, fn
{:do, {:__block__, _, []}}, acc_context ->
acc_context
{_, refined_context} =
of_expr(case_expr, head_type, case_expr, %{stack | reverse_arrow: :use}, context)
{:do, clauses}, {acc, context} ->
of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc)
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
{body_type, context} = of_expr(body, expected, expr, stack, context)
if compatible?(timeout_type, @timeout_type) do
{union(body_type, acc), Of.reset_vars(context, original)}
reset_warnings(refined_context, context)
else
error = {:badtimeout, timeout_type, timeout, context}
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
context
end
{body_acc, clauses_acc, context}
end)
|> dynamic_unless_static(stack)
end
# fn pat -> expr end
def of_expr({:fn, meta, clauses}, _expected, _expr, stack, context) do
cache_result(meta, stack, context, fn ->
[{:->, _, [head, _]} | _] = clauses
{patterns, _guards} = extract_head(head)
domain = Enum.map(patterns, fn _ -> dynamic() end)
{acc, context} =
of_clauses_fun(clauses, domain, :fn, stack, context, [], fn
trees, _precise?, {:->, _, [_, body]}, context, acc ->
{body_type, context} = of_expr(body, term(), body, stack, context)
args_types = Pattern.of_domain(trees, stack, context)
{add_inferred(acc, args_types, body_type), context}
end)
{fun_from_inferred_clauses(acc), context}
end)
end
def of_expr({:try, meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
cache_result(meta, stack, original, fn ->
{after_block, blocks} = Keyword.pop(blocks, :after)
{else_block, blocks} = Keyword.pop(blocks, :else)
{type, context} =
if else_block do
{type, context} = of_expr(body, term(), body, stack, original)
info = {:try_else, type}
of_clauses(else_block, [type], expected, info, stack, context, none())
else
of_expr(body, expected, expr, stack, original)
end
{type, context} =
blocks
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
{:rescue, clauses}, {_acc, %{failed: failed?}} = acc_context ->
Enum.reduce(clauses, acc_context, fn
{:->, meta, [[head], body]}, {acc, context} ->
{failed?, context} = reset_failed(context, failed?)
context =
case head do
{:in, meta, [var, mods]} ->
of_rescue(var, mods, expr, :rescue, meta, stack, context)
var ->
of_rescue(var, [], var, :anonymous_rescue, meta, stack, context)
end
{type, context} = of_expr(body, expected, body, stack, context)
{union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
end)
{:catch, clauses}, {acc, context} ->
args = [@try_catch, dynamic()]
of_clauses(clauses, args, expected, :try_catch, stack, context, acc)
end)
|> dynamic_unless_static(stack)
if after_block do
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
{type, context}
else
{type, context}
end
end)
end
@timeout_type union(integer(), atom([:infinity]))
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
cache_result(meta, stack, original, fn ->
blocks
|> Enum.reduce({none(), original}, fn
{:do, {:__block__, _, []}}, acc_context ->
acc_context
{:do, clauses}, {acc, context} ->
of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc)
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
{body_type, context} = of_expr(body, expected, expr, stack, context)
if compatible?(timeout_type, @timeout_type) do
{union(body_type, acc), Of.reset_vars(context, original)}
else
error = {:badtimeout, timeout_type, timeout, context}
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
end
end)
|> dynamic_unless_static(stack)
end)
end
def of_expr({:for, meta, [_ | _] = args}, expected, expr, stack, context) do
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
cache_result(meta, stack, context, fn ->
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
# We don't need to type check uniq, as it is a compile-time boolean.
# We handle reduce and into accordingly instead.
if Keyword.has_key?(opts, :reduce) do
reduce = Keyword.fetch!(opts, :reduce)
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
# TODO: We need to type check against dynamic() instead of using reduce_type
# because this is recursive. We need to infer the block type first.
args = [dynamic()]
of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type)
else
# TODO: Use the collectable protocol for the output
into = Keyword.get(opts, :into, [])
{into_type, into_kind, context} = for_into(into, meta, stack, context)
{block_type, context} = of_expr(block, @pending, block, stack, context)
# We don't need to type check uniq, as it is a compile-time boolean.
# We handle reduce and into accordingly instead.
if Keyword.has_key?(opts, :reduce) do
reduce = Keyword.fetch!(opts, :reduce)
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
# TODO: We need to type check against dynamic() instead of using reduce_type
# because this is recursive. We need to infer the block type first.
args = [dynamic()]
of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type)
else
# TODO: Use the collectable protocol for the output
into = Keyword.get(opts, :into, [])
{into_type, into_kind, context} = for_into(into, meta, stack, context)
case into_kind do
:bitstring ->
case compatible_intersection(block_type, bitstring()) do
{:ok, intersection} ->
{return_union(into_type, intersection, stack), context}
case into_kind do
:bitstring ->
{block_type, context} = of_expr(block, bitstring(), block, stack, context)
intersection = intersection(block_type, bitstring())
{:error, _} ->
if empty?(intersection) do
error = {:badbitbody, block_type, block, context}
{error_type(), error(__MODULE__, error, meta, stack, context)}
end
else
{union(into_type, intersection), context}
end
:non_empty_list ->
{return_union(into_type, non_empty_list(block_type), stack), context}
:non_empty_list ->
expected =
case list_hd(expected) do
{:ok, head} -> head
_ -> term()
end
:none ->
{into_type, context}
{block_type, context} = of_expr(block, expected, block, stack, context)
{union(into_type, non_empty_list(block_type)), context}
:none ->
{_, context} = of_expr(block, term(), block, stack, context)
{into_type, context}
end
|> dynamic_unless_static(stack)
end
end
end)
end
# TODO: with pat <- expr do expr end
def of_expr({:with, _meta, [_ | _] = clauses}, _expected, _expr, stack, original) do
{clauses, [options]} = Enum.split(clauses, -1)
context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2))
context = Enum.reduce(options, context, &with_option(&1, stack, &2, original))
{dynamic(), context}
def of_expr({:with, meta, [_ | _] = clauses}, expected, _expr, stack, original) do
cache_result(meta, stack, original, fn ->
{clauses, [[do: do_block] ++ options]} = Enum.split(clauses, -1)
{else_types, context} =
Enum.reduce(clauses, {none(), original}, &with_clause(&1, stack, &2))
{do_result, context} = of_expr(do_block, expected, do_block, stack, context)
context = Of.reset_vars(context, original)
{else_result, context} =
case options do
[else: clauses] ->
info = {:with_else, else_types}
of_clauses(clauses, [else_types], expected, info, stack, context, none())
[] ->
{else_types, context}
end
dynamic_unless_static({union(do_result, else_result), context}, stack)
end)
end
def of_expr({{:., _, [fun]}, _, args} = call, _expected, _expr, stack, context) do
@@ -473,7 +671,7 @@ defmodule Module.Types.Expr do
# TODO: Perform inference based on the strong domain of a function
{args_types, context} =
Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
Enum.map_reduce(args, context, &of_expr(&1, term(), &1, stack, &2))
Apply.fun(fun_type, args_types, call, stack, context)
end
@@ -530,8 +728,7 @@ defmodule Module.Types.Expr do
# var
def of_expr({_, meta, _} = var, expected, expr, stack, context) when is_var(var) do
version = Keyword.fetch!(meta, :version)
{type, context} = Of.refine_body_var(version, expected, expr, stack, context)
{type, Pattern.of_changed([version], stack, context)}
Of.refine_body_var(version, expected, expr, stack, context)
end
## Tuples
@@ -557,11 +754,11 @@ defmodule Module.Types.Expr do
## Try
defp of_rescue(var, exceptions, body, expr, info, meta, stack, original) do
args = [__exception__: @atom_true]
defp of_rescue(var, exceptions, expr, info, meta, stack, context) do
args = [__exception__: term()]
{structs, context} =
Enum.map_reduce(exceptions, original, fn exception, context ->
Enum.map_reduce(exceptions, context, fn exception, context ->
# Exceptions are not validated in the compiler,
# to avoid export dependencies. So we do it here.
{info, context} = Of.struct_info(exception, :expr, meta, stack, context)
@@ -573,39 +770,41 @@ defmodule Module.Types.Expr do
end
end)
context =
case var do
{:_, _, _} ->
context
case var do
{:_, _, _} ->
context
_ ->
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
expr = {:__block__, [type_check: info], [expr]}
context = Of.declare_var(var, context)
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
context
end
{type, context} = of_expr(body, @pending, body, stack, context)
{type, Of.reset_vars(context, original)}
_ ->
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
expr = {:__block__, [type_check: info], [expr]}
context = Of.declare_var(var, context)
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
context
end
end
## Comprehensions
defp for_clause({:<-, meta, [left, right]}, stack, context) do
expr = {:<-, [type_check: :generator] ++ meta, [left, right]}
{pattern, guards} = extract_head([left])
meta = [type_check: :generator] ++ meta
expr = {:<-, meta, [left, right]}
{[pattern], guards} = extract_head([left])
# TODO: Extract the type from enumerable protocol
{_type, context} =
Apply.remote(Enumerable, :count, [right], dynamic(), expr, stack, context, &of_expr/5)
Apply.remote(Enumerable, :count, [right], term(), expr, stack, context, &of_expr/5)
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, stack, context)
{_tree, _precise, context} =
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, meta, stack, context)
context
end
defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do
{right_type, context} = of_expr(right, bitstring(), expr, stack, context)
info = {:for, expr, dynamic()}
context = Pattern.of_generator(left, [], bitstring(), info, expr, stack, context)
{_tree, _precise?, context} =
Pattern.of_generator(left, [], bitstring(), :for, expr, meta, stack, context)
if compatible?(right_type, bitstring()) do
context
@@ -666,34 +865,29 @@ defmodule Module.Types.Expr do
end
end
defp return_union(left, right, stack) do
Apply.return(union(left, right), [left, right], stack)
end
## With
defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do
{pattern, guards} = extract_head([left])
{_type, context} = of_expr(right, @pending, right, stack, context)
info = {:with, expr, dynamic()}
Pattern.of_generator(pattern, guards, dynamic(), info, expr, stack, context)
defp with_clause({:<-, meta, [left, right]} = expr, stack, {else_types, context}) do
{[pattern], guards} = extract_head([left])
{type, context} =
of_expr(right, term(), right, %{stack | reverse_arrow: :cache}, context)
{trees, precise?, context} =
Pattern.of_generator(pattern, guards, type, :with, expr, meta, stack, context)
[pattern_type] = Pattern.of_domain(trees, stack, context)
{_, refined_context} =
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :use}, context)
else_type = if precise?, do: difference(type, pattern_type), else: type
{union(else_type, else_types), reset_warnings(refined_context, context)}
end
defp with_clause(expr, stack, context) do
{_type, context} = of_expr(expr, @pending, expr, stack, context)
context
end
defp with_option({:do, body}, stack, context, original) do
{_type, context} = of_expr(body, @pending, body, stack, context)
Of.reset_vars(context, original)
end
defp with_option({:else, clauses}, stack, context, _original) do
{_, context} =
of_clauses(clauses, [dynamic()], @pending, :with_else, stack, context, none())
context
defp with_clause(expr, stack, {else_types, context}) do
{_type, context} = of_expr(expr, term(), expr, stack, context)
{else_types, context}
end
## General helpers
@@ -722,36 +916,88 @@ defmodule Module.Types.Expr do
defp reduce_non_empty([head | tail], acc, fun),
do: reduce_non_empty(tail, fun.(head, acc, false), fun)
defp maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?) do
if is_warning(stack) do
case truthiness(head_type) do
:always_true when not last? ->
warning = {:badcond, "always match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
:always_false ->
warning = {:badcond, "never match", head_type, head, context}
warn(__MODULE__, warning, meta, stack, context)
_ ->
context
end
else
context
end
end
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
fun = fn _args_types, result, _context, acc -> union(result, acc) end
of_clauses_fun(clauses, domain, expected, base_info, stack, context, acc, fun)
defp cache_result(meta, %{reverse_arrow: reverse_arrow}, context, fun) do
case reverse_arrow do
nil ->
fun.()
:cache ->
{result, context} = fun.()
version = Keyword.fetch!(meta, :version)
context = put_in(context.reverse_arrows[version], result)
{result, context}
:use ->
version = Keyword.fetch!(meta, :version)
{Map.fetch!(context.reverse_arrows, version), context}
end
end
defp of_clauses_fun(clauses, domain, expected, base_info, stack, original, acc, fun) do
defp cache_arrows(_meta, %{reverse_arrow: nil}, fun) do
{result, _cache, context} = fun.()
{result, context}
end
defp cache_arrows(meta, %{reverse_arrow: :cache}, fun) do
{result, cache, context} = fun.()
version = Keyword.fetch!(meta, :version)
context = put_in(context.reverse_arrows[version], cache)
{result, context}
end
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc ->
{body_type, context} = of_expr(body, expected, body, stack, context)
{union(acc, body_type), context}
end
of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc)
end
defp of_clauses_fun(clauses, domain, base_info, stack, original, acc, of_acc) do
%{failed: failed?} = original
{result, _previous, context} =
Enum.reduce(clauses, {acc, Pattern.init_previous(), original}, fn
{:->, meta, [head, body]}, {acc, previous, context} ->
{:->, meta, [head, _]} = clause, {acc, previous, context} ->
{failed?, context} = reset_failed(context, failed?)
{patterns, guards} = extract_head(head)
info = {base_info, head}
{trees, previous, context} =
{trees, precise?, _, previous, context} =
Pattern.of_head(patterns, guards, domain, previous, info, meta, stack, context)
{result, context} = of_expr(body, expected, body, stack, context)
{fun.(trees, result, context, acc), previous,
context |> set_failed(failed?) |> Of.reset_vars(original)}
{acc, context} = of_acc.(trees, precise?, clause, context, acc)
{acc, previous, context |> set_failed(failed?) |> Of.reset_vars(original)}
end)
{result, context}
end
defp reset_warnings(context, %{warnings: warnings}), do: %{context | warnings: warnings}
defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}}
defp reset_failed(context, _), do: {false, context}
+27 -6
View File
@@ -363,7 +363,7 @@ defmodule Module.Types.Helpers do
{:case, meta, [expr, [do: clauses]]} ->
case meta[:type_check] do
{:case, _} ->
{:case, op} ->
case clauses do
[
{:->, _,
@@ -382,7 +382,8 @@ defmodule Module.Types.Helpers do
true
]},
{:->, _, [[{:_, _, Kernel}], false]}
] ->
]
when op == :! ->
{:!, meta, [expr]}
[
@@ -402,7 +403,8 @@ defmodule Module.Types.Helpers do
right_side
]},
{:->, _, [[{var, _, Kernel}], {var, _, Kernel}]}
] ->
]
when op == :|| ->
{:||, meta, [expr, right_side]}
[
@@ -422,7 +424,8 @@ defmodule Module.Types.Helpers do
{var, _, Kernel}
]},
{:->, _, [[{:_, _, Kernel}], right_side]}
] ->
]
when op == :&& ->
{:&&, meta, [expr, right_side]}
[
@@ -442,15 +445,33 @@ defmodule Module.Types.Helpers do
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
+1 -1
View File
@@ -495,7 +495,7 @@ defmodule Module.Types.Of 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, env) do
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
{:ok, info} -> {info, context}
{:error, _desc} -> {nil, context}
end
+186 -121
View File
@@ -18,6 +18,7 @@ defmodule Module.Types.Pattern do
defp empty_previous?({[], _}), do: true
defp empty_previous?({[_ | _], _}), do: false
# Previous is always an upper bound (static), so we can use subtype?
defp previous_subtype?(_, {[], _}), do: false
defp previous_subtype?([], _), do: true
defp previous_subtype?(args, {_, descr}), do: subtype?(args_to_previous(args), descr)
@@ -49,7 +50,9 @@ defmodule Module.Types.Pattern do
end
defp previous_to_string({list, _}) do
Enum.map_join(list, "\n ", fn types ->
list
|> Enum.reverse()
|> Enum.map_join("\n ", fn types ->
types
|> Enum.map_join(", ", &(&1 |> upper_bound() |> to_quoted_string()))
|> indent(4)
@@ -189,27 +192,28 @@ defmodule Module.Types.Pattern do
# First we check if it fails without previous, if it doesn't, check if it is redundant.
case of_precise_head(patterns, guards, expected, init_previous(), tag, stack, original) do
{other_trees, _, _, _, %{failed: true} = other_context} ->
{other_trees, previous, other_context}
{other_trees, false, args_types, previous, other_context}
{other_trees, _, _, args_types, other_context} ->
if previous_subtype?(args_types, previous) do
warning = {:redundant, tag, expected, args_types, previous, other_context}
{other_trees, previous, warn(__MODULE__, warning, meta, stack, other_context)}
context = warn(__MODULE__, warning, meta, stack, other_context)
{other_trees, false, args_types, previous, context}
else
{trees, previous, context}
{trees, false, args_types, previous, context}
end
end
else
cond do
check_previous? and previous_subtype?(args_types, previous) ->
warning = {:redundant, tag, expected, args_types, previous, context}
{trees, previous, warn(__MODULE__, warning, meta, stack, context)}
{trees, false, args_types, previous, warn(__MODULE__, warning, meta, stack, context)}
precise? ->
{trees, concat_previous(args_types, previous), context}
{trees, true, args_types, concat_previous(args_types, previous), context}
true ->
{trees, previous, context}
{trees, false, args_types, previous, context}
end
end
end
@@ -234,16 +238,15 @@ defmodule Module.Types.Pattern do
of_pattern_intersect(trees, 0, [], pattern_info, tag, stack, context),
# We compute the args types before we do the intersection with previous clauses
args_types =
(with [_ | _] <- previous,
{:ok, _types, context} <-
(with false <- empty_previous?(previous),
{:ok, context} <-
of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
trees_to_args_types(trees, stack, context)
else
_ -> nil
end),
{types, check_previous?} = of_pattern_previous(types, previous, stack),
{:ok, _types, context} <-
of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
{:ok, context} <- of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
{trees, pattern_precise? and guard_precise?, check_previous?,
args_types || trees_to_args_types(trees, stack, context), context}
else
@@ -261,18 +264,13 @@ defmodule Module.Types.Pattern do
@doc """
Computes the domain from the pattern tree and expected types.
Note we use `upper_bound` because the user of dynamic in the signature
Note we use `upper_bound` because the use of dynamic in the signature
won't make a difference.
"""
def of_domain([{tree, expected, _pattern} | trees], stack, context) do
[
intersection(of_pattern_tree(tree, stack, context), expected) |> upper_bound()
| of_domain(trees, stack, context)
]
end
def of_domain([], _stack, _context) do
[]
def of_domain(trees, stack, context) do
Enum.map(trees, fn {tree, _, _} ->
tree |> of_pattern_tree(stack, context) |> upper_bound()
end)
end
defp of_pattern_args_zip(
@@ -298,7 +296,8 @@ defmodule Module.Types.Pattern do
@doc """
Handles the match operator.
"""
def of_match(pattern, expected_fun, expr, stack, context) do
def of_match(pattern, expected_fun, expr, meta, stack, context) do
stack = %{stack | meta: meta}
context = init_pattern_info(context, [])
{tree, _precise?, context} =
@@ -312,9 +311,9 @@ defmodule Module.Types.Pattern do
with {:ok, types} <-
of_pattern_intersect(args, 0, [], pattern_info, tag, stack, context),
{:ok, [type], context} <-
{:ok, context} <-
of_pattern_refine(types, %{}, pattern_info, tag, stack, context) do
{type, context}
{of_pattern_tree(tree, stack, context), context}
else
{:error, context} -> {expected, context}
end
@@ -323,23 +322,27 @@ defmodule Module.Types.Pattern do
@doc """
Handles matches in generators.
"""
def of_generator(pattern, guards, expected, tag, expr, stack, %{vars: vars} = context) do
def of_generator(pattern, guards, expected, op, expr, meta, stack, %{vars: vars} = context)
when is_atom(op) do
stack = %{stack | meta: meta}
context = init_pattern_info(context, [])
{tree, _precise?, context} =
{tree, pattern_precise?, context} =
of_pattern(pattern, [%{root: {:arg, 0}, expr: expr}], stack, context)
{pattern_info, context} = pop_pattern_info(context)
{guard_precise?, changed, context} = of_guards(guards, vars, stack, context)
args = [{tree, expected, pattern}]
{_precise?, changed, context} = of_guards(guards, vars, stack, context)
tag = {op, expr, expected}
with {:ok, types} <-
of_pattern_intersect(args, 0, [], pattern_info, tag, stack, context),
{:ok, _types, context} <-
{:ok, context} <-
of_pattern_refine(types, changed, pattern_info, tag, stack, context) do
context
{args, pattern_precise? and guard_precise?, context}
else
{:error, context} -> context
{:error, context} -> {args, false, context}
end
end
@@ -398,7 +401,7 @@ defmodule Module.Types.Pattern do
context -> {:error, error_vars(pattern_info, context)}
else
{changed, context} ->
{:ok, types, of_changed(Map.keys(changed), stack, context)}
{:ok, of_changed(Map.keys(changed), stack, context)}
end
defp error_vars(pattern_info, context) do
@@ -410,9 +413,11 @@ defmodule Module.Types.Pattern do
defp match_var do
version = make_ref()
{version, {:match, [version: version], __MODULE__}}
{version, match_var(version)}
end
defp match_var(version), do: {:match, [version: version], __MODULE__}
defp match_error?({:match, _, __MODULE__}, _type), do: true
defp match_error?(_var, type), do: empty?(type)
@@ -805,12 +810,7 @@ defmodule Module.Types.Pattern do
end
end
# _
defp of_pattern({:_, _meta, _var_context}, _path, _stack, context) do
{term(), true, context}
end
# var
# var (includes underscores)
defp of_pattern({name, meta, ctx} = var, path, _stack, context)
when is_atom(name) and is_atom(ctx) do
version = Keyword.fetch!(meta, :version)
@@ -1023,8 +1023,8 @@ defmodule Module.Types.Pattern do
guard_context: :andalso,
parent_version: nil,
vars: vars,
changed: %{},
subpatterns: %{}
subpatterns_vars: %{},
changed: %{}
})
{precise?, context} = of_guards(guards, stack, context)
@@ -1049,7 +1049,12 @@ defmodule Module.Types.Pattern do
end)
expr = Enum.reduce(guards, {:_, [], []}, &{:when, [], [&2, &1]})
context = %{context | vars: vars, conditional_vars: conditional_vars}
context = %{
context
| vars: Map.merge(vars, context.pattern_info.subpatterns_vars),
conditional_vars: conditional_vars
}
{precise? and Of.all_same_conditional_vars?(vars_conds),
Of.reduce_conditional_vars(vars_conds, expr, stack, context)}
@@ -1070,7 +1075,7 @@ defmodule Module.Types.Pattern do
{false, context}
{true, maybe_or_always} ->
{maybe_or_always == :always and context.pattern_info.subpatterns == %{}, context}
{maybe_or_always == :always, context}
_false_tuple_or_none ->
error = {:badguard, type, guard, context}
@@ -1261,12 +1266,12 @@ defmodule Module.Types.Pattern do
defp of_remote(fun, _args, call, expected, stack, context)
when fun in [:and, :or, :andalso, :orelse] do
{both_domain, abort_domain, always_rhs?} =
{boolean, abort_domain, always_rhs?} =
case fun do
:andalso -> {@atom_true, @atom_false, false}
:orelse -> {@atom_false, @atom_true, false}
:and -> {@atom_true, @atom_false, true}
:or -> {@atom_false, @atom_true, true}
:andalso -> {true, @atom_false, false}
:orelse -> {false, @atom_true, false}
:and -> {true, @atom_false, true}
:or -> {false, @atom_true, true}
end
# If we have multiple operations in a row,
@@ -1282,8 +1287,8 @@ defmodule Module.Types.Pattern do
# only be true if both clauses are executed, so we know the first
# argument has to be true and the second has to be expected.
cond do
subtype?(expected, both_domain) ->
of_logical_all([left | right], true, both_domain, abort_domain, stack, context)
is_nil(context.pattern_info) or booleaness(expected) == {boolean, :always} ->
of_logical_all([left | right], true, expected, abort_domain, stack, context)
right == [] ->
of_guard(left, expected, left, stack, context)
@@ -1329,7 +1334,7 @@ defmodule Module.Types.Pattern do
call,
expected,
stack,
%{pattern_info: %{subpatterns: subpatterns}} = context
context
)
when fun in [:hd, :tl] do
arg_key =
@@ -1340,18 +1345,38 @@ defmodule Module.Types.Pattern do
subpattern_key = {fun, arg_key}
{var, context} =
case subpatterns do
%{^subpattern_key => var} ->
{var, context}
{found?, var_version, context} =
case context.subpatterns do
%{^subpattern_key => var_version} ->
case context.vars do
%{^var_version => _} -> {true, var_version, context}
%{} -> {false, var_version, context}
end
%{} ->
{type, context} =
Apply.remote(:erlang, fun, [arg], expected, call, stack, context, &of_guard/5)
var_version = make_ref()
{false, var_version, put_in(context.subpatterns[subpattern_key], var_version)}
end
{_, var} = match_var()
context = Of.declare_var(var, type, context)
{var, put_in(context.pattern_info.subpatterns[subpattern_key], var)}
var = match_var(var_version)
context =
if found? do
context
else
{type, context} =
Apply.remote(:erlang, fun, [arg], expected, call, stack, context, &of_guard/5)
context = Of.declare_var(var, type, context)
update_in(context.pattern_info, fn %{subpatterns_vars: subpatterns_vars} = pattern_info ->
%{
pattern_info
| vars: false,
subpatterns_vars:
Map.put(subpatterns_vars, var_version, Map.fetch!(context.vars, var_version))
}
end)
end
of_guard(var, expected, call, stack, context)
@@ -1521,30 +1546,43 @@ defmodule Module.Types.Pattern do
message =
with {_op, meta, expr, type} <- info,
true <- previous_subtype?(expected, previous) do
if match?({:case, :||}, meta[:type_check]) do
"""
the right-hand side of || will always execute:
case meta[:type_check] do
{:case, :||} ->
"""
the right-hand side of || will always execute:
#{expr_to_string(expr) |> indent(4)}
#{expr_to_string(expr) |> indent(4)}
because the left-hand side always evaluates to:
because the left-hand side always evaluates to:
#{to_quoted_string(type) |> indent(4)}
"""
else
"""
the following clause cannot match because the previous clauses already matched all possible values:
#{to_quoted_string(type) |> indent(4)}
"""
#{args_to_string(args) |> indent(4)} ->
{:case, :!} ->
"""
the following conditional expression:
it attempts to match on the result of:
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
#{expr_to_string(expr) |> indent(4)}
will always evaluate to true because the expression has type:
which has the already matched type:
#{to_quoted_string(type) |> indent(4)}
"""
#{to_quoted_string(type) |> indent(4)}
"""
_ ->
"""
the following clause cannot match because the previous clauses already matched all possible values:
#{args_to_string(args) |> indent(4)} ->
it attempts to match on the result of:
#{expr_to_string(expr) |> indent(4)}
which has the already matched type:
#{to_quoted_string(type) |> indent(4)}
"""
end
else
_ ->
@@ -1586,8 +1624,9 @@ defmodule Module.Types.Pattern do
#
# $ typep head_pattern =
# {{:def, kind, fun, types}, args, guards} or
# {{:case | :try_else, meta, expr, type}, [arg]} or
# {:for_reduce | :receive | :try_catch | :with_else | :fn, [arg]}
# {{:case, meta, expr, type}, [arg]} or
# {{:try_else | :with_else, type}, [arg]} or
# {:for_reduce | :receive | :try_catch | :fn, [arg]}
#
# $ typep match_pattern =
# {:with or :for or :match, pattern, type}
@@ -1633,62 +1672,74 @@ defmodule Module.Types.Pattern do
end
end
defp badpattern({{op, meta, expr, type}, args}, _index) when op in [:case, :try_else] do
with {:case, op} <- meta[:type_check] do
message =
cond do
op == :|| ->
additional =
with {:case, meta, [_, _]} <- expr,
{:case, :||} <- meta[:type_check] do
"(shown as ... below) "
else
_ -> ""
end
defp badpattern({{:case, meta, expr, type}, args}, _index) do
case meta[:type_check] do
{:case, op} ->
message =
cond do
op == :|| ->
additional =
with {:case, meta, [_, _]} <- expr,
{:case, :||} <- meta[:type_check] do
"(shown as ... below) "
else
_ -> ""
end
"""
the right-hand side of || #{additional}will never be executed:
"""
the right-hand side of || #{additional}will never be executed:
#{expr_to_string({:||, [], [expr, {:..., [], []}]}) |> indent(4)}
#{expr_to_string({:||, [], [expr, {:..., [], []}]}) |> indent(4)}
because the left-hand side always evaluates to:
because the left-hand side always evaluates to:
#{to_quoted_string(type) |> indent(4)}
"""
#{to_quoted_string(type) |> indent(4)}
"""
op in [:and, :or] ->
{first_message, second_message} =
case booleaness(type) do
{true, _} -> {" will always succeed", "because it evaluates to"}
{false, _} -> {" will never succeed", "because it evaluates to"}
:none -> {" will always fail", "because it evaluates to"}
_ -> {"", "will always evaluate to"}
end
op in [:and, :or] ->
{first_message, second_message} =
case booleaness(type) do
{true, _} -> {" will always succeed", "because it evaluates to"}
{false, _} -> {" will never succeed", "because it evaluates to"}
:none -> {" will always fail", "because it evaluates to"}
_ -> {"", "will always evaluate to"}
end
"""
the following conditional expression#{first_message}:
"""
the following conditional expression#{first_message}:
#{expr_to_string(expr) |> indent(4)}
#{expr_to_string(expr) |> indent(4)}
#{second_message}:
#{second_message}:
#{to_quoted_string(type) |> indent(4)}
"""
#{to_quoted_string(type) |> indent(4)}
"""
true ->
"""
the following conditional expression:
op == :! ->
"""
the following conditional expression:
#{expr_to_string(expr) |> indent(4)}
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
will always evaluate to:
will always evaluate to false because the expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
end
#{to_quoted_string(type) |> indent(4)}
"""
true ->
"""
the following conditional expression:
#{expr_to_string(expr) |> indent(4)}
will always evaluate to:
#{to_quoted_string(type) |> indent(4)}
"""
end
{expr, message}
{expr, message}
else
_ ->
{args,
"""
@@ -1707,8 +1758,22 @@ defmodule Module.Types.Pattern do
end
end
defp badpattern({op, args}, index)
when op in [:for_reduce, :receive, :try_catch, :with_else, :fn] do
defp badpattern({{op, type}, args}, index) when op in [:try_else, :with_else] do
arg = Enum.fetch!(args, index)
{arg,
"""
the following clause will never match:
#{args_to_string(args) |> indent(4)} ->
it is expected to match on type:
#{to_quoted_string(type) |> indent(4)}
"""}
end
defp badpattern({op, args}, index) when op in [:for_reduce, :receive, :try_catch, :fn] do
arg = Enum.fetch!(args, index)
{arg,
+8 -2
View File
@@ -952,8 +952,14 @@ defmodule OptionParser do
case types |> List.delete(:keep) |> List.first(:string) do
:boolean ->
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
case to_switch(name) do
"--no-" <> _ = switch ->
add_aliases(switch, name, reverse_aliases)
switch ->
base = "#{switch}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
end
type ->
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
+1 -1
View File
@@ -485,7 +485,7 @@ defmodule Path do
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
case :file.get_cwd() do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
_ -> path
_ -> IO.chardata_to_string(path)
end
end
-2
View File
@@ -387,8 +387,6 @@ defmodule Process do
automatically canceled when `dest` is an atom (as the atom resolution is done
on delivery).
Inlined by the compiler.
## Options
* `:abs` - (boolean) when `false`, `time` is treated as relative to the
+1 -1
View File
@@ -786,7 +786,7 @@ defmodule Protocol do
end
defp fallback_clause_for(value, _protocol, meta) do
{meta, [quote(do: _)], [], value}
{meta, [{:_, [version: -1], __MODULE__}], [], value}
end
# Finally compile the module and emit its bytecode.
+191 -76
View File
@@ -258,7 +258,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case safe_lookup_second(key_ets, key) do
case lookup_second(:unique, key_ets, key) do
{pid, _} ->
if Process.alive?(pid), do: pid, else: :undefined
@@ -369,7 +369,10 @@ defmodule Registry do
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
many different keys (e.g., many topics with few subscribers each). This makes
key-based lookups more efficient as they only need to check a single partition
instead of all partitions.
instead of all partitions. This option uses a different internal ETS table type
(`ordered_set` instead of `duplicate_bag`), which means match specs passed to
`select/2` and `count_select/2` that reference `:"$_"` may behave differently.
Use named match variables (`:"$1"`, `:"$2"`, etc.) instead.
"""
@doc since: "1.5.0"
@@ -532,13 +535,17 @@ defmodule Registry do
given as an option, the dispatching happens in parallel. In both cases,
the callback is only invoked if there are entries for that partition.
For `{:duplicate, :key}` registries, all entries for a given key are in a
single partition, so the `:parallel` option has no effect.
See the module documentation for examples of using the `dispatch/3`
function for building custom dispatching or a pubsub system.
## Options
* `:parallel` - if `true`, the dispatching is done in parallel
across all partitions. Defaults to `false`.
across all partitions. Only meaningful for `{:duplicate, :pid}`
registries. Defaults to `false`.
"""
@doc since: "1.4.0"
@@ -549,17 +556,24 @@ defmodule Registry do
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
case key_info!(registry) do
{:unique, partitions, key_ets} ->
(key_ets || key_ets!(registry, key, partitions))
|> safe_lookup_second(key)
key_ets = key_ets || key_ets!(registry, key, partitions)
:unique
|> lookup_second(key_ets, key)
|> List.wrap()
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, _}, 1, key_ets} ->
key_ets
|> safe_lookup_second(key)
{{:duplicate, _} = kind, 1, key_ets} ->
kind
|> lookup_second(key_ets, key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, _}, partitions, _} ->
{{:duplicate, :key}, partitions, _} ->
{:duplicate, :key}
|> lookup_second(key_ets!(registry, key, partitions), key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{{:duplicate, :pid}, partitions, _} ->
if Keyword.get(opts, :parallel, false) do
registry
|> dispatch_parallel(key, mfa_or_fun, partitions)
@@ -579,9 +593,8 @@ defmodule Registry do
defp dispatch_serial(registry, key, mfa_or_fun, partition) do
partition = partition - 1
registry
|> key_ets!(partition)
|> safe_lookup_second(key)
{:duplicate, :pid}
|> lookup_second(key_ets!(registry, partition), key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
dispatch_serial(registry, key, mfa_or_fun, partition)
@@ -597,9 +610,8 @@ defmodule Registry do
task =
Task.async(fn ->
registry
|> key_ets!(partition)
|> safe_lookup_second(key)
{:duplicate, :pid}
|> lookup_second(key_ets!(registry, partition), key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
Process.unlink(parent)
@@ -663,7 +675,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case safe_lookup_second(key_ets, key) do
case lookup_second(:unique, key_ets, key) do
{_, _} = pair ->
[pair]
@@ -671,16 +683,15 @@ defmodule Registry do
[]
end
{{:duplicate, _}, 1, key_ets} ->
safe_lookup_second(key_ets, key)
{{:duplicate, _} = kind, 1, key_ets} ->
lookup_second(kind, key_ets, key)
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
safe_lookup_second(key_ets!(registry, partition), key)
lookup_second({:duplicate, :key}, key_ets!(registry, key, partitions), key)
{{:duplicate, :pid}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- safe_lookup_second(key_ets!(registry, partition), key),
pair <- lookup_second({:duplicate, :pid}, key_ets!(registry, partition), key),
do: pair
end
end
@@ -791,21 +802,18 @@ defmodule Registry do
@doc since: "1.4.0"
@spec match(registry, key, match_pattern, guards) :: [{pid, term}]
def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
case key_info!(registry) do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
{{:duplicate, :pid}, partitions, nil} ->
spec = match_spec({:duplicate, :pid}, key, pattern, guards)
{{:duplicate, _}, 1, key_ets} ->
:ets.select(key_ets, spec)
{{:duplicate, _}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- :ets.select(key_ets!(registry, partition), spec),
do: pair
{kind, partitions, key_ets} ->
spec = match_spec(kind, key, pattern, guards)
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
end
end
@@ -943,7 +951,7 @@ defmodule Registry do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
case safe_lookup_second(key_ets, key) do
case lookup_second(:unique, key_ets, key) do
{^pid, value} ->
[value]
@@ -951,18 +959,17 @@ defmodule Registry do
[]
end
{{:duplicate, _}, 1, key_ets} ->
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, _} = kind, 1, key_ets} ->
for {^pid, value} <- lookup_second(kind, key_ets, key), do: value
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
key_ets = key_ets!(registry, key, partitions)
for {^pid, value} <- lookup_second({:duplicate, :key}, key_ets, key), do: value
{{:duplicate, :pid}, partitions, _key_ets} ->
partition = hash(pid, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
for {^pid, value} <- lookup_second({:duplicate, :pid}, key_ets, key), do: value
end
end
@@ -1016,7 +1023,8 @@ defmodule Registry do
# Remove first from the key_ets because in case of crashes
# the pid_ets will still be able to clean up. The last step is
# to clean if we have no more entries.
true = __unregister__(key_ets, {key, {self, :_}}, 1)
true = __unregister__(key_ets, {key, {self, :_}}, 1, ordered?(kind))
true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2)
unlink_if_unregistered(pid_server, pid_ets, self)
@@ -1081,13 +1089,10 @@ defmodule Registry do
# to clean if we have no more entries.
# Here we want to count all entries for this pid under this key, regardless of pattern.
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
{total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards)
total = :ets.select_count(key_ets, total_spec)
# We only want to delete things that match the pattern
delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]
case :ets.select_delete(key_ets, delete_spec) do
# We deleted everything, we can just delete the object
^total ->
@@ -1121,6 +1126,25 @@ defmodule Registry do
:ok
end
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
else
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
end
end
defp unregister_match_specs(_kind, key, self, pattern, guards) do
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
end
@doc """
Registers the current process under the given `key` in `registry`.
@@ -1180,7 +1204,14 @@ defmodule Registry do
counter = System.unique_integer()
true = :ets.insert(pid_ets, {self, key, key_ets, counter})
case register_key(kind, key_ets, key, {key, {self, value}}) do
key_entry =
if ordered?(kind) do
{{key, self, counter}, value}
else
{key, {self, value}}
end
case register_key(kind, key_ets, key, key_entry) do
:ok ->
for listener <- listeners do
Kernel.send(listener, {:register, registry, key, self, value})
@@ -1409,21 +1440,18 @@ defmodule Registry do
@spec count_match(registry, key, match_pattern, guards) :: non_neg_integer()
def count_match(registry, key, pattern, guards \\ [])
when is_atom(registry) and is_list(guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
spec = [{{:_, {:_, pattern}}, guards, [true]}]
case key_info!(registry) do
{:unique, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
{{:duplicate, :pid}, partitions, nil} ->
spec = count_match_spec({:duplicate, :pid}, key, pattern, guards)
{{:duplicate, _}, 1, key_ets} ->
:ets.select_count(key_ets, spec)
{{:duplicate, _}, partitions, _key_ets} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
{kind, partitions, key_ets} ->
spec = count_match_spec(kind, key, pattern, guards)
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
end
end
@@ -1452,6 +1480,9 @@ defmodule Registry do
some operations like `:element` to modify the output format.
Do not use special match variables `:"$_"` and `:"$$"`, because they might not work as expected.
In particular, `{:duplicate, :key}` registries use a different internal ETS layout, so match specs
that reference the underlying entry structure via `:"$_"` will return different results.
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
Note that for large registries with many partitions this will be costly as it builds the result by
concatenating all the partitions.
@@ -1479,15 +1510,16 @@ defmodule Registry do
@spec select(registry, spec) :: [term]
def select(registry, spec)
when is_atom(registry) and is_list(spec) do
spec = group_match_headers(spec, __ENV__.function)
case key_info!(registry) do
{_kind, partitions, nil} ->
{kind, partitions, nil} ->
spec = group_match_headers(spec, kind, __ENV__.function)
Enum.flat_map(0..(partitions - 1), fn partition_index ->
:ets.select(key_ets!(registry, partition_index), spec)
end)
{_kind, 1, key_ets} ->
{kind, 1, key_ets} ->
spec = group_match_headers(spec, kind, __ENV__.function)
:ets.select(key_ets, spec)
end
end
@@ -1510,24 +1542,30 @@ defmodule Registry do
@spec count_select(registry, spec) :: non_neg_integer()
def count_select(registry, spec)
when is_atom(registry) and is_list(spec) do
spec = group_match_headers(spec, __ENV__.function)
case key_info!(registry) do
{_kind, partitions, nil} ->
{kind, partitions, nil} ->
spec = group_match_headers(spec, kind, __ENV__.function)
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
{_kind, 1, key_ets} ->
{kind, 1, key_ets} ->
spec = group_match_headers(spec, kind, __ENV__.function)
:ets.select_count(key_ets, spec)
end
end
defp group_match_headers(spec, {fun, arity}) do
defp group_match_headers(spec, kind, {fun, arity}) do
ordered = ordered?(kind)
for part <- spec do
case part do
{{key, pid, value}, guards, select} ->
{{key, {pid, value}}, guards, select}
{{key, pid, value}, guards, body} when ordered ->
{{{key, pid, :_}, value}, guards, body}
{{key, pid, value}, guards, body} ->
{{key, {pid, value}}, guards, body}
_ ->
raise ArgumentError,
@@ -1578,7 +1616,23 @@ defmodule Registry do
:ets.lookup_element(registry, partition, 3)
end
defp safe_lookup_second(ets, key) do
defp lookup_second({:duplicate, :key}, ets, key) do
spec =
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
else
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
end
try do
:ets.select(ets, spec)
catch
:error, :badarg -> []
end
end
defp lookup_second(_kind, ets, key) do
try do
:ets.lookup_element(ets, key, 2)
catch
@@ -1607,7 +1661,19 @@ defmodule Registry do
end
@doc false
def __unregister__(table, match, pos) do
def __unregister__(table, match, pos, ordered \\ false)
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
else
:ets.match_delete(table, {{key, pid, :_}, :_})
end
end
def __unregister__(table, match, pos, false = _ordered) do
key = :erlang.element(pos, match)
# We need to perform an element comparison if we have a special atom key.
@@ -1620,9 +1686,52 @@ defmodule Registry do
end
end
# For {:duplicate, :key} registries, the ETS entry layout is
# {{key, pid, counter}, value} (ordered_set with composite key),
# so :"$_" refers to that whole tuple. The body extracts {pid, value}
# to match the public API's return shape of [{pid, value}].
defp match_spec({:duplicate, :key}, key, pattern, guards) do
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, body}]
else
[{{{key, :_, :_}, pattern}, guards, body}]
end
end
defp match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
end
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
guards = [
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
]
[{{{:_, :_, :_}, pattern}, guards, [true]}]
else
[{{{key, :_, :_}, pattern}, guards, [true]}]
end
end
defp count_match_spec(_kind, key, pattern, guards) do
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
[{{:_, {:_, pattern}}, guards, [true]}]
end
defp reserved_atom?("_"), do: true
defp reserved_atom?("$" <> _), do: true
defp reserved_atom?(_), do: false
defp ordered?({:duplicate, :key}), do: true
defp ordered?(_), do: false
end
defmodule Registry.Supervisor do
@@ -1734,7 +1843,8 @@ defmodule Registry.Partition do
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
end
{:ok, {pid_ets, %{}}}
ordered = kind == {:duplicate, :key}
{:ok, {pid_ets, %{}, ordered}}
end
# The key partition is a set for unique keys,
@@ -1744,7 +1854,12 @@ defmodule Registry.Partition do
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
defp init_key_ets({:duplicate, :key}, key_partition, compressed) do
opts = [:ordered_set, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets({:duplicate, :pid}, key_partition, compressed) do
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
@@ -1768,7 +1883,7 @@ defmodule Registry.Partition do
{:reply, :ok, state}
end
def handle_call({:lock, key}, from, {ets, lock}) do
def handle_call({:lock, key}, from, {ets, lock, ordered}) do
lock =
case lock do
%{^key => queue} ->
@@ -1779,10 +1894,10 @@ defmodule Registry.Partition do
Map.put(lock, key, :queue.new())
end
{:noreply, {ets, lock}}
{:noreply, {ets, lock, ordered}}
end
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
def handle_info({:EXIT, pid, _reason}, {ets, lock, ordered}) do
entries = :ets.take(ets, pid)
for {_pid, key, key_ets, _counter} <- entries do
@@ -1797,13 +1912,13 @@ defmodule Registry.Partition do
end
try do
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1)
Registry.__unregister__(key_ets, {key, {pid, :_}}, 1, ordered)
catch
:error, :badarg -> :badarg
end
end
{:noreply, {ets, lock}}
{:noreply, {ets, lock, ordered}}
end
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
@@ -1815,7 +1930,7 @@ defmodule Registry.Partition do
unlock(key, state)
end
defp unlock(key, {ets, lock}) do
defp unlock(key, {ets, lock, ordered}) do
%{^key => queue} = lock
lock =
@@ -1824,7 +1939,7 @@ defmodule Registry.Partition do
{:not_empty, queue} -> Map.put(lock, key, queue)
end
{:noreply, {ets, lock}}
{:noreply, {ets, lock, ordered}}
end
defp dequeue(queue, key) do
+18 -2
View File
@@ -1437,7 +1437,7 @@ defmodule Stream do
do_cycle(cycle, [], cycle, fun.(element, acc), fun)
{_, []} ->
do_cycle(cycle, [], cycle, {:cont, acc}, fun)
do_cycle(check_cycle_subsequent_element(cycle), [], cycle, {:cont, acc}, fun)
end
end
@@ -1446,10 +1446,26 @@ defmodule Stream do
end
defp check_cycle_first_element(reduce) do
check_cycle_non_empty(
reduce,
ArgumentError,
"cannot cycle over an empty enumerable"
)
end
defp check_cycle_subsequent_element(reduce) do
check_cycle_non_empty(
reduce,
RuntimeError,
"cycled enumerable became empty after a previous iteration produced elements"
)
end
defp check_cycle_non_empty(reduce, exception, message) do
fn acc ->
case reduce.(acc) do
{state, []} when state in [:done, :halted] and elem(acc, 0) != :halt ->
raise ArgumentError, "cannot cycle over an empty enumerable"
raise exception, message
other ->
other
+31 -2
View File
@@ -745,7 +745,15 @@ defmodule String do
iex> String.count("hello world", "")
12
The `pattern` can also be a compiled pattern:
The `pattern` can also be a list:
iex> String.count("hello world", ["e", "o"])
3
iex> String.count("hello world", [])
0
Or a compiled pattern:
iex> pattern = :binary.compile_pattern([" ", "!"])
iex> String.count("foo bar baz!!", pattern)
@@ -756,6 +764,8 @@ defmodule String do
@doc since: "1.19.0"
def count(string, <<>>), do: length(string) + 1
def count(_string, []), do: 0
def count(string, pattern) when is_struct(pattern, Regex) do
Kernel.length(Regex.scan(pattern, string, return: :index))
end
@@ -2290,6 +2300,19 @@ defmodule String do
end
end
# 56-bit SWAR guard: all 7 bytes are ASCII (< 128) and none is \r (0x0D).
# Uses Mycroft's zero-byte detection: XOR with 0x0D and check for zero bytes.
defguardp ascii_no_cr_swar?(w)
when Bitwise.band(w, 0x80808080808080) == 0 and
Bitwise.band(
Bitwise.bxor(w, 0x0D0D0D0D0D0D0D) - 0x01010101010101,
0x80808080808080
) == 0
defp skip_length(<<w::56, b, rest::binary>>, acc)
when b <= 127 and b != ?\r and ascii_no_cr_swar?(w),
do: skip_length(rest, acc + 8)
defp skip_length(<<byte, rest::binary>>, acc)
when byte <= 127 and byte != ?\r,
do: skip_length(rest, acc + 1)
@@ -3213,7 +3236,13 @@ defmodule String do
byte_size_unicode(unicode)
end
defp byte_size_remaining_at(unicode, n) do
defp byte_size_remaining_at(<<byte1, byte2, rest::binary>> = binary, n)
when n > 0 and byte1 <= 127 and byte1 != ?\r and byte2 <= 127 and byte2 != ?\r do
skip = min(skip_length(rest, 1), n)
byte_size_remaining_at(binary_part(binary, skip, byte_size(binary) - skip), n - skip)
end
defp byte_size_remaining_at(unicode, n) when n > 0 do
case :unicode_util.gc(unicode) do
[_] -> 0
[_ | rest] -> byte_size_remaining_at(rest, n - 1)
+34 -13
View File
@@ -556,19 +556,29 @@ defmodule Supervisor do
Developers typically invoke `Supervisor.init/2` at the end of their
init callback to return the proper supervision flags.
See `start_link/2` for more information.
"""
@callback init(init_arg :: term) ::
{:ok,
{sup_flags(), [child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
| :ignore
@typedoc "Return values of `start_link/2` and `start_link/3`."
@typedoc """
Return values of `start_link/2` and `start_link/3`.
See the documentation for those functions for more information.
"""
@type on_start ::
{:ok, pid}
| :ignore
| {:error, {:already_started, pid} | {:shutdown, term} | term}
@typedoc "Return values of `start_child/2`."
@typedoc """
Return values of `start_child/2`.
See the documentation for those functions for more information.
"""
@type on_start_child ::
{:ok, child}
| {:ok, child, info :: term}
@@ -698,15 +708,18 @@ defmodule Supervisor do
If the supervisor and all child processes are successfully spawned
(if the start function of each child process returns `{:ok, child}`,
`{:ok, child, info}`, or `:ignore`), this function returns
`{:ok, pid}`, where `pid` is the PID of the supervisor. If the supervisor
is given a name and a process with the specified name already exists,
the function returns `{:error, {:already_started, pid}}`, where `pid`
is the PID of that process.
`{:ok, pid}`, where `pid` is the PID of the supervisor.
If the start function of any of the child processes fails or returns an error
tuple or an erroneous value, the supervisor first terminates with reason
`:shutdown` all the child processes that have already been started, and then
terminates itself and returns `{:error, {:shutdown, reason}}`.
If the supervisor is given a name and a process with the specified name
already exists, the function returns `{:error, {:already_started, pid}}`,
where `pid` is the PID of that process.
If the start function of any of the child processes fails or returns
an error tuple or an erroneous value, the supervisor first terminates
with reason `:shutdown` all the child processes that have already been
started, and then terminates itself. If the caller process is trapping
exits, this function then returns
`{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits
@@ -939,9 +952,17 @@ defmodule Supervisor do
If the `c:init/1` callback returns `:ignore`, this function returns
`:ignore` as well and the supervisor terminates with reason `:normal`.
If it fails or returns an incorrect value, this function returns
`{:error, term}` where `term` is a term with information about the
error, and the supervisor terminates with reason `term`.
If the `c:init/1` callback fails or returns an incorrect value, the supervisor
will shutdown with reason `term`, where `term` contains information
about the error. If the caller process is trapping exits, this function then
returns `{:error, term()}`.
If the start function of any of the child processes returned by `c:init/1`
fail or return an error tuple or an erroneous value, the supervisor first
terminates with reason `:shutdown` all the child processes that have already
been started, and then terminates itself. If the caller process is trapping
exits, this function then returns `{:error, {:shutdown, {:failed_to_start_child, id, reason}}}`.
The `:name` option can also be given in order to register a supervisor
name, the supported values are described in the "Name registration"
@@ -75,7 +75,7 @@ iex> trunc(3.58)
3
```
Finally, we work with different data types, we will learn Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
Finally, as we work with different data types, we will learn that Elixir provides several predicate functions to check for the type of a value. For example, [`is_integer`](`is_integer/1`) can be used to check if a value is an integer or not:
```elixir
iex> is_integer(1)
@@ -310,4 +310,4 @@ false
The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](`Kernel#module-structural-comparison`).
Elixir also provides data-types for expressing collections, such as lists and tuples, which we learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come on later chapters. Let's move forward.
Elixir also provides data-types for expressing collections, such as lists and tuples, which we'll learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come in later chapters. Let's move forward.
@@ -212,7 +212,11 @@ end
The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above:
> #### Module names are isolated {: .info}
>
> You don't have to define the `Foo` module before defining the `Foo.Bar` module, as they are effectively independent.
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive, which we will learn in the next chapter:
```elixir
defmodule Foo.Bar do
@@ -223,24 +227,3 @@ defmodule Foo do
# Can still access it as `Bar`
end
```
> #### Module names are isolated {: .info}
>
> You don't have to define the `Foo` module before defining the `Foo.Bar` module, as they are effectively independent.
Aliasing a module only alias a single module, it doesn't alias any of its parents. Consider the following example:
```elixir
defmodule Foo do
defmodule Bar do
defmodule Baz do
end
end
end
alias Foo.Bar.Baz
# The module `Foo.Bar.Baz` is now available as `Baz`
# However, the module `Foo.Bar` is *not* available as `Bar`
```
As we will see in later chapters, aliases also work well with macros, to guarantee they are hygienic.
@@ -241,7 +241,7 @@ $ mix help
mix deps # Lists dependencies and their status
mix deps.clean # Deletes the given dependencies' files
mix deps.compile # Compiles dependencies
mix deps.get # Gets all out of date dependencies
mix deps.get # Fetches unavailable and out of date dependencies
mix deps.tree # Prints the dependency tree
mix deps.unlock # Unlocks the given dependencies
mix deps.update # Updates the given dependencies
@@ -143,7 +143,7 @@ With this change, we are saying that we want to run `KV.Server.accept(4040)` as
Now that the server is part of the supervision tree, it should start automatically when we run the application. Run `iex -S mix` to boot the app and use the `telnet` client to make sure that everything still works:
```console
$ telnet 127.0.0.1 4321
$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
@@ -158,7 +158,7 @@ Yes, it works! However, can it handle more than one client?
Try to connect two telnet clients at the same time. When you do so, you will notice that the second client doesn't echo:
```console
$ telnet 127.0.0.1 4321
$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
@@ -14,12 +14,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Development
1.19 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
@@ -243,4 +242,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md#4-hard-deprecations
@@ -233,18 +233,24 @@ Inferring type signatures comes with a series of trade-offs:
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing, without requiring any effort from them and keeping the expressiveness of the language.
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
In the long term, Elixir developers who want static typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
## Roadmap
The current milestone is to implement type inference of existing codebases, as well as type checking of all language constructs, without changes to the Elixir language. At this stage, we want to collect feedback on the quality of error messages and performance, and therefore the type system has no user facing API. Full type inference of patterns was released in Elixir v1.18, and complete inference is expected as part of Elixir v1.20.
At this moment, Elixir implements type inference of all language constructs. The goal is to assess performance and collect feedback on the quality of error messages, before introducing user facing types.
If the results are satisfactory, the next milestone will include a mechanism for defining typed structs. Elixir programs frequently pattern match on structs, which reveals information about the struct fields, but it knows nothing about their respective types. By propagating types from structs and their fields throughout the program, we will increase the type system’s ability to find errors while further straining our type system implementation. Proposals including the required changes to the language surface will be sent to the community once we reach this stage.
The third milestone is to introduce set-theoretic type signatures for functions. Unfortunately, the existing Erlang Typespecs are not precise enough for set-theoretic types and they will be phased out of the language and have their postprocessing moved into a separate library once this stage concludes.
## Resources
* (Paper) ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391)
* (Video) ["The foundations of the Elixir type system" by José Valim](https://youtu.be/giYbq4HmfGA)
* (Video) ["Precision in type system design" by José Valim](https://youtu.be/Ay-gnCqDw9o?t=102)
## Acknowledgements
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), and [Tidewave](https://tidewave.ai/).
+26 -13
View File
@@ -304,6 +304,9 @@ eval_forms(Tree, Binding, OrigE) ->
eval_forms(Tree, Binding, OrigE, []).
eval_forms(Tree, Binding, OrigE, Opts) ->
Prune = proplists:get_value(prune_binding, Opts, false),
%% We save and restore the previous dbg_callback so nested eval calls in
%% the same process do not clobber the outer callback.
PreviousDbg = erlang:get({elixir, dbg_callback}),
case proplists:get_value(dbg_callback, Opts) of
undefined -> ok;
DbgCallback -> erlang:put({elixir, dbg_callback}, DbgCallback)
@@ -332,7 +335,10 @@ eval_forms(Tree, Binding, OrigE, Opts) ->
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
end
after
erlang:erase({elixir, dbg_callback})
case PreviousDbg of
undefined -> erlang:erase({elixir, dbg_callback});
_ -> erlang:put({elixir, dbg_callback}, PreviousDbg)
end
end.
%% Evaluate Erlang code with careful handling of local and external functions
@@ -341,20 +347,27 @@ erl_eval(Expr, Binding, Env) ->
LocalHandler = {value, fun ?MODULE:eval_local_handler/2},
ExternalHandler = {value, fun ?MODULE:eval_external_handler/3},
%% ?elixir_eval_env is used by the external handler.
%%
%% The reason why we use the process dictionary to pass the environment
%% is because we want to avoid passing closures to erl_eval, as that
%% would effectively tie the eval code to the Elixir version and it is
%% best if it depends solely on Erlang/OTP.
%%
%% The downside is that functions that escape the eval context will no
%% longer have the original environment they came from.
%%
%% We save and restore the previous env so nested eval calls in the same
%% process do not clobber the outer env.
PreviousEvalEnv = erlang:get(?elixir_eval_env),
erlang:put(?elixir_eval_env, Env),
try
%% ?elixir_eval_env is used by the external handler.
%%
%% The reason why we use the process dictionary to pass the environment
%% is because we want to avoid passing closures to erl_eval, as that
%% would effectively tie the eval code to the Elixir version and it is
%% best if it depends solely on Erlang/OTP.
%%
%% The downside is that functions that escape the eval context will no
%% longer have the original environment they came from.
erlang:put(?elixir_eval_env, Env),
erl_eval:expr(Expr, Binding, LocalHandler, ExternalHandler)
after
erlang:erase(?elixir_eval_env)
case PreviousEvalEnv of
undefined -> erlang:erase(?elixir_eval_env);
_ -> erlang:put(?elixir_eval_env, PreviousEvalEnv)
end
end.
eval_local_handler(FunName, Args) ->
@@ -439,7 +452,7 @@ quoted_to_erl(Quoted, ErlS, ExS, Env) ->
%% Converts a given string (charlist) into quote expression
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_binary(File) ->
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_integer(StartColumn), is_binary(File) ->
case elixir_tokenizer:tokenize(String, StartLine, StartColumn, Opts) of
{ok, _Line, _Column, Warnings, Tokens, Terminators} ->
{ok, lists:reverse(Tokens, Terminators), Warnings};
+5 -3
View File
@@ -31,8 +31,8 @@
prematch=none,
%% Stores if __STACKTRACE__ is allowed
stacktrace=false,
%% A map of unused vars and a version counter for vars
unused={#{}, 0},
%% A map of unused vars
unused=#{},
%% A list of modules defined in functions (runtime)
runtime_modules=[],
%% A tuple with maps of read and optional write current vars.
@@ -46,7 +46,9 @@
%% if you write foo(a = 123), the value of `a` cannot be
%% read in the following argument, only after the call
%%
vars={#{}, false}
vars={#{}, false},
%% Stores expression version counter
version=0
}).
-record(elixir_erl, {
+4 -2
View File
@@ -61,7 +61,7 @@ expand(BitstrMeta, Fun, [H | T], Acc, S, E, Alignment, RequireSize) ->
InferredMeta = [{inferred_bitstring_spec, true} | Meta],
EAcc = concat_or_prepend_bitstring(InferredMeta, ELeft, ERight, Acc, ES, MatchOrRequireSize),
expand(Meta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
expand(BitstrMeta, Fun, T, EAcc, {SS, OriginalS}, ES, Alignment, RequireSize).
extract_meta({_, Meta, _}, _) -> Meta;
extract_meta(_, Meta) -> Meta.
@@ -115,7 +115,9 @@ concat_or_prepend_bitstring(Meta, {'<<>>', PartsMeta, Parts} = ELeft, ERight, Ac
[{'::', Meta, [ELeft, ERight]} | Acc]
end;
{bitstring, _, nil} ->
lists:reverse(Parts, Acc)
lists:reverse(Parts, Acc);
_ ->
[{'::', Meta, [ELeft, ERight]} | Acc]
end;
concat_or_prepend_bitstring(Meta, ELeft, ERight, Acc, _E, _RequireSize) ->
[{'::', Meta, [ELeft, ERight]} | Acc].
+12 -7
View File
@@ -155,13 +155,14 @@ recur_cycles(Cycles, Current, Source, Seen, SkipList, Meta, Expr, E) ->
%% Match
match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
#elixir_ex{vars=Current, unused={_, Counter} = Unused} = AfterS,
#elixir_ex{vars=Current, unused=Unused, version=Counter} = AfterS,
#elixir_ex{vars={Read, _}, prematch=Prematch} = BeforeS,
CallS = BeforeS#elixir_ex{
prematch={Read, {#{}, []}, Counter},
unused=Unused,
vars=Current
vars=Current,
version=Counter
},
CallE = E#{context := match},
@@ -170,7 +171,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
#elixir_ex{
vars=NewCurrent,
unused=NewUnused,
prematch={_, Cycles, _}
prematch={_, Cycles, _},
version=NewCounter
} = SE,
validate_cycles(Cycles, Meta, {match, Expr}, E),
@@ -178,7 +180,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) ->
EndS = AfterS#elixir_ex{
prematch=Prematch,
unused=NewUnused,
vars=NewCurrent
vars=NewCurrent,
version=NewCounter
},
EndE = EE#{context := ?key(E, context)},
@@ -319,7 +322,9 @@ with(Meta, Args, S, E) ->
ok
end,
{{with, Meta, EExprs ++ [[{do, EDo} | EOpts]]}, S3, E}.
#elixir_ex{version=Counter} = S3,
{{with, [{version, Counter} | Meta], EExprs ++ [[{do, EDo} | EOpts]]},
S3#elixir_ex{version=Counter+1}, E}.
expand_with({'<-', Meta, [Left, Right]}, {S, E, HasMatch}) ->
{ERight, SR, ER} = elixir_expand:expand(Right, S, E),
@@ -535,8 +540,8 @@ warn_catch_before_rescue([_ | Rest], Meta, E, Found) ->
warn_catch_before_rescue(Rest, Meta, E, Found).
origin(Meta, Default) ->
case lists:keyfind(origin, 1, Meta) of
{origin, Origin} -> Origin;
case lists:keyfind(definition, 1, Meta) of
{definition, Definition} -> Definition;
false -> Default
end.
+5 -3
View File
@@ -98,9 +98,11 @@ do_is_purgeable(<<_:4/binary, Size:32, Beam/binary>>) ->
do_is_purgeable(Rest).
dispatch(Module, Fun, Args, Purgeable) ->
Res = Module:Fun(Args),
return_compiler_module(Module, Purgeable),
Res.
try
Module:Fun(Args)
after
return_compiler_module(Module, Purgeable)
end.
code_fun(nil) -> '__FILE__';
code_fun(_) -> '__MODULE__'.
+10 -10
View File
@@ -247,7 +247,7 @@ def_to_clauses(_Kind, Meta, Args, Guards, [{do, Body}], _E) ->
def_to_clauses(Kind, Meta, Args, Guards, Body, E) ->
case is_list(Body) andalso lists:keyfind(do, 1, Body) of
{do, _} ->
[{Meta, Args, Guards, {'try', [{origin, Kind} | Meta], [Body]}}];
[{Meta, Args, Guards, {'try', [{definition, Kind} | Meta], [Body]}}];
false ->
elixir_errors:file_error(Meta, E, elixir_expand, {missing_option, Kind, [do]})
end.
@@ -295,17 +295,17 @@ store_definition(CheckClauses, Kind, Meta, Name, Arity, File, Module, Defaults,
%% Handling of defaults
unpack_defaults(Kind, Meta, Name, Args, S, E) ->
{Expanded, #elixir_ex{unused={_, VersionOffset}}} = expand_defaults(Args, S, E#{context := nil}, []),
unpack_expanded(Kind, Meta, Name, Expanded, VersionOffset, [], []).
{Expanded, #elixir_ex{version=Counter}} = expand_defaults(Args, S, E#{context := nil}, []),
unpack_expanded(Kind, Meta, Name, Expanded, Counter, [], []).
unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, VersionOffset, Acc, Clauses) ->
Base = match_defaults(Acc, length(Acc) + VersionOffset, []),
{Args, Invoke} = extract_defaults(List, length(Base) + VersionOffset, [], []),
unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, Counter, Acc, Clauses) ->
Base = match_defaults(Acc, length(Acc) + Counter, []),
{Args, Invoke} = extract_defaults(List, length(Base) + Counter, [], []),
Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}}, {default, true} | DefaultMeta], Base ++ Invoke}},
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [Expr | Acc], [Clause | Clauses]);
unpack_expanded(Kind, Meta, Name, [H | T], VersionOffset, Acc, Clauses) ->
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [H | Acc], Clauses);
unpack_expanded(_Kind, _Meta, _Name, [], _VersionOffset, Acc, Clauses) ->
unpack_expanded(Kind, Meta, Name, T, Counter, [Expr | Acc], [Clause | Clauses]);
unpack_expanded(Kind, Meta, Name, [H | T], Counter, Acc, Clauses) ->
unpack_expanded(Kind, Meta, Name, T, Counter, [H | Acc], Clauses);
unpack_expanded(_Kind, _Meta, _Name, [], _Counter, Acc, Clauses) ->
{lists:reverse(Acc), lists:reverse(Clauses)}.
expand_defaults([{'\\\\', Meta, [Expr, Default]} | Args], S, E, Acc) ->
+2 -2
View File
@@ -349,7 +349,7 @@ prune_stacktrace([{_, _, [Caller | _], _} | _], _MFA, Info, {ok, Caller}) ->
prune_stacktrace([{M, F, A, _} | _], {M, F, A}, Info, _E) ->
Info;
%% We've reached the elixir_dispatch internals, skip it with the rest
prune_stacktrace([{Mod, _, _, _} | _], _MFA, Info, _E) when Mod == elixir_dispatch; Mod == elixir_exp ->
prune_stacktrace([{Mod, _, _, _} | _], _MFA, Info, _E) when Mod == elixir_dispatch; Mod == elixir_expand ->
Info;
prune_stacktrace([H | T], MFA, Info, E) ->
[H | prune_stacktrace(T, MFA, Info, E)];
@@ -363,7 +363,7 @@ format_error({import, {conflict, Receiver}, Name, Arity}) ->
"please rename the local macro or remove the conflicting import",
[Name, Arity, elixir_aliases:inspect(Receiver), Name, Arity]);
format_error({import, {ambiguous, [Mod1, Mod2 | _]}, Name, Arity}) ->
io_lib:format("function ~ts/~B imported from both ~ts and ~ts, call is ambiguous",
io_lib:format("conflicting ~ts/~B import from modules ~ts and ~ts",
[Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]);
format_error({compile_env, Name, Arity}) ->
io_lib:format("Application.~s/~B is discouraged in the module body, use Application.compile_env/3 instead", [Name, Arity]);
+8 -8
View File
@@ -56,12 +56,12 @@ env_to_ex(#{context := match, versioned_vars := Vars}) ->
#elixir_ex{
prematch={Vars, {#{}, []}, Counter},
vars={Vars, false},
unused={#{}, Counter}
version=Counter
};
env_to_ex(#{versioned_vars := Vars}) ->
#elixir_ex{
vars={Vars, false},
unused={#{}, map_size(Vars)}
version=map_size(Vars)
}.
%% VAR HANDLING
@@ -94,10 +94,10 @@ merge_vars(V1, V2) ->
%% UNUSED VARS
reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) ->
S#elixir_ex{unused={#{}, Version}}.
reset_unused_vars(#elixir_ex{} = S) ->
S#elixir_ex{unused=#{}}.
check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) ->
check_unused_vars(#elixir_ex{unused=Unused}, E) ->
[elixir_errors:file_warn(calculate_span(Meta, Name), E, ?MODULE, {unused_var, Name, Overridden}) ||
{{{Name, _Kind}, _Count}, {Meta, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
E.
@@ -111,10 +111,10 @@ calculate_span(Meta, Name) ->
Meta
end.
merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _Version}}, E) ->
#elixir_ex{unused={ClauseUnused, Version}} = S,
merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused=Unused}, E) ->
#elixir_ex{unused=ClauseUnused} = S,
NewUnused = merge_and_check_unused_vars(Read, Unused, ClauseUnused, E),
S#elixir_ex{unused={NewUnused, Version}, vars={Read, Write}}.
S#elixir_ex{unused=NewUnused, vars={Read, Write}}.
merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
maps:fold(fun
+1 -1
View File
@@ -11,7 +11,7 @@
-define(typespecs, 'Elixir.Kernel.Typespec').
checker_version() ->
elixir_checker_v7.
elixir_checker_v8.
%% debug_info callback
+1
View File
@@ -138,6 +138,7 @@ format_warnings(Opts, Warnings) ->
%% Those we implement ourselves
handle_file_warning(_, _File, {_Line, v3_core, {map_key_repeated, _}}) -> ok;
handle_file_warning(_, _File, {_Line, sys_core_fold, {ignored, useless_building}}) -> ok;
handle_file_warning(_, _File, {_Line, sys_core_fold,{failed,{eval_failure,_,_}}}) -> ok;
%% We skip all of no_match related to no_clause, clause_type, guard, shadow.
%% Those have too little information and they overlap with the type system.
+4 -2
View File
@@ -318,8 +318,10 @@ no_var(ParentAnn, Elements) ->
no_var_expr(Ann, {string, _, String}) -> [{var, Ann, '_'} || _ <- String];
no_var_expr(Ann, _) -> [{var, Ann, '_'}].
no_var_size({var, _, _}) -> throw(unbound_size);
no_var_size(Size) -> Size.
no_var_size(default) -> default;
no_var_size(Size) when is_integer(Size) -> Size;
no_var_size(_) -> throw(unbound_size).
build_comprehension(Ann, Clauses, Expr, Into) ->
{comprehension_kind(Into), Ann, Expr, comprehension_clause(Clauses)}.
+42 -17
View File
@@ -230,7 +230,7 @@ translate({Name, Meta, Args}, _Ann, S) when is_atom(Name), is_list(Meta), is_lis
translate({{'.', _, [Left, Right]}, Meta, []}, _Ann, #elixir_erl{context=guard} = S)
when is_tuple(Left), is_atom(Right), is_list(Meta) ->
Ann = ?ann(Meta),
{TLeft, SL} = translate(Left, Ann, S),
{TLeft, SL} = translate(Left, Ann, S),
TRight = {atom, Ann, Right},
{?remote(Ann, erlang, map_get, [TRight, TLeft]), SL};
@@ -576,19 +576,6 @@ translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
{clause, Generated, [Var], [[Guard]], [Fast]},
{clause, Generated, [Var], [], [Slow]}
]}, VS};
translate_remote(maps, put, Meta, [Key, Value, Map], S) ->
Ann = ?ann(Meta),
case translate_args([Key, Value, Map], Ann, S) of
{[TKey, TValue, {map, _, InnerMap, Pairs}], TS} ->
{{map, Ann, InnerMap, Pairs ++ [{map_field_assoc, Ann, TKey, TValue}]}, TS};
{[TKey, TValue, {map, _, Pairs}], TS} ->
{{map, Ann, Pairs ++ [{map_field_assoc, Ann, TKey, TValue}]}, TS};
{[TKey, TValue, TMap], TS} ->
{{map, Ann, TMap, [{map_field_assoc, Ann, TKey, TValue}]}, TS}
end;
translate_remote(lists, member, Meta, [Expr, [Head | Tail] = List], S) ->
Ann = ?ann(Meta),
@@ -655,12 +642,18 @@ translate_remote(Left, Right, Meta, Args, S) ->
[TOne] -> {{op, Ann, Right, TOne}, SA};
[TOne, TTwo] -> {{op, Ann, Right, TOne, TTwo}, SA}
end;
{reorder, ErlLeft, ErlRight, FunArgs} ->
evaluate_first(Args, Ann, S, fun(ErlArgs) ->
TLeft = {atom, Ann, ErlLeft},
TRight = {atom, Ann, ErlRight},
{call, Ann, {remote, Ann, TLeft, TRight}, FunArgs(Ann, ErlArgs)}
end);
{inline_pure, Result} ->
Generated = erl_anno:set_generated(true, Ann),
translate(Result, Generated, S);
{inline_args, NewArgs} ->
{TLeft, SL} = translate(Left, Ann, S),
{TArgs, SA} = translate_args(NewArgs, Ann, SL),
{TArgs, SA} = translate_args(NewArgs, Ann, S),
TLeft = {atom, Ann, Left},
TRight = {atom, Ann, Right},
{{call, Ann, {remote, Ann, TLeft, TRight}, TArgs}, SA};
none ->
@@ -711,9 +704,41 @@ rewrite_strategy(Left, Right, Args) ->
none
end;
false ->
none
reorder_strategy(Left, Right, length(Args))
end.
-define(
reorder(ExMod, ExFun, ExArity, ExArgs, ErlMod, ErlFun, ErlArgs),
reorder_strategy(ExMod, ExFun, ExArity) -> {reorder, ErlMod, ErlFun, fun(Ann, ExArgs) -> _ = Ann, ErlArgs end}
).
?reorder('Elixir.Kernel', elem, 2, [Tuple, Index], erlang, element, [increment(Ann, Index), Tuple]);
?reorder('Elixir.Kernel', put_elem, 2, [Tuple, Index, Term], erlang, element, [increment(Ann, Index), Tuple, Term]);
?reorder('Elixir.Kernel', is_map_key, 2, [Map, Key], erlang, is_map_key, [Key, Map]);
?reorder('Elixir.Map', delete, 2, [Map, Key], maps, remove, [Key, Map]);
?reorder('Elixir.Map', fetch, 2, [Map, Key], maps, find, [Key, Map]);
?reorder('Elixir.Map', 'fetch!', 2, [Map, Key], maps, get, [Key, Map]);
?reorder('Elixir.Map', 'has_key?', 2, [Map, Key], maps, is_key, [Key, Map]);
?reorder('Elixir.Map', put, 3, [Map, Key, Value], maps, put, [Key, Value, Map]);
?reorder('Elixir.Map', 'replace!', 3, [Map, Key, Value], maps, update, [Key, Value, Map]);
?reorder('Elixir.Process', group_leader, 2, [Pid, Leader], erlang, group_leader, [Leader, Pid]);
?reorder('Elixir.Tuple', delete_at, 2, [Tuple, Index], erlang, delete_element, [increment(Ann, Index), Tuple]);
?reorder('Elixir.Tuple', duplicate, 2, [Data, Size], erlang, make_tuple, [Size, Data]);
?reorder('Elixir.Tuple', insert_at, 3, [Tuple, Index, Term], erlang, insert_element, [increment(Ann, Index), Tuple, Term]);
reorder_strategy(_, _, _) -> none.
increment(_Ann, {integer, Ann, Number}) when is_number(Number) -> {integer, Ann, Number + 1};
increment(Ann, Other) -> {op, Ann, '+', Other, {integer, Ann, 1}}.
evaluate_first([{Var, _Meta, Ctx} | _Tail] = Args, Ann, S, Fun) when is_atom(Var), is_atom(Ctx) ->
{TArgs, ST} = translate_args(Args, Ann, S),
{Fun(TArgs), ST};
evaluate_first(Args, Ann, S, Fun) ->
{VarName, SV} = elixir_erl_var:build('_', S),
{[Head | Tail], ST} = translate_args(Args, Ann, SV),
Var = {var, Ann, VarName},
{{block, Ann, [{match, Ann, Var, Head}, Fun([Var | Tail])]}, ST}.
inline_pure_function('Elixir.Duration', 'new!') -> true;
inline_pure_function('Elixir.MapSet', new) -> true;
inline_pure_function('Elixir.String', length) -> true;
+6 -2
View File
@@ -117,7 +117,7 @@ rescue_guards(Meta, Var, Aliases) ->
ElixirGuards =
[erl_and(Meta,
{erl(Meta, '=='), Meta, [{erl(Meta, map_get), Meta, ['__struct__', Var]}, Alias]},
{erl(Meta, map_get), Meta, ['__exception__', Var]}
{erl(Meta, is_map_key), Meta, ['__exception__', Var]}
) || Alias <- Aliases],
{ElixirGuards ++ ErlangGuards, ErlangAliases}.
@@ -214,7 +214,11 @@ erl_rescue_guard_for(Meta, Var, 'Elixir.ErlangError') ->
erl_and(
Meta,
{erl(Meta, is_map), Meta, [Var]},
{erl(Meta, is_map_key), Meta, ['__exception__', Var]}
erl_and(
Meta,
{erl(Meta, is_map_key), Meta, ['__struct__', Var]},
{erl(Meta, is_map_key), Meta, ['__exception__', Var]}
)
),
{erl(Meta, 'not'), Meta, [Condition]};
+2 -2
View File
@@ -45,7 +45,7 @@ build(Key, #elixir_erl{counter=Counter} = S) ->
{build_name(Key, Count),
S#elixir_erl{counter=Counter#{Key => Count}}}.
build_name('_', Count) -> list_to_atom("_@" ++ integer_to_list(Count));
build_name('_', Count) -> list_to_atom("_" ++ integer_to_list(Count));
build_name(Name, Count) -> list_to_atom("_" ++ atom_to_list(Name) ++ "@" ++ integer_to_list(Count)).
%% BINDINGS
@@ -102,7 +102,7 @@ load_pair({Pair, Value}) -> {Pair, Value}.
dump_binding(Binding, ErlS, ExS, PruneBefore) ->
#elixir_erl{var_names=ErlVars} = ErlS,
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
#elixir_ex{vars={ExVars, _}, unused=Unused} = ExS,
maps:fold(fun
({Var, Kind} = Pair, Version, {B, V})
+1 -1
View File
@@ -317,7 +317,7 @@ compile_error(Meta, File, Message) when is_list(Message) ->
%% Tokenization parsing/errors.
-spec parse_error(elixir:keyword(), binary() | {binary(), binary()},
binary(), binary(), {unicode:charlist(), integer(), integer()}) -> no_return().
binary(), binary(), {unicode:charlist(), integer(), integer(), non_neg_integer()}) -> no_return().
parse_error(Location, File, Error, <<>>, Input) ->
Message = case Error of
<<"syntax error before: ">> -> <<"syntax error: expression is incomplete">>;
+26 -19
View File
@@ -331,7 +331,8 @@ expand({'cond', Meta, [Opts]}, S, E) ->
assert_no_match_or_guard_scope(Meta, "cond", S, E),
assert_no_underscore_clause_in_cond(Opts, E),
{EClauses, SC, EC} = elixir_clauses:'cond'(Meta, Opts, S, E),
{{'cond', Meta, [EClauses]}, SC, EC};
#elixir_ex{version=Counter} = SC,
{{'cond', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC};
expand({'case', Meta, [Expr, Options]}, S, E) ->
assert_no_match_or_guard_scope(Meta, "case", S, E),
@@ -340,12 +341,14 @@ expand({'case', Meta, [Expr, Options]}, S, E) ->
expand({'receive', Meta, [Opts]}, S, E) ->
assert_no_match_or_guard_scope(Meta, "receive", S, E),
{EClauses, SC, EC} = elixir_clauses:'receive'(Meta, Opts, S, E),
{{'receive', Meta, [EClauses]}, SC, EC};
#elixir_ex{version=Counter} = SC,
{{'receive', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC};
expand({'try', Meta, [Opts]}, S, E) ->
assert_no_match_or_guard_scope(Meta, "try", S, E),
{EClauses, SC, EC} = elixir_clauses:'try'(Meta, Opts, S, E),
{{'try', Meta, [EClauses]}, SC, EC};
#elixir_ex{version=Counter} = SC,
{{'try', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC};
%% Comprehensions
@@ -385,20 +388,22 @@ expand({'^', Meta, [Arg]}, S, E) ->
function_error(Meta, E, ?MODULE, {pin_outside_of_match, Arg}),
{{'^', Meta, [Arg]}, S#elixir_ex{tainted_function=true}, E};
expand({'_', Meta, Kind} = Var, S, #{context := Context} = E) when is_atom(Kind) ->
expand({'_', Meta, Kind}, #elixir_ex{version=Counter} = S, #{context := Context} = E) when is_atom(Kind) ->
NewVar = {'_', [{version, Counter} | Meta], Kind},
case Context of
match ->
{Var, S, E};
{NewVar, S#elixir_ex{version=Counter+1}, E};
_ ->
function_error(Meta, E, ?MODULE, unbound_underscore),
{Var, S#elixir_ex{tainted_function=true}, E}
{NewVar, S#elixir_ex{tainted_function=true, version=Counter+1}, E}
end;
expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_atom(Kind) ->
#elixir_ex{
prematch={_, _, PrematchVersion},
unused={Unused, Version},
vars={Read, Write}
vars={Read, Write},
unused=Unused
} = S,
Pair = {Name, elixir_utils:var_context(Meta, Kind)},
@@ -408,29 +413,31 @@ expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_at
#{Pair := VarVersion} when VarVersion >= PrematchVersion ->
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E),
NewUnused = var_used(Pair, Meta, VarVersion, Unused),
NewWrite = (Write /= false) andalso Write#{Pair => Version},
NewWrite = (Write /= false) andalso Write#{Pair => VarVersion},
Var = {Name, [{version, VarVersion} | Meta], Kind},
{Var, S#elixir_ex{vars={Read, NewWrite}, unused={NewUnused, Version}}, E};
{Var, S#elixir_ex{vars={Read, NewWrite}, unused=NewUnused}, E};
%% Variable is being overridden now
#{Pair := _} ->
Version = S#elixir_ex.version,
NewUnused = var_unused(Pair, Meta, Version, Unused, true),
NewRead = Read#{Pair => Version},
NewWrite = (Write /= false) andalso Write#{Pair => Version},
Var = {Name, [{version, Version} | Meta], Kind},
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused={NewUnused, Version + 1}}, E};
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused=NewUnused, version=Version + 1}, E};
%% Variable defined for the first time
_ ->
Version = S#elixir_ex.version,
NewUnused = var_unused(Pair, Meta, Version, Unused, false),
NewRead = Read#{Pair => Version},
NewWrite = (Write /= false) andalso Write#{Pair => Version},
Var = {Name, [{version, Version} | Meta], Kind},
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused={NewUnused, Version + 1}}, E}
{Var, S#elixir_ex{vars={NewRead, NewWrite}, unused=NewUnused, version=Version + 1}, E}
end;
expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
#elixir_ex{vars={Read, _Write}, unused={Unused, Version}, prematch=Prematch} = S,
#elixir_ex{vars={Read, _Write}, unused=Unused, prematch=Prematch} = S,
Pair = {Name, elixir_utils:var_context(Meta, Kind)},
Result =
@@ -471,7 +478,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
{ok, PairVersion} ->
maybe_warn_underscored_var_access(Meta, Name, Kind, E),
Var = {Name, [{version, PairVersion} | Meta], Kind},
{Var, S#elixir_ex{unused={var_used(Pair, Meta, PairVersion, Unused), Version}}, E};
{Var, S#elixir_ex{unused=var_used(Pair, Meta, PairVersion, Unused)}, E};
Error ->
case lists:keyfind(if_undefined, 1, Meta) of
@@ -795,8 +802,8 @@ expand_case(Meta, Expr, Opts, S, E) ->
false -> Opts
end,
{EOpts, SO, EO} = elixir_clauses:'case'(Meta, ROpts, SE, EE),
{{'case', Meta, [EExpr, EOpts]}, SO, EO}.
{EOpts, #elixir_ex{version=Counter} = SO, EO} = elixir_clauses:'case'(Meta, ROpts, SE, EE),
{{'case', [{version, Counter} | Meta], [EExpr, EOpts]}, SO#elixir_ex{version=Counter+1}, EO}.
rewrite_case_clauses([{do, [
{'->', FalseMeta, [
@@ -853,9 +860,9 @@ expand_for({for, Meta, [_ | _] = Args}, S, E, Return) ->
{error, Error} -> {file_error(Meta, E, ?MODULE, Error), EOpts}
end,
{{for, Meta, ECases ++ [[{do, EExpr} | NormalizedOpts]]},
elixir_env:merge_and_check_unused_vars(SE, S, EE),
E}.
#elixir_ex{version=Counter} = SF = elixir_env:merge_and_check_unused_vars(SE, S, EE),
{{for, [{version, Counter} | Meta], ECases ++ [[{do, EExpr} | NormalizedOpts]]},
SF#elixir_ex{version=Counter+1}, E}.
validate_for_options([{into, _} = Pair | Opts], _Into, Uniq, Reduce, Return, Meta, E, Acc) ->
validate_for_options(Opts, Pair, Uniq, Reduce, Return, Meta, E, [Pair | Acc]);
+3 -2
View File
@@ -29,7 +29,8 @@ expand(Meta, Clauses, S, E) when is_list(Clauses) ->
case lists:usort(EArities) of
[_] ->
{{fn, Meta, EClauses}, SE, E};
#elixir_ex{version=Counter} = SE,
{{fn, [{version, Counter} | Meta], EClauses}, SE#elixir_ex{version=Counter+1}, E};
_ ->
file_error(Meta, E, ?MODULE, clauses_with_different_arities)
end.
@@ -154,7 +155,7 @@ escape({'&', Meta, [Pos]}, E, Dict) when is_integer(Pos), Pos > 0 ->
{Var, Dict};
error ->
Next = elixir_module:next_counter(?key(E, module)),
Var = {capture, [{counter, Next}, {capture, Pos} | Meta], nil},
Var = {'_&', [{counter, Next}, {capture, Pos} | Meta], ?MODULE},
{Var, orddict:store(Pos, Var, Dict)}
end;
escape({'&', Meta, [Pos]}, E, _Dict) when is_integer(Pos) ->
+3 -2
View File
@@ -194,7 +194,8 @@ is_sigil({Name, 2}) ->
[H|T] when H >= $A, H =< $Z ->
lists:all(fun(L) -> (L >= $0 andalso L =< $9)
orelse (L>= $A andalso L =< $Z)
end, T)
end, T);
_ -> false
end;
_ ->
false
@@ -247,7 +248,7 @@ format_error({invalid_import, {Receiver, Name, Arity}}) ->
[elixir_aliases:inspect(Receiver), Name, Arity]);
format_error({invalid_option, only, Value}) ->
Message = "invalid :only option for import, expected value to be an atom :functions, :macros"
Message = "invalid :only option for import, expected value to be an atom :functions, :macros, :sigils"
", or a literal keyword list of function names with arity as values, got: ~s",
io_lib:format(Message, ['Elixir.Macro':to_string(Value)]);
+4 -1
View File
@@ -33,6 +33,9 @@ extract([$\\, $\r, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol,
extract([$\\, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
extract_nl(Rest, [$\n, $\\ | Buffer], Output, Line, Scope, Interpol, Last);
extract([$\r, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
extract_nl(Rest, [$\n, $\r | Buffer], Output, Line, Scope, Interpol, Last);
extract([$\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
extract_nl(Rest, [$\n | Buffer], Output, Line, Scope, Interpol, Last);
@@ -81,7 +84,7 @@ extract([$\\ | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last) ->
%% Catch all clause
extract([Char1, Char2 | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last)
when Char1 =< 255, Char2 =< 255 ->
when Char1 =< 255, Char2 =< 255, not (?break(Char1)) ->
extract([Char2 | Rest], [Char1 | Buffer], Output, Line, Column + 1, Scope, Interpol, Last);
extract(Rest, Buffer, Output, Line, Column, Scope, Interpol, Last) ->
+32 -27
View File
@@ -3,7 +3,7 @@
%% SPDX-FileCopyrightText: 2012 Plataformatec
-module(elixir_map).
-export([expand_map/4, expand_struct/5, format_error/1, maybe_load_struct_info/3]).
-export([expand_map/4, expand_struct/5, format_error/1, maybe_load_struct_info/4]).
-import(elixir_errors, [function_error/4, file_error/4, file_warn/4]).
-include("elixir.hrl").
@@ -126,7 +126,7 @@ validate_struct(Atom, _) when is_atom(Atom) -> true;
validate_struct(_, _) -> false.
assert_struct_info_if_not_function(Meta, Name, Assocs, #{function := nil} = E) ->
case maybe_load_struct_info(Meta, Name, E) of
case maybe_load_struct_info(Meta, Name, hard, E) of
{ok, Info} ->
[lists:any(fun(Field) -> ?key(Field, field) =:= Key end, Info) orelse
function_error(Meta, E, ?MODULE, {unknown_key_for_struct, Name, Key})
@@ -139,13 +139,22 @@ assert_struct_info_if_not_function(Meta, Name, Assocs, #{function := nil} = E) -
assert_struct_info_if_not_function(_Meta, _Name, _Assocs, _E) ->
ok.
maybe_load_struct_info(Meta, Name, E) ->
maybe_load_struct_info(Meta, Name, Mode, E) ->
try
case is_open(Name, Meta, E) andalso lookup_struct_info_from_data_tables(Name) of
Lookup = in_context(Name, E) orelse is_compiling_struct(Meta, Name, Mode, E),
case lookup_struct_info_from_data_tables(Name) of
%% If I am accessing myself and there is no attribute,
%% don't invoke the fallback to avoid calling loaded code.
false when ?key(E, module) =:= Name -> nil;
false -> Name:'__info__'(struct);
false ->
%% If we attempted to lookup a definition, we always invoke it, to deal
%% with potential race conditions about the table being deleted at the
%% time we were looking up.
case Lookup orelse erlang:module_loaded(Name) of
true -> Name:'__info__'(struct);
false -> nil
end;
InfoList -> InfoList
end
of
@@ -165,20 +174,8 @@ lookup_struct_info_from_data_tables(Module) ->
load_struct(Meta, Name, Assocs, E) ->
try
maybe_load_struct(Meta, Name, Assocs, E)
of
{ok, Struct} -> Struct;
{error, Desc} -> file_error(Meta, E, ?MODULE, Desc)
catch
Kind:Reason ->
Info = [{Name, '__struct__', 1, [{file, "expanding struct"}]},
elixir_utils:caller(?line(Meta), ?key(E, file), ?key(E, module), ?key(E, function))],
erlang:raise(Kind, Reason, Info)
end.
maybe_load_struct(Meta, Name, Assocs, E) ->
try
case is_open(Name, Meta, E) andalso elixir_def:external_for(Meta, Name, '__struct__', 1, [def]) of
case (in_context(Name, E) orelse is_compiling_struct(Meta, Name, hard, E)) andalso
elixir_def:external_for(Meta, Name, '__struct__', 1, [def]) of
%% If I am accessing myself and there is no __struct__ function,
%% don't invoke the fallback to avoid calling loaded code.
false when ?key(E, module) =:= Name ->
@@ -207,15 +204,23 @@ maybe_load_struct(Meta, Name, Assocs, E) ->
[maps:is_key(Key, Struct) orelse
function_error(Meta, E, ?MODULE, {unknown_key_for_struct, Name, Key})
|| {Key, _} <- Assocs],
{ok, Struct};
Struct;
#{'__struct__' := StructName} when is_atom(StructName) ->
{error, {struct_name_mismatch, Name, StructName}};
Desc = {struct_name_mismatch, Name, StructName},
file_error(Meta, E, ?MODULE, Desc);
Other ->
{error, {invalid_struct_return_value, Name, Other}}
Desc = {invalid_struct_return_value, Name, Other},
file_error(Meta, E, ?MODULE, Desc)
catch
error:undef -> {error, struct_undef(Name, E)}
error:undef ->
file_error(Meta, E, ?MODULE, struct_undef(Name, E));
Kind:Reason ->
Info = [{Name, '__struct__', 1, [{file, "expanding struct"}]},
elixir_utils:caller(?line(Meta), ?key(E, file), ?key(E, module), ?key(E, function))],
erlang:raise(Kind, Reason, Info)
end.
assert_and_trace_struct_assocs(Meta, Name, Assocs, E) ->
@@ -226,17 +231,17 @@ assert_and_trace_struct_assocs(Meta, Name, Assocs, E) ->
elixir_env:trace({struct_expansion, Meta, Name, Keys}, E),
Keys.
is_open(Name, Meta, E) ->
in_context(Name, E) orelse ((code:ensure_loaded(Name) /= {module, Name}) andalso wait_for_struct(Name, Meta, E)).
is_compiling_struct(Meta, Name, Mode, E) ->
(code:ensure_loaded(Name) /= {module, Name}) andalso wait_for_struct(Meta, Name, Mode, E).
in_context(Name, E) ->
%% We also include the current module because it won't be present
%% in context module in case the module name is defined dynamically.
lists:member(Name, [?key(E, module) | ?key(E, context_modules)]).
wait_for_struct(Module, Meta, E) ->
wait_for_struct(Meta, Module, Mode, E) ->
(erlang:get(elixir_compiler_info) /= undefined) andalso
('Elixir.Kernel.ErrorHandler':ensure_compiled(Module, struct, hard, elixir_utils:get_line(Meta, E)) =:= found).
('Elixir.Kernel.ErrorHandler':ensure_compiled(Module, struct, Mode, elixir_utils:get_line(Meta, E)) =:= found).
struct_undef(Name, E) ->
case in_context(Name, E) andalso (?key(E, function) == nil) of
+12 -6
View File
@@ -159,6 +159,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
{DataSet, DataBag} = Tables,
try
CompilerInfo = erlang:get(elixir_compiler_info),
put_compiler_modules([Module | CompilerModules]),
{Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E),
CheckerInfo = checker_info(),
@@ -166,6 +167,11 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
{Binary, PersistedAttributes, Autoload} =
elixir_erl_compiler:spawn(fun() ->
case CompilerInfo of
undefined -> undefined;
_ -> erlang:put(elixir_compiler_info, CompilerInfo)
end,
PersistedAttributes = ets:lookup_element(DataBag, persisted_attributes, 2),
Attributes = attributes(DataSet, DataBag, PersistedAttributes),
AllDefinitions = elixir_def:fetch_definitions(Module, E),
@@ -494,7 +500,7 @@ maybe_prune_versioned_vars(false, _Vars, _Exs, E) ->
E;
maybe_prune_versioned_vars(true, Vars, ExS, E) ->
PruneBefore = length(Vars),
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
#elixir_ex{vars={ExVars, _}, unused=Unused} = ExS,
VersionedVars =
maps:filter(fun
@@ -574,7 +580,7 @@ bag_lookup_element(Table, Name, Pos) ->
end.
beam_location(ModuleAsCharlist) ->
case get(elixir_compiler_dest) of
case erlang:get(elixir_compiler_dest) of
{Dest, Forceload} when is_binary(Dest) ->
{filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"),
Forceload};
@@ -585,7 +591,7 @@ beam_location(ModuleAsCharlist) ->
%% Integration with elixir_compiler that makes the module available
checker_info() ->
case get(elixir_checker_info) of
case erlang:get(elixir_checker_info) of
undefined -> {self(), nil};
_ -> 'Elixir.Module.ParallelChecker':get()
end.
@@ -601,19 +607,19 @@ spawn_parallel_checker(CheckerInfo, Module, ModuleMap, Signatures, BeamLocation)
'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Signatures, BeamLocation, Log).
make_module_available(Module, Binary, Loaded) ->
case get(elixir_module_binaries) of
case erlang:get(elixir_module_binaries) of
Current when is_list(Current) ->
put(elixir_module_binaries, [{Module, Binary} | Current]);
_ ->
ok
end,
case get(elixir_compiler_info) of
case erlang:get(elixir_compiler_info) of
undefined ->
ok;
{PID, _} ->
Ref = make_ref(),
PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary, Loaded},
PID ! {module_available, self(), Ref, erlang:get(elixir_compiler_file), Module, Binary, Loaded},
receive {Ref, ack} -> ok end
end.
+5 -3
View File
@@ -55,7 +55,7 @@ end;
has_unquotes({{'.', _, [_, unquote]}, _, [_]}, _) -> true;
has_unquotes({Var, _, Ctx}, _) when is_atom(Var), is_atom(Ctx) -> false;
has_unquotes({Name, _, Args}, QuoteLevel) when is_list(Args) ->
has_unquotes(Name) orelse lists:any(fun(Child) -> has_unquotes(Child, QuoteLevel) end, Args);
has_unquotes(Name, QuoteLevel) orelse lists:any(fun(Child) -> has_unquotes(Child, QuoteLevel) end, Args);
has_unquotes({Left, Right}, QuoteLevel) ->
has_unquotes(Left, QuoteLevel) orelse has_unquotes(Right, QuoteLevel);
has_unquotes(List, QuoteLevel) when is_list(List) ->
@@ -583,8 +583,10 @@ tail_list(Left, Right, Tail) when is_list(Right), is_list(Tail) ->
tail_list(Left, Right, Tail) when is_list(Left) ->
validate_list(Left),
[H | T] = lists:reverse(Tail ++ Left),
lists:reverse([{'|', [], [H, Right]} | T]).
case lists:reverse(Tail ++ Left) of
[H | T] -> lists:reverse([{'|', [], [H, Right]} | T]);
[] -> argument_error(<<"unquote_splicing/1 failed because it attempted to splice an empty list before tail position">>)
end.
validate_list(List) ->
case valid_ast_list(List) of
+13 -36
View File
@@ -238,8 +238,8 @@ inner_inline(_, _, _, _) -> false.
%%
%% Rewrite rules are more complex than regular inlining code
%% as they may change the number of arguments. However, they
%% don't add new code (such as case expressions), at best they
%% perform dead code removal.
%% cannot change tha argument order, as that can change the
%% code semantics.
rewrite(?string_chars, DotMeta, to_string, Meta, [Arg]) ->
case is_always_string(Arg) of
true -> Arg;
@@ -253,48 +253,21 @@ rewrite(Receiver, DotMeta, Right, Meta, Args) ->
?rewrite(?float, to_charlist, [Arg], erlang, float_to_list, [Arg, [short]]);
?rewrite(?float, to_string, [Arg], erlang, float_to_binary, [Arg, [short]]);
?rewrite(?kernel, is_map_key, [Map, Key], erlang, is_map_key, [Key, Map]);
?rewrite(?map, delete, [Map, Key], maps, remove, [Key, Map]);
?rewrite(?map, fetch, [Map, Key], maps, find, [Key, Map]);
?rewrite(?map, 'fetch!', [Map, Key], maps, get, [Key, Map]);
?rewrite(?map, 'has_key?', [Map, Key], maps, is_key, [Key, Map]);
?rewrite(?map, put, [Map, Key, Value], maps, put, [Key, Value, Map]);
?rewrite(?map, 'replace!', [Map, Key, Value], maps, update, [Key, Value, Map]);
?rewrite(?port, monitor, [Arg], erlang, monitor, [port, Arg]);
?rewrite(?process, group_leader, [Pid, Leader], erlang, group_leader, [Leader, Pid]);
?rewrite(?process, monitor, [Arg], erlang, monitor, [process, Arg]);
?rewrite(?process, monitor, [Arg, Opts], erlang, monitor, [process, Arg, Opts]);
?rewrite(?process, send_after, [Dest, Msg, Time], erlang, send_after, [Time, Dest, Msg]);
?rewrite(?process, send_after, [Dest, Msg, Time, Opts], erlang, send_after, [Time, Dest, Msg, Opts]);
?rewrite(?tuple, duplicate, [Data, Size], erlang, make_tuple, [Size, Data]);
inner_rewrite(ex_to_erl, Meta, ?tuple, delete_at, [Tuple, Index]) ->
{erlang, delete_element, [increment(Meta, Index), Tuple]};
inner_rewrite(ex_to_erl, Meta, ?tuple, insert_at, [Tuple, Index, Term]) ->
{erlang, insert_element, [increment(Meta, Index), Tuple, Term]};
inner_rewrite(ex_to_erl, Meta, ?kernel, elem, [Tuple, Index]) ->
{erlang, element, [increment(Meta, Index), Tuple]};
inner_rewrite(ex_to_erl, Meta, ?kernel, put_elem, [Tuple, Index, Value]) ->
{erlang, setelement, [increment(Meta, Index), Tuple, Value]};
inner_rewrite(erl_to_ex, _Meta, erlang, delete_element, [Index, Tuple]) when is_number(Index) ->
{?tuple, delete_at, [Tuple, Index - 1], fun([Index, Tuple]) -> [Tuple, Index] end};
inner_rewrite(erl_to_ex, _Meta, erlang, insert_element, [Index, Tuple, Term]) when is_number(Index) ->
{?tuple, insert_at, [Tuple, Index - 1, Term], fun([Index, Tuple, Term]) -> [Tuple, Index, Term] end};
%% These are guard exclusive rewrites
inner_rewrite(erl_to_ex, _Meta, erlang, is_map_key, [Key, Map]) ->
{?kernel, is_map_key, [Map, Key], fun([Key, Map]) -> [Map, Key] end};
inner_rewrite(erl_to_ex, _Meta, erlang, element, [Index, Tuple]) when is_number(Index) ->
{?kernel, elem, [Tuple, Index - 1], fun([Index, Tuple]) -> [Tuple, Index] end};
inner_rewrite(erl_to_ex, _Meta, erlang, setelement, [Index, Tuple, Term]) when is_number(Index) ->
{?kernel, put_elem, [Tuple, Index - 1, Term], fun([Index, Tuple, Term]) -> [Tuple, Index, Term] end};
inner_rewrite(erl_to_ex, _Meta, erlang, delete_element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple]) ->
{?tuple, delete_at, [Tuple, Index], fun([Index, Tuple]) -> [Tuple, Index] end};
inner_rewrite(erl_to_ex, _Meta, erlang, insert_element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple, Term]) ->
{?tuple, insert_at, [Tuple, Index, Term], fun([Index, Tuple, Term]) -> [Tuple, Index, Term] end};
inner_rewrite(erl_to_ex, _Meta, erlang, element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple]) ->
{?kernel, elem, [Tuple, Index], fun([Index, Tuple]) -> [Tuple, Index] end};
inner_rewrite(erl_to_ex, _Meta, erlang, setelement, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple, Term]) ->
{?kernel, put_elem, [Tuple, Index, Term], fun([Index, Tuple, Term]) -> [Tuple, Index, Term] end};
inner_rewrite(erl_to_ex, _Meta, erlang, 'orelse', [_, _] = Args) ->
{?kernel, 'or', Args, fun identity/1};
inner_rewrite(erl_to_ex, _Meta, erlang, 'andalso', [_, _] = Args) ->
@@ -305,10 +278,8 @@ inner_rewrite(erl_to_ex, _Meta, Mod, Fun, Args) -> {Mod, Fun, Args, fun identity
identity(Arg) -> Arg.
increment(_Meta, Number) when is_number(Number) ->
Number + 1;
increment(Meta, Other) ->
{{'.', Meta, [erlang, '+']}, Meta, [Other, 1]}.
increment(_Meta, Number) when is_number(Number) -> Number + 1;
increment(Meta, Other) -> {{'.', Meta, [erlang, '+']}, Meta, [Other, 1]}.
%% Match rewrite
%%
@@ -337,6 +308,12 @@ static_append(_, _, _) -> throw(impossible).
%% Guard rewrite is similar to regular rewrite, except
%% it also verifies the resulting function is supported in
%% guard context - only certain BIFs and operators are.
guard(?kernel, DotMeta, is_map_key, Meta, [Map, Key], _S) ->
{ok, {{'.', DotMeta, [erlang, is_map_key]}, Meta, [Key, Map]}};
guard(?kernel, DotMeta, elem, Meta, [Tuple, Index], _S) ->
{ok, {{'.', DotMeta, [erlang, element]}, Meta, [increment(Meta, Index), Tuple]}};
guard(?kernel, DotMeta, put_elem, Meta, [Tuple, Index, Value], _S) ->
{ok, {{'.', DotMeta, [erlang, setelement]}, Meta, [increment(Meta, Index), Tuple, Value]}};
guard(Receiver, DotMeta, Right, Meta, Args, S) ->
case inner_rewrite(ex_to_erl, DotMeta, Receiver, Right, Args) of
{erlang, RRight, RArgs} ->
+10
View File
@@ -216,6 +216,11 @@ tokenize([$~, H | _T] = Original, Line, Column, Scope, Tokens) when ?is_upcase(H
% elixir_errors.erl as by default {char, _, _} tokens are "hijacked" by Erlang
% and printed with Erlang syntax ($a) in the parser's error messages.
%% Reject bare carriage return after ?\ (CR is only valid as part of CRLF line ending).
tokenize([$?, $\\, $\r | _Rest] = Original, Line, Column, Scope, Tokens) ->
Reason = {?LOC(Line, Column), "invalid bare carriage return after ?\\, use ?\\r instead: ", "\\u000D"},
error(Reason, Original, Scope, Tokens);
tokenize([$?, $\\, H | T], Line, Column, Scope, Tokens) ->
Char = elixir_interpolation:unescape_map(H),
@@ -248,6 +253,11 @@ tokenize([$?, $\\, H | T], Line, Column, Scope, Tokens) ->
tokenize(T, Line, Column + 3, NewScope, [Token | Tokens])
end;
%% Reject bare carriage return after ? (CR is only valid as part of CRLF line ending).
tokenize([$?, $\r | _Rest] = Original, Line, Column, Scope, Tokens) ->
Reason = {?LOC(Line, Column), "invalid bare carriage return after ?, use ?\\r instead: ", "\\u000D"},
error(Reason, Original, Scope, Tokens);
tokenize([$?, Char | T], Line, Column, Scope, Tokens) ->
NewScope = case handle_char(Char) of
{Escape, Name} ->
+15 -12
View File
@@ -24,20 +24,23 @@
%% Bidirectional control
%% Retrieved from https://trojansource.codes/trojan-source.pdf
-define(bidi(C), C =:= 16#202A;
C =:= 16#202B;
C =:= 16#202D;
C =:= 16#202E;
C =:= 16#2066;
C =:= 16#2067;
C =:= 16#2068;
C =:= 16#202C;
-define(bidi(C), C =:= 16#202A orelse
C =:= 16#202B orelse
C =:= 16#202D orelse
C =:= 16#202E orelse
C =:= 16#2066 orelse
C =:= 16#2067 orelse
C =:= 16#2068 orelse
C =:= 16#202C orelse
C =:= 16#2069).
%% Unsupported newlines
%% https://www.unicode.org/reports/tr55/
-define(break(C), C =:= 16#000B;
C =:= 16#000C;
C =:= 16#0085;
C =:= 16#2028;
%% CR (0x000D) is included: a bare CR not followed by LF is rejected.
%% Call sites must handle CRLF explicitly before the ?break check.
-define(break(C), C =:= 16#000B orelse
C =:= 16#000C orelse
C =:= 16#000D orelse
C =:= 16#0085 orelse
C =:= 16#2028 orelse
C =:= 16#2029).
@@ -30,7 +30,7 @@ defmodule Code.Normalizer.FormatterASTTest do
{quoted, comments} = Code.string_to_quoted_with_comments!(good, to_quoted_opts)
to_algebra_opts = [comments: comments, escape: false] ++ opts
to_algebra_opts = Keyword.merge([comments: comments, escape: false], opts)
quoted
|> Code.quoted_to_algebra(to_algebra_opts)
@@ -289,40 +289,80 @@ defmodule Code.Normalizer.FormatterASTTest do
end
test "with escaped new lines" do
assert_same ~S'''
"""
one\
#{"two"}\
three\
"""
'''
source =
String.trim(~S'''
"""
one\
#{"two"}\
three\
"""
''')
assert_same source
# This is the same default as assert_same
assert string_to_string(source, unescape: false, escape: false) == source
# If we unescape, we cannot keep the original representation
# but we must keep it as a valid heredoc
assert string_to_string(source, unescape: true, escape: true) ==
String.trim(~S'''
"""
one#{"two"}three
"""
''')
end
test "with empty escaped new lines" do
source =
String.trim(~S'''
"""
one\
#{"two"}\
three\
"""
''')
assert_same source
# This is the same default as assert_same
assert string_to_string(source, unescape: false, escape: false) == source
# If we unescape, we cannot keep the original representation
# but we must keep it as a valid heredoc
assert string_to_string(source, unescape: true, escape: true) ==
String.trim(~S'''
"""
one#{"two"}three
"""
''')
end
end
describe "charlist heredocs" do
test "without escapes" do
test "charlist heredocs" do
ExUnit.CaptureIO.capture_io(:stderr, fn ->
# without escapes
assert_same ~S"""
~c'''
hello
'''
"""
end
test "with escapes" do
# with escapes
assert_same ~S"""
~c'''
f\a\b\ro
'''
"""
# with quotes
assert_same ~S"""
~c'''
multiple "\"" quotes
'''
"""
end
test "with interpolation" do
# with interpolation
assert_same ~S"""
~c'''
one
@@ -340,7 +380,31 @@ defmodule Code.Normalizer.FormatterASTTest do
three
'''
"""
end
# with empty escaped new lines
source =
String.trim(~S"""
'''
one\
#{"two"}\
three\
'''
""")
assert_same source
# This is the same default as assert_same
assert string_to_string(source, unescape: false, escape: false) == source
# If we unescape, we cannot keep the original representation
# but we must keep it as a valid heredoc
assert string_to_string(source, unescape: true, escape: true) ==
String.trim(~S"""
'''
one#{"two"}three
'''
""")
end)
end
describe "keyword list" do
@@ -467,6 +531,23 @@ defmodule Code.Normalizer.FormatterASTTest do
'''rsa
"""
end
test "with empty escaped new lines" do
source =
String.trim(~S"""
~c'''
one\
#{"two"}\
three\
'''
""")
assert_same source
# Heredoc in sigils always preserves newlines regardless of escape/unescape
assert string_to_string(source, unescape: false, escape: false) == source
assert string_to_string(source, unescape: true, escape: true) == source
end
end
describe "preserves comments formatting" do
+64 -2
View File
@@ -215,6 +215,33 @@ defmodule CodeTest do
end
end
test "prunes internal modules from macro expansion stacktraces" do
defmodule PruneStacktraceMacro do
defmacro bad do
quote do
receive 1
end
end
end
stacktrace =
try do
Code.eval_quoted(
quote do
require PruneStacktraceMacro
PruneStacktraceMacro.bad()
end
)
rescue
CompileError -> __STACKTRACE__
end
refute Enum.any?(
stacktrace,
&match?({mod, _, _, _} when mod in [:elixir_expand, :elixir_dispatch], &1)
)
end
test "warns when lexical tracker process is dead" do
{pid, ref} = spawn_monitor(fn -> :ok end)
assert_receive {:DOWN, ^ref, _, _, _}
@@ -261,6 +288,41 @@ defmodule CodeTest do
assert {1, _binding} = Code.eval_string("dbg(1)", [])
end)
end
test "nested eval preserves outer :dbg_callback" do
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
assert {{2, []}, _binding} =
Code.eval_string(
"""
Code.eval_string("dbg(1)")
""",
[],
opts
)
end
test "nested eval preserves outer env in exception stacktrace" do
env = %{Code.env_for_eval([]) | file: "outer_file.ex"}
stacktrace =
try do
Code.eval_string(
"""
Code.eval_string("1 + 1")
raise "boom"
""",
[],
env
)
rescue
_ -> __STACKTRACE__
end
assert Enum.any?(stacktrace, fn
{_, _, _, meta} -> Keyword.get(meta, :file) == ~c"outer_file.ex"
end)
end
end
describe "eval_quoted/1" do
@@ -749,13 +811,13 @@ defmodule CodeTest do
message = "unknown compiler option: :not_a_valid_option"
assert_raise RuntimeError, message, fn ->
Code.put_compiler_option(:not_a_valid_option, :foo)
Code.put_compiler_option(Process.get(:unused, :not_a_valid_option), :ok)
end
message = "compiler option :debug_info should be a boolean, got: :not_a_boolean"
assert_raise RuntimeError, message, fn ->
Code.put_compiler_option(:debug_info, :not_a_boolean)
Code.put_compiler_option(:debug_info, Process.get(:unused, :not_a_boolean))
end
end
@@ -77,8 +77,13 @@ defmodule Config.ProviderTest do
assert Provider.validate_config_path!("/foo") == :ok
assert Provider.validate_config_path!({:system, "foo", "bar"}) == :ok
assert_raise ArgumentError, fn -> Provider.validate_config_path!({:system, 1, 2}) end
assert_raise ArgumentError, fn -> Provider.validate_config_path!(~c"baz") end
assert_raise ArgumentError, fn ->
Provider.validate_config_path!(Process.get(:unused, {:system, 1, 2}))
end
assert_raise ArgumentError, fn ->
Provider.validate_config_path!(Process.get(:unused, ~c"baz"))
end
end
test "resolve!" do
@@ -52,6 +52,19 @@ defmodule Config.ReaderTest do
~r"expected config for app :sample in .*/bad_app.exs to return keyword list",
fn -> Config.Reader.read!(fixture_path("configs/bad_app.exs")) end
assert_raise ArgumentError,
~r"expected config .* to be a keyword list of {atom, keyword} pairs",
fn ->
Config.Reader.eval!(
"nofile",
~s([{"not_atom", [key: :val]}, {:valid, [k: :v]}])
)
end
assert_raise ArgumentError,
~r"expected config .* to be a keyword list of {atom, keyword} pairs",
fn -> Config.Reader.eval!("nofile", "[:not_a_pair]") end
assert_raise RuntimeError, "no :env key was given to this configuration file", fn ->
Config.Reader.read!(fixture_path("configs/env.exs"))
end
+8 -3
View File
@@ -869,21 +869,26 @@ defmodule FileTest do
assert src_mode == dest_mode
end
test "cp_r preserves directory mode" do
test "cp_r preserves directory mode with preserve_directory_permissions: true" do
src = tmp_path("tmp/src_dir")
dest = tmp_path("tmp/dest_dir")
inner = Path.join(src, "inner")
File.mkdir_p!(inner)
File.chmod!(inner, 0o700)
File.write!(Path.join(inner, "hello"), "world")
File.chmod!(inner, 0o500)
try do
File.cp_r!(src, dest)
File.cp_r!(src, dest, preserve_directory_permissions: true)
%File.Stat{mode: src_mode} = File.stat!(inner)
%File.Stat{mode: dest_mode} = File.stat!(Path.join(dest, "inner"))
assert src_mode == dest_mode
assert File.read!(Path.join(dest, "inner/hello")) == "world"
after
File.chmod!(inner, 0o700)
File.chmod!(Path.join(dest, "inner"), 0o700)
File.rm_rf!(src)
File.rm_rf!(dest)
end
@@ -14,6 +14,11 @@ defmodule Dialyzer.Opaqueness do
bar(@my_set)
end
def contains?(ids, id) do
id_set = MapSet.new(ids)
MapSet.member?(id_set, id)
end
# Task.Supervisor returns a Task.t() containing an opaque Task.ref()
@spec run_task() :: Task.t()
def run_task do
+107
View File
@@ -103,6 +103,38 @@ defmodule FloatTest do
assert Float.floor(5.0e-324, precision) === 0.0
end
end
test "with already-exact floats does not bump" do
# The integer rounding step must not add 1 when the truncated remainder
# is exactly zero (e.g. -1.5 has no content below the 1st decimal place).
assert Float.floor(-1.5, 1) === -1.5
assert Float.floor(-1.875, 3) === -1.875
assert Float.floor(-3.0, 5) === -3.0
end
test "with extremely small floats" do
# `tiny_round`: ceil(+) and floor(-) must bump to ±10^-precision.
assert Float.floor(-1.0e-200, 5) === -1.0e-5
assert Float.floor(-1.0e-200, 15) === -1.0e-15
assert Float.floor(1.0e-200, 5) === 0.0
end
test "with already-integer floats" do
# |f| >= 2^52 — fast path returns the float unchanged.
assert Float.floor(1.0e20, 3) === 1.0e20
assert Float.floor(-1.0e20, 3) === -1.0e20
end
test "with very large floats hits the bignum slow path" do
# n = round(|f| * 10^p) >= 2^53 — exercises bignum_to_float / align.
# The slow path must round-trip representable floats back to themselves.
assert Float.floor(2_661_101_816_343_531.5, 1) === 2_661_101_816_343_531.5
assert Float.floor(3.0e15, 1) === 3.0e15
assert Float.floor(-3.0e15, 1) === -3.0e15
assert Float.floor(1.234e15, 3) === 1.234e15
assert Float.floor(1.234567e11, 5) === 1.234567e11
assert Float.floor(-1.234567e11, 5) === -1.234567e11
end
end
test "ceil/1" do
@@ -163,6 +195,32 @@ defmodule FloatTest do
assert Float.ceil(-5.0e-324, precision) === -0.0
end
end
test "with already-exact floats does not bump" do
assert Float.ceil(1.5, 1) === 1.5
assert Float.ceil(1.875, 3) === 1.875
assert Float.ceil(3.0, 5) === 3.0
end
test "with extremely small floats" do
assert Float.ceil(1.0e-200, 5) === 1.0e-5
assert Float.ceil(1.0e-200, 15) === 1.0e-15
assert Float.ceil(-1.0e-200, 5) === -0.0
end
test "with already-integer floats" do
assert Float.ceil(1.0e20, 3) === 1.0e20
assert Float.ceil(-1.0e20, 3) === -1.0e20
end
test "with very large floats hits the bignum slow path" do
assert Float.ceil(2_661_101_816_343_531.5, 1) === 2_661_101_816_343_531.5
assert Float.ceil(3.0e15, 1) === 3.0e15
assert Float.ceil(-3.0e15, 1) === -3.0e15
assert Float.ceil(1.234e15, 3) === 1.234e15
assert Float.ceil(1.234567e11, 5) === 1.234567e11
assert Float.ceil(-1.234567e11, 5) === -1.234567e11
end
end
describe "round/2" do
@@ -203,6 +261,55 @@ defmodule FloatTest do
assert Float.round(-5.0e-324, precision) === -0.0
end
end
test "rounds up across a digit boundary" do
# Rounding pushes the integer answer to 10^precision; the float-emission
# step must produce the next-magnitude value cleanly.
assert Float.round(0.9995, 3) === 1.0
assert Float.round(0.9999, 3) === 1.0
assert Float.round(99.9999, 3) === 100.0
assert Float.round(-99.9999, 3) === -100.0
end
test "with already-integer floats" do
assert Float.round(1.0e20, 3) === 1.0e20
assert Float.round(-1.0e20, 3) === -1.0e20
assert Float.round(3.0e15, 3) === 3.0e15
assert Float.round(-3.0e15, 3) === -3.0e15
end
test "with very large floats hits the bignum slow path" do
assert Float.round(3.0e15, 1) === 3.0e15
assert Float.round(-3.0e15, 1) === -3.0e15
assert Float.round(1.234e15, 3) === 1.234e15
assert Float.round(1.234567e11, 5) === 1.234567e11
assert Float.round(-1.234567e11, 5) === -1.234567e11
assert Float.round(2_251_799_813_685_248.5, 1) === 2_251_799_813_685_248.5
assert Float.round(2_251_799_813_685_249.5, 1) === 2_251_799_813_685_249.5
assert Float.round(-2_251_799_813_685_248.5, 1) === -2_251_799_813_685_248.5
assert Float.round(-2_251_799_813_685_249.5, 1) === -2_251_799_813_685_249.5
end
test "preserves documented tie behavior" do
# The actual binary representation of 5.5675 is 5.567499999..., so
# half-up rounding correctly gives 5.567 (not 5.568).
assert Float.round(5.5675, 3) === 5.567
assert Float.round(-5.5675, 3) === -5.567
assert Float.round(12.5, 0) === 13.0
assert Float.round(-12.5, 0) === -13.0
end
end
test "round/2, floor/2, and ceil/2 preserve powers of two" do
# Powers of two are exactly representable and have no fractional content,
# so rounding at any precision must return the input unchanged.
for k <- 0..60 do
f = :math.pow(2, k)
assert Float.round(f, 1) === f
assert Float.floor(f, 1) === f
assert Float.ceil(f, 1) === f
end
end
describe "ratio/1" do
+30
View File
@@ -268,6 +268,36 @@ defmodule Inspect.TupleTest do
end
end
defmodule Inspect.NativeRecordTest do
use ExUnit.Case, async: true
@moduletag skip: System.otp_release() < "29"
@tag :tmp_dir
test "basic", %{tmp_dir: tmp_dir} do
File.write!("#{tmp_dir}/native_records_test.erl", """
-module(native_records_test).
-export([test/0]).
-record #point{ x = 0.0, y = 0.0 }.
test() ->
#point{}.
""")
{"", 0} = System.cmd("erlc", ["native_records_test.erl"], cd: tmp_dir, stderr_to_stdout: true)
{:module, _} =
:code.load_binary(
:native_records_test,
~c"nofile",
File.read!("#{tmp_dir}/native_records_test.beam")
)
assert inspect(apply(:native_records_test, :test, [])) ==
"#native_records_test:point{x = 0.0,y = 0.0}"
end
end
defmodule Inspect.ListTest do
use ExUnit.Case, async: true
@@ -196,6 +196,12 @@ defmodule Kernel.BinaryTest do
assert_compile_error(message, fn ->
Code.eval_string(~s[<<"foo"::float>>])
end)
assert_compile_error(message, fn ->
# We need to wrap the example below in a module because
# we attempt to continue compilation when inside a function
Code.compile_string("defmodule Repro do\n def run, do: <<(<<1>>)::integer>>\nend")
end)
end
@bitstring <<"foo", 16::4>>
@@ -513,6 +513,11 @@ defmodule Kernel.ComprehensionTest do
bin = <<8, 1, 32, 2, 3>>
assert for(<<s, x::size(s) <- bin>>, into: "", do: <<x::size(s)>>) == <<1>>
assert for(<<s, x::size(s) <- bin>>, into: %{}, do: {s, x}) == %{8 => 1}
# Computed
bin = <<1, 1, 2, 2, 3>>
assert for(<<s, x::size(s * 8) <- bin>>, into: "", do: <<x::size(s * 8)>>) == <<1, 2, 3>>
assert for(<<s, x::size(s * 8) <- bin>>, into: %{}, do: {s, x}) == %{1 => 1, 2 => 515}
end
test "binary for comprehensions where value is not used" do
+15 -3
View File
@@ -267,6 +267,18 @@ defmodule Kernel.ErrorsTest do
)
end
test "invalid literal on bitstring" do
assert_compile_error(
["nofile:1:1", "a bitstring only accepts binaries, numbers, and variables inside a match"],
~c"""
<<
"foo",
[1]
>> = List.flatten([])
"""
)
end
test "literal on map and struct" do
assert_compile_error(
["nofile:1:10", "expected key-value pairs in a map, got: put_in(foo.bar.baz, nil)"],
@@ -662,7 +674,7 @@ defmodule Kernel.ErrorsTest do
test "function import conflict" do
assert_compile_error(
["nofile:3:16", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"],
["nofile:3:16", "conflicting exit/1 import from modules :erlang and Kernel"],
~c"""
defmodule Kernel.ErrorsTest.FunctionImportConflict do
import :erlang, only: [exit: 1], warn: false
@@ -672,7 +684,7 @@ defmodule Kernel.ErrorsTest do
)
assert_compile_error(
["nofile:3:17", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"],
["nofile:3:17", "conflicting exit/1 import from modules :erlang and Kernel"],
~c"""
defmodule Kernel.ErrorsTest.FunctionImportConflict do
import :erlang, only: [exit: 1], warn: false
@@ -686,7 +698,7 @@ defmodule Kernel.ErrorsTest do
assert_compile_error(
[
"nofile:3:3",
"invalid :only option for import, expected value to be an atom :functions, :macros, or a literal keyword list of function names with arity as values, got: x"
"invalid :only option for import, expected value to be an atom :functions, :macros, :sigils, or a literal keyword list of function names with arity as values, got: x"
],
~c"""
defmodule Kernel.ErrorsTest.Only do
@@ -316,16 +316,6 @@ defmodule Kernel.ExpansionTest do
end)
end
test "expands vars to local call when :on_undefined_variable is :warn" do
Code.put_compiler_option(:on_undefined_variable, :warn)
{output, env} = expand_env({:a, [], nil}, __ENV__, [])
assert output == {:a, [if_undefined: :warn], []}
assert Macro.Env.vars(env) == []
after
Code.put_compiler_option(:on_undefined_variable, :raise)
end
test "expands vars to local call without warning" do
env = __ENV__
@@ -397,8 +387,8 @@ defmodule Kernel.ExpansionTest do
[
{:=, _, [var_ver(:x, 0), 0]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 1), 1]},
{:=, _, [_, var_ver(:x, 1)]}
{:=, _, [var_ver(:x, 2), 1]},
{:=, _, [_, var_ver(:x, 2)]}
]} =
expand_with_version(
quote do
@@ -414,7 +404,7 @@ defmodule Kernel.ExpansionTest do
{:=, _, [var_ver(:x, 0), 0]},
{:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 3), 3]}
{:=, _, [var_ver(:x, 5), 3]}
]} =
expand_with_version(
quote do
@@ -430,7 +420,7 @@ defmodule Kernel.ExpansionTest do
{:=, _, [var_ver(:x, 0), 0]},
{:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 2), 2]}
{:=, _, [var_ver(:x, 4), 2]}
]} =
expand_with_version(
quote do
@@ -1227,8 +1217,8 @@ defmodule Kernel.ExpansionTest do
[
{:->, [line: 1],
[
[{:capture, [capture: 1, line: 1], nil}],
{:capture, [capture: 1, line: 1], nil}
[{:"_&", [capture: 1, line: 1], :elixir_fn}],
{:"_&", [capture: 1, line: 1], :elixir_fn}
]}
]}
end
@@ -483,6 +483,44 @@ defmodule Kernel.GuardTest do
"""
end
defguard with_or_and_or(foo, bar, baz) when foo or (bar and (baz or foo))
test "expands conditionals" do
args = quote(do: [1 + 1, 2 + 2, 3 + 3])
assert expand_defguard_to_string(:with_or_and_or, args, :guard) == """
:erlang.orelse(1 + 1, :erlang.andalso(2 + 2, :erlang.orelse(3 + 3, 1 + 1)))
"""
assert expand_defguard_to_string(:with_or_and_or, args, nil) == """
{arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3}
case arg1 do
false ->
case arg2 do
false ->
false
true ->
case arg3 do
false -> arg1
true -> true
other -> :erlang.error({:badbool, :or, other})
end
other ->
:erlang.error({:badbool, :and, other})
end
true ->
true
other ->
:erlang.error({:badbool, :or, other})
end
"""
end
defguard in_list(a) when Kernel.in(a, [:test])
test "expands remote functions" do

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